diff --git a/README.md b/README.md index 5ee205c..e994007 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,12 @@ agent review init # scaffold a starter review pipeline (code_rev agent config list # list saved agent configs agent config get # show one config as YAML (--json for JSON) agent config init [path] # scaffold a starter config (default: agents/my_agent.yaml) -agent config create --repo api --file agents/foo.yaml # create an agent via a pull request (or --template ) +agent config create --file agents/foo.yaml # create an agent, live at once (or --template ) +agent config create --repo api --file agents/foo.yaml # instead define it as a file, via a pull request +agent config edit --file agents/foo.yaml # replace its definition, live at once +agent config delete # delete it; the agent stops and its name is freed +agent config link --repo api # move it into a repository, via a pull request +agent config unlink # take it over from its file, so the API changes it agent config default # the effective default agent for the repo you are standing in agent config default set # set the account default agent (--repo [owner/name] for one repo) agent config default clear # clear the account default (--repo [owner/name] for one repo) diff --git a/bun.lock b/bun.lock index 74b3332..fa9b651 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@ellipsis/cli", "dependencies": { - "@ellipsis-dev/sdk": "^0.8.1", + "@ellipsis-dev/sdk": "^0.11.0", "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^12.1.0", @@ -35,7 +35,7 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@ellipsis-dev/sdk": ["@ellipsis-dev/sdk@0.8.1", "", {}, "sha512-ZMpg+IXWgTSbsi1JPG82M7O1WktIDTR577yrQ+CM94ZKV0zgUTnziVBGP4PGqlqyGcscOImfj5ydeOUlZjBUew=="], + "@ellipsis-dev/sdk": ["@ellipsis-dev/sdk@0.11.0", "", {}, "sha512-Oj+ptXTWdzqDcSNhDY32RO7gCJPJPkyv2D3eJV5aNNn4GVu9wNSpJHs6yDfCOAX1V36M7Wr6F11oDnmS47knzw=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], diff --git a/package.json b/package.json index 35cd9e1..e8ad188 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:watch": "vitest" }, "dependencies": { - "@ellipsis-dev/sdk": "^0.10.0", + "@ellipsis-dev/sdk": "^0.11.0", "chalk": "^5.6.2", "cli-table3": "^0.6.5", "commander": "^12.1.0", diff --git a/skills/ellipsis/SKILL.md b/skills/ellipsis/SKILL.md index 47a6365..9c4ca88 100644 --- a/skills/ellipsis/SKILL.md +++ b/skills/ellipsis/SKILL.md @@ -444,14 +444,24 @@ Author and deploy agents: agent config init agents/my_agent.yaml # scaffold a starter config locally agent config list # saved configs with their source file agent config get # one config as YAML -agent config create --repo api --file agents/my_agent.yaml # deploy via a pull request +agent config create --file agents/my_agent.yaml # create it, live at once +agent config edit --file agents/my_agent.yaml # replace its definition, live at once +agent config delete # delete it; it stops and frees its name +agent config link --repo api # move it into a repo, via a pull request +agent config unlink # take it over from its file agent config default set # the account default (--repo for one repo) agent template list # built-in templates and their slugs agent model list # the model ids valid under claude.model ``` -`agent config create` opens a pull request adding the file, exactly as the -dashboard does; the agent goes live when it merges. +An agent is owned by one of two writers, and that is what these verbs move. +`agent config create` with no `--repo` creates it through the API alone: no +file, live immediately, changed by `config edit`. With `--repo` it instead +opens a pull request adding the file, exactly as the dashboard does, and the +agent goes live when that merges — thereafter the file is what changes it, and +`config edit` is refused. `config link` moves an API-owned agent into a +repository (by pull request; it keeps running unchanged until the merge) and +`config unlink` takes one back from its file, leaving the file in place, inert. Platform and integrations: diff --git a/src/commands/config.ts b/src/commands/config.ts index 1fad4d3..c534e31 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -12,6 +12,7 @@ import type { AgentConfig, AgentDefaultView, CreateAgentConfigRequest, + CreatedAgentConfig, SavedAgentConfig, } from '../lib/types' @@ -82,18 +83,20 @@ export function registerConfig(program: Command): void { }) }) - // Create an agent config the same way the dashboard does: Ellipsis opens a - // pull request adding the YAML to the repo, and the agent goes live when it - // merges. Distinct from `config init`, which scaffolds a local file. + // Create an agent. Two shapes, chosen by --repo: with it, Ellipsis opens a + // pull request adding the YAML to that repo and the agent goes live when it + // merges (what the dashboard has always done); without it, the agent is + // created through the API alone — no file, live at once, changed by `config + // edit`. Distinct from `config init`, which scaffolds a local file. apiRoutes( config .command('create') - .description('Create an agent config by opening a pull request that adds it to a repo'), + .description('Create an agent, live immediately or by pull request with --repo'), 'POST /agents/configs', ) - .requiredOption( + .option( '-r, --repo ', - 'repository in your account to open the pull request against', + 'define the agent as a file in this repository, by pull request (default: no file, live at once)', ) .option('-f, --file ', 'agent config file (.yaml/.yml or .json) to add') .option( @@ -102,12 +105,12 @@ export function registerConfig(program: Command): void { ) .option( '--path ', - 'file path within the repo for the config (default: agents/.yaml; must be a synced location)', + 'file path within the repo for the config (default: agents/.yaml; must be a synced location; needs --repo)', ) .option('--json', 'output raw JSON') .action( async (opts: { - repo: string + repo?: string file?: string template?: string path?: string @@ -119,6 +122,9 @@ export function registerConfig(program: Command): void { if (!opts.file === !opts.template) { throw new Error('provide exactly one of --file or --template ') } + if (opts.path && !opts.repo) { + throw new Error('--path names a location in a repository, so it needs --repo ') + } const req: CreateAgentConfigRequest = { repository: opts.repo, path: opts.path, @@ -130,13 +136,109 @@ export function registerConfig(program: Command): void { printJson(created) return } - console.log(`✓ opened a pull request adding the agent config (${created.path})`) - console.log(created.pull_request_url) - console.log('Merge it to deploy the agent.') + printCreated(created) + }) + }, + ) + + // Replace an API-managed agent's whole definition, live at once. Refused for + // an agent defined by a repository file (the next push would revert it) — + // `config unlink` takes ownership first. + apiRoutes( + alsoKnownAs( + config + .command('edit ') + .description("Replace an API-managed agent's definition from a file, live immediately"), + 'update', + ), + 'PUT /agents/configs/{id}', + ) + .requiredOption('-f, --file ', 'agent config file (.yaml/.yml or .json) to replace it with') + .option('--json', 'output raw JSON') + .action(async (configId: string, opts: { file: string; json?: boolean }) => { + await runAction(async () => { + const { config: updated } = await api().agents.configs.update(configId, { + config: readConfigFile(opts.file) as AgentConfig, + }) + if (opts.json) { + printJson(updated) + return + } + console.log(`✓ updated "${configName(updated)}" (${updated.id}) — live now`) + }) + }) + + apiRoutes( + alsoKnownAs( + config + .command('delete ') + .description('Delete an API-managed agent; it stops running and frees its name'), + 'rm', + ), + 'DELETE /agents/configs/{id}', + ) + .option('--json', 'output raw JSON') + .action(async (configId: string, opts: { json?: boolean }) => { + await runAction(async () => { + await api().agents.configs.delete(configId) + // 204 No Content — nothing to echo, so confirm with what was addressed. + if (opts.json) printJson({ id: configId, deleted: true }) + else console.log(`✓ deleted ${configId}`) + }) + }) + + // The two ownership moves. `link` hands an API-managed agent over to a file + // (by pull request; it keeps running unchanged until that merges); `unlink` + // takes one back from its file (immediate, and the file is left inert). + apiRoutes( + config + .command('link ') + .description('Move an agent into a repository by opening a pull request that adds its file'), + 'POST /agents/configs/{id}/link', + ) + .requiredOption('-r, --repo ', 'repository in your account to move the agent into') + .option( + '--path ', + 'file path within the repo for the config (default: agents/.yaml; must be a synced location)', + ) + .option('--json', 'output raw JSON') + .action( + async (configId: string, opts: { repo: string; path?: string; json?: boolean }) => { + await runAction(async () => { + const linked = await api().agents.configs.link(configId, { + repository: opts.repo, + path: opts.path, + }) + if (opts.json) { + printJson(linked) + return + } + console.log(`✓ opened a pull request adding the agent config (${linked.path})`) + console.log(linked.pull_request_url) + console.log('The agent keeps running meanwhile; merging hands it over to the file.') }) }, ) + apiRoutes( + config + .command('unlink ') + .description('Take an agent over from its file, so this API changes it instead'), + 'POST /agents/configs/{id}/unlink', + ) + .option('--json', 'output raw JSON') + .action(async (configId: string, opts: { json?: boolean }) => { + await runAction(async () => { + const { config: unlinked } = await api().agents.configs.unlink(configId) + if (opts.json) { + printJson(unlinked) + return + } + console.log(`✓ took over "${configName(unlinked)}" (${unlinked.id})`) + console.log('Its file no longer governs it and is left in place, inert.') + }) + }) + // ------------------------------- defaults -------------------------------- // The default-config ladder a bare session start resolves: repo default -> // account default -> the bare platform config. Rung-addressed, never row @@ -313,14 +415,13 @@ export function registerConfig(program: Command): void { return } await runAction(async () => { - const created = await api().agents.configs.create({ - template_id: opts.template, - repository: opts.repo!, - path: opts.path, - }) - console.log(`✓ opened a pull request adding the agent config (${created.path})`) - console.log(created.pull_request_url) - console.log('Merge it to deploy the agent.') + printCreated( + await api().agents.configs.create({ + template_id: opts.template, + repository: opts.repo!, + path: opts.path, + }), + ) }) return } @@ -342,6 +443,23 @@ export function registerConfig(program: Command): void { const COMMIT_HINT = 'Commit it to your default branch. Ellipsis syncs agent configs from GitHub.' +// A create answers two ways: with a repository the agent waits on a pull +// request, without one it is already live and has no file. +function printCreated(created: CreatedAgentConfig): void { + if (created.pull_request_url) { + console.log(`✓ opened a pull request adding the agent config (${created.path})`) + console.log(created.pull_request_url) + console.log('Merge it to deploy the agent.') + return + } + console.log(`✓ created "${configName(created.config)}" (${created.config.id}) — live now`) + console.log('It has no file; change it with `agent config edit`, or `agent config link` to move it into a repo.') +} + +function configName(c: SavedAgentConfig): string { + return c.agent_config.ellipsis.name ?? c.id +} + // --repo semantics on defaults mutations: absent -> the account rung; bare // --repo -> the repo you're standing in (from the origin remote, an error // when there isn't one); --repo owner/name -> that repo. Shared with @@ -397,13 +515,15 @@ claude: } // GitHub source as `path@branch` (repo is only an opaque numeric id in the API). -// Prefixed with ⚠ when the last sync failed so it stands out in the list. +// Prefixed with ⚠ when the last sync failed so it stands out in the list. An +// API-managed agent has no file at all, which is a different thing from a +// github-managed one whose source is momentarily unknown — so name it. function configSource(c: SavedAgentConfig): string { const s = c.agent_config_source_details as | { repo_id: number; path: string; branch: string } | null | undefined - const base = s ? `${s.path}@${s.branch}` : '—' + const base = s ? `${s.path}@${s.branch}` : c.managed_by === 'api' ? 'api' : '—' return c.last_sync_error ? `⚠ ${base}` : base } diff --git a/src/lib/types.ts b/src/lib/types.ts index dcfae0f..b181537 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -60,6 +60,8 @@ export type SavedAgentConfig = S['Config'] export type ListAgentConfigsResponse = S['AgentConfigsListResponse'] export type CreateAgentConfigRequest = Parameters[0] export type CreatedAgentConfig = S['CreateAgentConfigResponse'] +export type ConfigManagedBy = S['ConfigManagedBy'] +export type LinkedAgentConfig = S['LinkAgentConfigResponse'] export type AgentDefaultView = S['AgentDefault'] export type ListAgentDefaultsResponse = S['AgentDefaultsListResponse'] export type PutAgentDefaultRequest = S['PutAgentDefaultRequest']