@@ -12,6 +12,7 @@ import type {
1212 AgentConfig ,
1313 AgentDefaultView ,
1414 CreateAgentConfigRequest ,
15+ CreatedAgentConfig ,
1516 SavedAgentConfig ,
1617} from '../lib/types'
1718
@@ -82,18 +83,20 @@ export function registerConfig(program: Command): void {
8283 } )
8384 } )
8485
85- // Create an agent config the same way the dashboard does: Ellipsis opens a
86- // pull request adding the YAML to the repo, and the agent goes live when it
87- // merges. Distinct from `config init`, which scaffolds a local file.
86+ // Create an agent. Two shapes, chosen by --repo: with it, Ellipsis opens a
87+ // pull request adding the YAML to that repo and the agent goes live when it
88+ // merges (what the dashboard has always done); without it, the agent is
89+ // created through the API alone — no file, live at once, changed by `config
90+ // edit`. Distinct from `config init`, which scaffolds a local file.
8891 apiRoutes (
8992 config
9093 . command ( 'create' )
91- . description ( 'Create an agent config by opening a pull request that adds it to a repo' ) ,
94+ . description ( 'Create an agent, live immediately or by pull request with -- repo' ) ,
9295 'POST /agents/configs' ,
9396 )
94- . requiredOption (
97+ . option (
9598 '-r, --repo <name>' ,
96- 'repository in your account to open the pull request against ' ,
99+ 'define the agent as a file in this repository, by pull request (default: no file, live at once) ' ,
97100 )
98101 . option ( '-f, --file <path>' , 'agent config file (.yaml/.yml or .json) to add' )
99102 . option (
@@ -102,12 +105,12 @@ export function registerConfig(program: Command): void {
102105 )
103106 . option (
104107 '--path <path>' ,
105- 'file path within the repo for the config (default: agents/<slug>.yaml; must be a synced location)' ,
108+ 'file path within the repo for the config (default: agents/<slug>.yaml; must be a synced location; needs --repo )' ,
106109 )
107110 . option ( '--json' , 'output raw JSON' )
108111 . action (
109112 async ( opts : {
110- repo : string
113+ repo ? : string
111114 file ?: string
112115 template ?: string
113116 path ?: string
@@ -119,6 +122,9 @@ export function registerConfig(program: Command): void {
119122 if ( ! opts . file === ! opts . template ) {
120123 throw new Error ( 'provide exactly one of --file <path> or --template <slug>' )
121124 }
125+ if ( opts . path && ! opts . repo ) {
126+ throw new Error ( '--path names a location in a repository, so it needs --repo <name>' )
127+ }
122128 const req : CreateAgentConfigRequest = {
123129 repository : opts . repo ,
124130 path : opts . path ,
@@ -130,13 +136,109 @@ export function registerConfig(program: Command): void {
130136 printJson ( created )
131137 return
132138 }
133- console . log ( `✓ opened a pull request adding the agent config (${ created . path } )` )
134- console . log ( created . pull_request_url )
135- console . log ( 'Merge it to deploy the agent.' )
139+ printCreated ( created )
140+ } )
141+ } ,
142+ )
143+
144+ // Replace an API-managed agent's whole definition, live at once. Refused for
145+ // an agent defined by a repository file (the next push would revert it) —
146+ // `config unlink` takes ownership first.
147+ apiRoutes (
148+ alsoKnownAs (
149+ config
150+ . command ( 'edit <config-id>' )
151+ . description ( "Replace an API-managed agent's definition from a file, live immediately" ) ,
152+ 'update' ,
153+ ) ,
154+ 'PUT /agents/configs/{id}' ,
155+ )
156+ . requiredOption ( '-f, --file <path>' , 'agent config file (.yaml/.yml or .json) to replace it with' )
157+ . option ( '--json' , 'output raw JSON' )
158+ . action ( async ( configId : string , opts : { file : string ; json ?: boolean } ) => {
159+ await runAction ( async ( ) => {
160+ const { config : updated } = await api ( ) . agents . configs . update ( configId , {
161+ config : readConfigFile ( opts . file ) as AgentConfig ,
162+ } )
163+ if ( opts . json ) {
164+ printJson ( updated )
165+ return
166+ }
167+ console . log ( `✓ updated "${ configName ( updated ) } " (${ updated . id } ) — live now` )
168+ } )
169+ } )
170+
171+ apiRoutes (
172+ alsoKnownAs (
173+ config
174+ . command ( 'delete <config-id>' )
175+ . description ( 'Delete an API-managed agent; it stops running and frees its name' ) ,
176+ 'rm' ,
177+ ) ,
178+ 'DELETE /agents/configs/{id}' ,
179+ )
180+ . option ( '--json' , 'output raw JSON' )
181+ . action ( async ( configId : string , opts : { json ?: boolean } ) => {
182+ await runAction ( async ( ) => {
183+ await api ( ) . agents . configs . delete ( configId )
184+ // 204 No Content — nothing to echo, so confirm with what was addressed.
185+ if ( opts . json ) printJson ( { id : configId , deleted : true } )
186+ else console . log ( `✓ deleted ${ configId } ` )
187+ } )
188+ } )
189+
190+ // The two ownership moves. `link` hands an API-managed agent over to a file
191+ // (by pull request; it keeps running unchanged until that merges); `unlink`
192+ // takes one back from its file (immediate, and the file is left inert).
193+ apiRoutes (
194+ config
195+ . command ( 'link <config-id>' )
196+ . description ( 'Move an agent into a repository by opening a pull request that adds its file' ) ,
197+ 'POST /agents/configs/{id}/link' ,
198+ )
199+ . requiredOption ( '-r, --repo <name>' , 'repository in your account to move the agent into' )
200+ . option (
201+ '--path <path>' ,
202+ 'file path within the repo for the config (default: agents/<slug>.yaml; must be a synced location)' ,
203+ )
204+ . option ( '--json' , 'output raw JSON' )
205+ . action (
206+ async ( configId : string , opts : { repo : string ; path ?: string ; json ?: boolean } ) => {
207+ await runAction ( async ( ) => {
208+ const linked = await api ( ) . agents . configs . link ( configId , {
209+ repository : opts . repo ,
210+ path : opts . path ,
211+ } )
212+ if ( opts . json ) {
213+ printJson ( linked )
214+ return
215+ }
216+ console . log ( `✓ opened a pull request adding the agent config (${ linked . path } )` )
217+ console . log ( linked . pull_request_url )
218+ console . log ( 'The agent keeps running meanwhile; merging hands it over to the file.' )
136219 } )
137220 } ,
138221 )
139222
223+ apiRoutes (
224+ config
225+ . command ( 'unlink <config-id>' )
226+ . description ( 'Take an agent over from its file, so this API changes it instead' ) ,
227+ 'POST /agents/configs/{id}/unlink' ,
228+ )
229+ . option ( '--json' , 'output raw JSON' )
230+ . action ( async ( configId : string , opts : { json ?: boolean } ) => {
231+ await runAction ( async ( ) => {
232+ const { config : unlinked } = await api ( ) . agents . configs . unlink ( configId )
233+ if ( opts . json ) {
234+ printJson ( unlinked )
235+ return
236+ }
237+ console . log ( `✓ took over "${ configName ( unlinked ) } " (${ unlinked . id } )` )
238+ console . log ( 'Its file no longer governs it and is left in place, inert.' )
239+ } )
240+ } )
241+
140242 // ------------------------------- defaults --------------------------------
141243 // The default-config ladder a bare session start resolves: repo default ->
142244 // account default -> the bare platform config. Rung-addressed, never row
@@ -313,14 +415,13 @@ export function registerConfig(program: Command): void {
313415 return
314416 }
315417 await runAction ( async ( ) => {
316- const created = await api ( ) . agents . configs . create ( {
317- template_id : opts . template ,
318- repository : opts . repo ! ,
319- path : opts . path ,
320- } )
321- console . log ( `✓ opened a pull request adding the agent config (${ created . path } )` )
322- console . log ( created . pull_request_url )
323- console . log ( 'Merge it to deploy the agent.' )
418+ printCreated (
419+ await api ( ) . agents . configs . create ( {
420+ template_id : opts . template ,
421+ repository : opts . repo ! ,
422+ path : opts . path ,
423+ } ) ,
424+ )
324425 } )
325426 return
326427 }
@@ -342,6 +443,23 @@ export function registerConfig(program: Command): void {
342443const COMMIT_HINT =
343444 'Commit it to your default branch. Ellipsis syncs agent configs from GitHub.'
344445
446+ // A create answers two ways: with a repository the agent waits on a pull
447+ // request, without one it is already live and has no file.
448+ function printCreated ( created : CreatedAgentConfig ) : void {
449+ if ( created . pull_request_url ) {
450+ console . log ( `✓ opened a pull request adding the agent config (${ created . path } )` )
451+ console . log ( created . pull_request_url )
452+ console . log ( 'Merge it to deploy the agent.' )
453+ return
454+ }
455+ console . log ( `✓ created "${ configName ( created . config ) } " (${ created . config . id } ) — live now` )
456+ console . log ( 'It has no file; change it with `agent config edit`, or `agent config link` to move it into a repo.' )
457+ }
458+
459+ function configName ( c : SavedAgentConfig ) : string {
460+ return c . agent_config . ellipsis . name ?? c . id
461+ }
462+
345463// --repo semantics on defaults mutations: absent -> the account rung; bare
346464// --repo -> the repo you're standing in (from the origin remote, an error
347465// when there isn't one); --repo owner/name -> that repo. Shared with
@@ -397,13 +515,15 @@ claude:
397515}
398516
399517// GitHub source as `path@branch` (repo is only an opaque numeric id in the API).
400- // Prefixed with ⚠ when the last sync failed so it stands out in the list.
518+ // Prefixed with ⚠ when the last sync failed so it stands out in the list. An
519+ // API-managed agent has no file at all, which is a different thing from a
520+ // github-managed one whose source is momentarily unknown — so name it.
401521function configSource ( c : SavedAgentConfig ) : string {
402522 const s = c . agent_config_source_details as
403523 | { repo_id : number ; path : string ; branch : string }
404524 | null
405525 | undefined
406- const base = s ? `${ s . path } @${ s . branch } ` : '—'
526+ const base = s ? `${ s . path } @${ s . branch } ` : c . managed_by === 'api' ? 'api' : '—'
407527 return c . last_sync_error ? `⚠ ${ base } ` : base
408528}
409529
0 commit comments