@ekanos/cli 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/apps-api.d.ts +27 -0
  3. package/dist/apps-api.js +80 -0
  4. package/dist/apps-api.js.map +1 -0
  5. package/dist/claude-docs.d.ts +39 -0
  6. package/dist/claude-docs.js +70 -0
  7. package/dist/claude-docs.js.map +1 -0
  8. package/dist/commands/agents-init.d.ts +34 -0
  9. package/dist/commands/agents-init.js +119 -0
  10. package/dist/commands/agents-init.js.map +1 -0
  11. package/dist/commands/agents.d.ts +5 -1
  12. package/dist/commands/agents.js +18 -5
  13. package/dist/commands/agents.js.map +1 -1
  14. package/dist/commands/apps.d.ts +25 -0
  15. package/dist/commands/apps.js +155 -0
  16. package/dist/commands/apps.js.map +1 -0
  17. package/dist/commands/catalog.js +3 -2
  18. package/dist/commands/catalog.js.map +1 -1
  19. package/dist/commands/init.d.ts +1 -1
  20. package/dist/commands/init.js +2 -1
  21. package/dist/commands/init.js.map +1 -1
  22. package/dist/commands/login.d.ts +4 -0
  23. package/dist/commands/login.js +16 -3
  24. package/dist/commands/login.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +34 -2
  27. package/dist/index.js.map +1 -1
  28. package/package.json +2 -2
  29. package/templates/agents-claude.md.tmpl +14 -0
  30. package/templates/claude-skill.md.tmpl +4 -8
  31. package/templates/package.json.tmpl +4 -4
  32. package/templates/skills/build-agent/SKILL.md +118 -0
  33. package/templates/skills/build-agent/references/agent-json.md +85 -0
  34. package/templates/skills/build-agent/references/rollout.md +53 -0
  35. package/templates/skills/build-agent/references/triggers.md +47 -0
  36. package/templates/skills/ekanos-platform/SKILL.md +107 -0
package/README.md CHANGED
@@ -28,7 +28,7 @@ npx @ekanos/cli dev
28
28
  | `status` | The read verb: login state for the resolved host, this project's `ekanos.json`, and its submissions on the host (`GET /api/partner/submissions`). Never fails just because you are logged out — it reports `authenticated: false` instead. |
29
29
  | `sources` | List every Ekanos source you hold a developer or source-admin seat on (`GET /api/partner/sources`), with a marker on this project's current publish target. |
30
30
  | `catalog` | `ekanos catalog [<product-slug>]` — what a source can already use (`GET /api/partner/catalog`): every product enabled, switched off or available to enable, with its tools, agent triggers and generated setup steps. Check it before building something the source may already have. |
31
- | `agents` | `ekanos agents list | show | pull | apply | publish | rollback` — the source's agents, held by Ekanos (`/api/partner/agents`). `apply` stores a new version as a draft, `publish` makes a version the one every team runs, `rollback` publishes an earlier one. Either seat applies; only the source admin publishes.
31
+ | `agents` | `ekanos agents init | list | show | pull | apply | publish | rollback` — the source's agents, held by Ekanos (`/api/partner/agents`). `init` sets up a folder for designing agents with Claude (the `build-agent` and `ekanos-platform` skills, the docs MCP server, a CLAUDE.md) without scaffolding an integration. `apply` stores a new version as a draft, `publish` makes a version the one every team runs, `rollback` publishes an earlier one. Either seat applies; only the source admin publishes.
32
32
  | `use` | `ekanos use <source-slug>` — set this project's publish target to a source you hold a seat on. Refuses (`forbidden`, with your actual seats in the hint) for a slug you don't hold a seat on, so a typo can't silently repoint a project. |
33
33
  | `publish` | Validate, pack and submit the integration to an Ekanos host — refusing (`forbidden`) a target you hold no seat on, and confirming an ambiguous multi-seat target before submitting. See [Publish target verification](#publish-target-verification). |
34
34
  | `upgrade` | Move this toolchain to a coherent target version, with verify-and-rollback. `--check` reports current vs. target and changes nothing; a bare `upgrade` installs, verifies (`validate` + the test script), and rolls back automatically on any failure. See [Keeping the toolchain current](#keeping-the-toolchain-current). |
@@ -0,0 +1,27 @@
1
+ import { type ProviderAppSetResult, type ProviderAppStatus } from '@ekanos/integration-schema';
2
+ /**
3
+ * The CLI's client for a Source's provider apps — the OAuth client an
4
+ * integration signs every team in through.
5
+ *
6
+ * GET /api/partner/apps/<product>?source=<slug|id> → { ok, data: ProviderAppStatus }
7
+ * PUT /api/partner/apps/<product> { source, clientId, clientSecret }
8
+ * → { ok, data: ProviderAppStatus & { replaced } }
9
+ *
10
+ * → 401 / 3xx refresh once and retry
11
+ * → 403 { code: "SOURCE_ADMIN_REQUIRED" | "MFA_REQUIRED" | "FORBIDDEN" }
12
+ * → 404 { code: "PRODUCT_NOT_ENTITLED" | "SOURCE_NOT_FOUND" }
13
+ * → 409 { code: "NO_PROVIDER_APP" }
14
+ */
15
+ export type AppsApiResult<T> = {
16
+ status: 'ok';
17
+ data: T;
18
+ } | {
19
+ status: 'unauthorized';
20
+ };
21
+ export declare function showProviderApp(host: string, accessToken: string, source: string, product: string): Promise<AppsApiResult<ProviderAppStatus>>;
22
+ export declare function setProviderApp(host: string, accessToken: string, body: {
23
+ source: string;
24
+ product: string;
25
+ clientId: string;
26
+ clientSecret: string;
27
+ }): Promise<AppsApiResult<ProviderAppSetResult>>;
@@ -0,0 +1,80 @@
1
+ import { ProviderAppSetResultSchema, ProviderAppStatusSchema, } from '@ekanos/integration-schema';
2
+ import { request } from './auth/ekanos-api.js';
3
+ import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, } from './errors.js';
4
+ export function showProviderApp(host, accessToken, source, product) {
5
+ return call(host, accessToken, ProviderAppStatusSchema, product, {
6
+ method: 'GET',
7
+ path: `${route(product)}?${new URLSearchParams({ source })}`,
8
+ });
9
+ }
10
+ export function setProviderApp(host, accessToken, body) {
11
+ return call(host, accessToken, ProviderAppSetResultSchema, body.product, {
12
+ method: 'PUT',
13
+ path: route(body.product),
14
+ body: {
15
+ source: body.source,
16
+ clientId: body.clientId,
17
+ clientSecret: body.clientSecret,
18
+ },
19
+ });
20
+ }
21
+ function route(product) {
22
+ return `/api/partner/apps/${encodeURIComponent(product)}`;
23
+ }
24
+ async function call(host, accessToken, schema, product, init) {
25
+ const response = await request(`${host}${init.path}`, {
26
+ method: init.method,
27
+ headers: Object.assign({ accept: 'application/json', authorization: `Bearer ${accessToken}` }, (init.body === undefined
28
+ ? {}
29
+ : { 'content-type': 'application/json' })),
30
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
31
+ });
32
+ if (response.status === 401 ||
33
+ (response.status >= 300 && response.status < 400)) {
34
+ return { status: 'unauthorized' };
35
+ }
36
+ const body = await readOptionalJson(response);
37
+ const code = typeof body.code === 'string' ? body.code : null;
38
+ const message = typeof body.error === 'string' && body.error ? body.error : null;
39
+ if (response.status === 403 && code === 'MFA_REQUIRED') {
40
+ throw authRequiredError(message !== null && message !== void 0 ? message : `${host} requires multi-factor authentication.`, 'Complete MFA in the browser, then run "ekanos login" again.');
41
+ }
42
+ if (response.status === 403) {
43
+ throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
44
+ ? "Only a Source admin manages the Source's apps. Ask one to run it."
45
+ : 'Run "ekanos sources" to list the sources you hold a seat on.');
46
+ }
47
+ if (response.status === 404) {
48
+ throw notFoundError(message !== null && message !== void 0 ? message : `"${product}" was not found.`, code === 'PRODUCT_NOT_ENTITLED'
49
+ ? `"ekanos catalog" lists what this Source has; Ekanos enables a product for a Source.`
50
+ : 'Run "ekanos sources" to list the sources you hold a seat on.');
51
+ }
52
+ if (response.status === 409 && code === 'NO_PROVIDER_APP') {
53
+ throw invalidStateError(message !== null && message !== void 0 ? message : `"${product}" has no app for the Source to set up.`, `Each team connects ${product} with its own app, from the team's Integrations page. "ekanos catalog ${product}" shows its setup steps.`);
54
+ }
55
+ if (response.status === 400) {
56
+ throw usageError(message !== null && message !== void 0 ? message : 'The host refused the request.', 'See "ekanos apps --help".');
57
+ }
58
+ if (!response.ok) {
59
+ throw networkError(`${host}${init.path.split('?')[0]} responded ${response.status}.`, response.status >= 500
60
+ ? 'The Ekanos deployment returned a server error. Retry shortly.'
61
+ : 'Confirm the host runs a build that includes the apps routes.');
62
+ }
63
+ const parsed = schema.safeParse(body.data);
64
+ if (!parsed.success) {
65
+ throw networkError(`${host}/api/partner/apps answered in a shape this CLI does not read.`, 'Run "ekanos upgrade" — the CLI and the host disagree on the apps contract.');
66
+ }
67
+ return { status: 'ok', data: parsed.data };
68
+ }
69
+ async function readOptionalJson(response) {
70
+ try {
71
+ const parsed = await response.json();
72
+ return typeof parsed === 'object' && parsed !== null
73
+ ? parsed
74
+ : {};
75
+ }
76
+ catch (_a) {
77
+ return {};
78
+ }
79
+ }
80
+ //# sourceMappingURL=apps-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps-api.js","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ProviderAppSetResult,\n ProviderAppSetResultSchema,\n type ProviderAppStatus,\n ProviderAppStatusSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's provider apps — the OAuth client an\n * integration signs every team in through.\n *\n * GET /api/partner/apps/<product>?source=<slug|id> → { ok, data: ProviderAppStatus }\n * PUT /api/partner/apps/<product> { source, clientId, clientSecret }\n * → { ok, data: ProviderAppStatus & { replaced } }\n *\n * → 401 / 3xx refresh once and retry\n * → 403 { code: \"SOURCE_ADMIN_REQUIRED\" | \"MFA_REQUIRED\" | \"FORBIDDEN\" }\n * → 404 { code: \"PRODUCT_NOT_ENTITLED\" | \"SOURCE_NOT_FOUND\" }\n * → 409 { code: \"NO_PROVIDER_APP\" }\n */\n\nexport type AppsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nexport function showProviderApp(\n host: string,\n accessToken: string,\n source: string,\n product: string,\n): Promise<AppsApiResult<ProviderAppStatus>> {\n return call(host, accessToken, ProviderAppStatusSchema, product, {\n method: 'GET',\n path: `${route(product)}?${new URLSearchParams({ source })}`,\n });\n}\n\nexport function setProviderApp(\n host: string,\n accessToken: string,\n body: {\n source: string;\n product: string;\n clientId: string;\n clientSecret: string;\n },\n): Promise<AppsApiResult<ProviderAppSetResult>> {\n return call(host, accessToken, ProviderAppSetResultSchema, body.product, {\n method: 'PUT',\n path: route(body.product),\n body: {\n source: body.source,\n clientId: body.clientId,\n clientSecret: body.clientSecret,\n },\n });\n}\n\nfunction route(product: string): string {\n return `/api/partner/apps/${encodeURIComponent(product)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n product: string,\n init: { method: 'GET' | 'PUT'; path: string; body?: unknown },\n): Promise<AppsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? \"Only a Source admin manages the Source's apps. Ask one to run it.\"\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${product}\" was not found.`,\n code === 'PRODUCT_NOT_ENTITLED'\n ? `\"ekanos catalog\" lists what this Source has; Ekanos enables a product for a Source.`\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 && code === 'NO_PROVIDER_APP') {\n throw invalidStateError(\n message ?? `\"${product}\" has no app for the Source to set up.`,\n `Each team connects ${product} with its own app, from the team's Integrations page. \"ekanos catalog ${product}\" shows its setup steps.`,\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos apps --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the apps routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}/api/partner/apps answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the apps contract.',\n );\n }\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
@@ -0,0 +1,39 @@
1
+ import type { ResultRow } from './ui.js';
2
+ /** The Ekanos docs MCP server, as Claude Code knows it. */
3
+ export declare const DOCS_MCP_SERVER_NAME = "ekanos-sdk-docs";
4
+ export declare const DOCS_MCP_SERVER_URL = "https://sdk.ekanos.dev/mcp";
5
+ export type ClaudeDocsOutcome =
6
+ /** Added to Claude Code's user config: every project sees it. */
7
+ 'added'
8
+ /** Claude Code already has it. */
9
+ | 'present'
10
+ /** The user said no, or passed --no-claude. */
11
+ | 'declined'
12
+ /** Could not ask (a script or JSON run) and --claude was not passed. */
13
+ | 'not-asked'
14
+ /** Claude Code is not installed. */
15
+ | 'no-claude'
16
+ /** `claude mcp add` failed. */
17
+ | 'failed';
18
+ export type RunClaude = (args: readonly string[]) => {
19
+ status: number | null;
20
+ missing: boolean;
21
+ };
22
+ export interface OfferDocsOptions {
23
+ /** `--claude` (true) adds without asking; `--no-claude` (false) skips. */
24
+ choice?: boolean;
25
+ /** Whether a person is there to answer. */
26
+ canPrompt: boolean;
27
+ confirm: (question: string) => Promise<boolean>;
28
+ env: Record<string, string | undefined>;
29
+ /** Injectable for tests. */
30
+ runClaude?: RunClaude;
31
+ }
32
+ /**
33
+ * Offer to add the Ekanos docs MCP server to Claude Code, for every project
34
+ * (user scope), so Claude can look Ekanos up instead of guessing. Never asks
35
+ * a script or a JSON run — there it only acts on an explicit `--claude`.
36
+ */
37
+ export declare function offerDocsToClaude(options: OfferDocsOptions): Promise<ClaudeDocsOutcome>;
38
+ /** The row a command's human summary shows for the outcome, when it says anything. */
39
+ export declare function claudeDocsRow(outcome: ClaudeDocsOutcome): ResultRow | null;
@@ -0,0 +1,70 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ /** The Ekanos docs MCP server, as Claude Code knows it. */
3
+ export const DOCS_MCP_SERVER_NAME = 'ekanos-sdk-docs';
4
+ export const DOCS_MCP_SERVER_URL = 'https://sdk.ekanos.dev/mcp';
5
+ /**
6
+ * Offer to add the Ekanos docs MCP server to Claude Code, for every project
7
+ * (user scope), so Claude can look Ekanos up instead of guessing. Never asks
8
+ * a script or a JSON run — there it only acts on an explicit `--claude`.
9
+ */
10
+ export async function offerDocsToClaude(options) {
11
+ var _a;
12
+ if (options.choice === false)
13
+ return 'declined';
14
+ // A script or JSON run touches Claude Code only when told to.
15
+ if (options.choice !== true && !options.canPrompt)
16
+ return 'not-asked';
17
+ const run = (_a = options.runClaude) !== null && _a !== void 0 ? _a : defaultRunClaude(options.env);
18
+ const existing = run(['mcp', 'get', DOCS_MCP_SERVER_NAME]);
19
+ if (existing.missing)
20
+ return 'no-claude';
21
+ if (existing.status === 0)
22
+ return 'present';
23
+ if (options.choice !== true) {
24
+ const yes = await options.confirm('Add the Ekanos docs to Claude Code, for every project? [y/N]');
25
+ if (!yes)
26
+ return 'declined';
27
+ }
28
+ const added = run([
29
+ 'mcp',
30
+ 'add',
31
+ '--transport',
32
+ 'http',
33
+ '--scope',
34
+ 'user',
35
+ DOCS_MCP_SERVER_NAME,
36
+ DOCS_MCP_SERVER_URL,
37
+ ]);
38
+ return added.status === 0 ? 'added' : 'failed';
39
+ }
40
+ /** The row a command's human summary shows for the outcome, when it says anything. */
41
+ export function claudeDocsRow(outcome) {
42
+ switch (outcome) {
43
+ case 'added':
44
+ return ['Claude Code', 'Ekanos docs added for every project'];
45
+ case 'present':
46
+ return ['Claude Code', 'Ekanos docs already added'];
47
+ case 'not-asked':
48
+ return ['Claude Code', 'Pass --claude to add the Ekanos docs'];
49
+ case 'failed':
50
+ return [
51
+ 'Claude Code',
52
+ `Could not add the docs — run: claude mcp add --transport http --scope user ${DOCS_MCP_SERVER_NAME} ${DOCS_MCP_SERVER_URL}`,
53
+ ];
54
+ default:
55
+ return null;
56
+ }
57
+ }
58
+ function defaultRunClaude(env) {
59
+ return (args) => {
60
+ var _a;
61
+ const result = spawnSync('claude', args, {
62
+ env: Object.assign(Object.assign({}, process.env), env),
63
+ stdio: 'ignore',
64
+ timeout: 30000,
65
+ });
66
+ const missing = ((_a = result.error) === null || _a === void 0 ? void 0 : _a.code) === 'ENOENT';
67
+ return { status: result.status, missing };
68
+ };
69
+ }
70
+ //# sourceMappingURL=claude-docs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-docs.js","sourceRoot":"","sources":["../src/claude-docs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI/C,2DAA2D;AAC3D,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AACtD,MAAM,CAAC,MAAM,mBAAmB,GAAG,4BAA4B,CAAC;AAgChE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAyB;;IAEzB,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IAChD,8DAA8D;IAC9D,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS;QAAE,OAAO,WAAW,CAAC;IAEtE,MAAM,GAAG,SAAG,OAAO,CAAC,SAAS,mCAAI,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAC3D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,WAAW,CAAC;IACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE5C,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,CAC/B,8DAA8D,CAC/D,CAAC;QACF,IAAI,CAAC,GAAG;YAAE,OAAO,UAAU,CAAC;IAC9B,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC;QAChB,KAAK;QACL,KAAK;QACL,aAAa;QACb,MAAM;QACN,SAAS;QACT,MAAM;QACN,oBAAoB;QACpB,mBAAmB;KACpB,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;AACjD,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,CAAC,aAAa,EAAE,qCAAqC,CAAC,CAAC;QAChE,KAAK,SAAS;YACZ,OAAO,CAAC,aAAa,EAAE,2BAA2B,CAAC,CAAC;QACtD,KAAK,WAAW;YACd,OAAO,CAAC,aAAa,EAAE,sCAAsC,CAAC,CAAC;QACjE,KAAK,QAAQ;YACX,OAAO;gBACL,aAAa;gBACb,8EAA8E,oBAAoB,IAAI,mBAAmB,EAAE;aAC5H,CAAC;QACJ;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAuC;IAC/D,OAAO,CAAC,IAAI,EAAE,EAAE;;QACd,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE;YACvC,GAAG,kCAAO,OAAO,CAAC,GAAG,GAAK,GAAG,CAAE;YAC/B,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,KAAM;SAChB,CAAC,CAAC;QACH,MAAM,OAAO,GACX,CAAA,MAAC,MAAM,CAAC,KAA2C,0CAAE,IAAI,MAAK,QAAQ,CAAC;QACzE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;IAC5C,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { spawnSync } from 'node:child_process';\n\nimport type { ResultRow } from './ui';\n\n/** The Ekanos docs MCP server, as Claude Code knows it. */\nexport const DOCS_MCP_SERVER_NAME = 'ekanos-sdk-docs';\nexport const DOCS_MCP_SERVER_URL = 'https://sdk.ekanos.dev/mcp';\n\nexport type ClaudeDocsOutcome =\n /** Added to Claude Code's user config: every project sees it. */\n | 'added'\n /** Claude Code already has it. */\n | 'present'\n /** The user said no, or passed --no-claude. */\n | 'declined'\n /** Could not ask (a script or JSON run) and --claude was not passed. */\n | 'not-asked'\n /** Claude Code is not installed. */\n | 'no-claude'\n /** `claude mcp add` failed. */\n | 'failed';\n\nexport type RunClaude = (args: readonly string[]) => {\n status: number | null;\n missing: boolean;\n};\n\nexport interface OfferDocsOptions {\n /** `--claude` (true) adds without asking; `--no-claude` (false) skips. */\n choice?: boolean;\n /** Whether a person is there to answer. */\n canPrompt: boolean;\n confirm: (question: string) => Promise<boolean>;\n env: Record<string, string | undefined>;\n /** Injectable for tests. */\n runClaude?: RunClaude;\n}\n\n/**\n * Offer to add the Ekanos docs MCP server to Claude Code, for every project\n * (user scope), so Claude can look Ekanos up instead of guessing. Never asks\n * a script or a JSON run — there it only acts on an explicit `--claude`.\n */\nexport async function offerDocsToClaude(\n options: OfferDocsOptions,\n): Promise<ClaudeDocsOutcome> {\n if (options.choice === false) return 'declined';\n // A script or JSON run touches Claude Code only when told to.\n if (options.choice !== true && !options.canPrompt) return 'not-asked';\n\n const run = options.runClaude ?? defaultRunClaude(options.env);\n const existing = run(['mcp', 'get', DOCS_MCP_SERVER_NAME]);\n if (existing.missing) return 'no-claude';\n if (existing.status === 0) return 'present';\n\n if (options.choice !== true) {\n const yes = await options.confirm(\n 'Add the Ekanos docs to Claude Code, for every project? [y/N]',\n );\n if (!yes) return 'declined';\n }\n\n const added = run([\n 'mcp',\n 'add',\n '--transport',\n 'http',\n '--scope',\n 'user',\n DOCS_MCP_SERVER_NAME,\n DOCS_MCP_SERVER_URL,\n ]);\n return added.status === 0 ? 'added' : 'failed';\n}\n\n/** The row a command's human summary shows for the outcome, when it says anything. */\nexport function claudeDocsRow(outcome: ClaudeDocsOutcome): ResultRow | null {\n switch (outcome) {\n case 'added':\n return ['Claude Code', 'Ekanos docs added for every project'];\n case 'present':\n return ['Claude Code', 'Ekanos docs already added'];\n case 'not-asked':\n return ['Claude Code', 'Pass --claude to add the Ekanos docs'];\n case 'failed':\n return [\n 'Claude Code',\n `Could not add the docs — run: claude mcp add --transport http --scope user ${DOCS_MCP_SERVER_NAME} ${DOCS_MCP_SERVER_URL}`,\n ];\n default:\n return null;\n }\n}\n\nfunction defaultRunClaude(env: Record<string, string | undefined>): RunClaude {\n return (args) => {\n const result = spawnSync('claude', args, {\n env: { ...process.env, ...env },\n stdio: 'ignore',\n timeout: 30_000,\n });\n const missing =\n (result.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT';\n return { status: result.status, missing };\n };\n}\n"]}
@@ -0,0 +1,34 @@
1
+ import type { CliContext } from '../context.js';
2
+ import type { ExitCode } from '../exit-codes.js';
3
+ import type { Ui } from '../ui.js';
4
+ /** The skills a folder that only manages agents needs — not the integration-authoring ones. */
5
+ export declare const AGENT_SKILLS: readonly ['ekanos-platform', 'build-agent'];
6
+ export interface AgentsInitArgs {
7
+ dir: string;
8
+ force: boolean;
9
+ /** `--claude` / `--no-claude`: add the docs to Claude Code without asking, or don't. */
10
+ claude?: boolean;
11
+ canPrompt: boolean;
12
+ confirm: (question: string) => Promise<boolean>;
13
+ env: Record<string, string | undefined>;
14
+ }
15
+ /**
16
+ * `ekanos agents init` — make a folder ready for Claude to design agents: the
17
+ * `ekanos-platform` and `build-agent` skills, the docs MCP server in
18
+ * `.mcp.json`, and a short CLAUDE.md. Unlike `ekanos init` it scaffolds no
19
+ * integration: an agent is data in Ekanos, not code in this folder. Local,
20
+ * no session needed, and idempotent: existing files are kept (merged, for
21
+ * `.mcp.json`) unless `--force`. Where Claude Code is installed it also offers
22
+ * to add the docs server for every project, not just this folder.
23
+ */
24
+ export declare function runAgentsInit(ctx: CliContext, args: AgentsInitArgs): Promise<ExitCode>;
25
+ type DocsServerOutcome = 'created' | 'merged' | 'present' | 'unreadable';
26
+ /** Create `.mcp.json` with the docs server, or add it to one that lacks it. */
27
+ export declare function ensureDocsServer(targetDir: string): {
28
+ outcome: DocsServerOutcome;
29
+ };
30
+ /** Whether this folder already has the docs server — `agents`/`catalog` suggest `agents init` when not. */
31
+ export declare function hasDocsServer(targetDir: string): boolean;
32
+ /** Suggest `ekanos agents init` (skills + the docs MCP server) where this folder lacks it. */
33
+ export declare function withDocsHint(ui: Ui, dir: string, summary: string): string;
34
+ export {};
@@ -0,0 +1,119 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { claudeDocsRow, DOCS_MCP_SERVER_NAME, DOCS_MCP_SERVER_URL, offerDocsToClaude, } from '../claude-docs.js';
4
+ import { readFileNoFollow, writeFileNoFollow } from '../safe-fs.js';
5
+ import { templatesDir } from '../templates.js';
6
+ import { copyBundledSkillTrees } from './init.js';
7
+ /** The skills a folder that only manages agents needs — not the integration-authoring ones. */
8
+ export const AGENT_SKILLS = ['ekanos-platform', 'build-agent'];
9
+ const DOCS_MCP_SERVER = { type: 'http', url: DOCS_MCP_SERVER_URL };
10
+ /**
11
+ * `ekanos agents init` — make a folder ready for Claude to design agents: the
12
+ * `ekanos-platform` and `build-agent` skills, the docs MCP server in
13
+ * `.mcp.json`, and a short CLAUDE.md. Unlike `ekanos init` it scaffolds no
14
+ * integration: an agent is data in Ekanos, not code in this folder. Local,
15
+ * no session needed, and idempotent: existing files are kept (merged, for
16
+ * `.mcp.json`) unless `--force`. Where Claude Code is installed it also offers
17
+ * to add the docs server for every project, not just this folder.
18
+ */
19
+ export async function runAgentsInit(ctx, args) {
20
+ const targetDir = path.resolve(args.dir);
21
+ fs.mkdirSync(targetDir, { recursive: true });
22
+ const skills = copyBundledSkillTrees(targetDir, args.force, undefined, 'throw', AGENT_SKILLS);
23
+ const created = [...skills.created];
24
+ const skipped = [...skills.skipped];
25
+ const mcp = ensureDocsServer(targetDir);
26
+ if (mcp.outcome === 'created' || mcp.outcome === 'merged') {
27
+ created.push('.mcp.json');
28
+ }
29
+ else if (mcp.outcome === 'unreadable') {
30
+ skipped.push({
31
+ file: '.mcp.json',
32
+ reason: 'it is not valid JSON',
33
+ hint: `Add "${DOCS_MCP_SERVER_NAME}": ${JSON.stringify(DOCS_MCP_SERVER)} under "mcpServers" yourself.`,
34
+ });
35
+ }
36
+ const claudeFile = path.join(targetDir, 'CLAUDE.md');
37
+ if (args.force || readFileNoFollow(targetDir, claudeFile) === null) {
38
+ writeFileNoFollow(targetDir, claudeFile, fs.readFileSync(path.join(templatesDir(), 'agents-claude.md.tmpl'), 'utf8'));
39
+ created.push('CLAUDE.md');
40
+ }
41
+ else {
42
+ skipped.push({
43
+ file: 'CLAUDE.md',
44
+ reason: 'it already exists',
45
+ hint: 'Point it at the build-agent and ekanos-platform skills, or re-run with --force to replace it.',
46
+ });
47
+ }
48
+ const claudeDocs = await offerDocsToClaude({
49
+ choice: args.claude,
50
+ canPrompt: args.canPrompt,
51
+ confirm: args.confirm,
52
+ env: args.env,
53
+ });
54
+ const claudeRow = claudeDocsRow(claudeDocs);
55
+ const rows = [
56
+ ['Created', created.length > 0 ? created : ['nothing new']],
57
+ ...(skipped.length > 0
58
+ ? [
59
+ ['Kept', skipped.map((entry) => `${entry.file} (${entry.reason})`)],
60
+ ]
61
+ : []),
62
+ ...(claudeRow ? [claudeRow] : []),
63
+ ];
64
+ return ctx.succeed({ dir: targetDir, created, skipped, mcpServer: mcp.outcome, claudeDocs }, [
65
+ ctx.ui.result({ title: 'Ready to build agents', rows }),
66
+ ctx.ui.hint('Run "claude" here and describe the agent you want — e.g. "a support agent that triages new tickets".'),
67
+ ].join('\n'));
68
+ }
69
+ /** Create `.mcp.json` with the docs server, or add it to one that lacks it. */
70
+ export function ensureDocsServer(targetDir) {
71
+ var _a;
72
+ const file = path.join(targetDir, '.mcp.json');
73
+ const existing = readFileNoFollow(targetDir, file);
74
+ if (existing === null) {
75
+ writeJson(targetDir, file, {
76
+ mcpServers: { [DOCS_MCP_SERVER_NAME]: DOCS_MCP_SERVER },
77
+ });
78
+ return { outcome: 'created' };
79
+ }
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(existing);
83
+ }
84
+ catch (_b) {
85
+ return { outcome: 'unreadable' };
86
+ }
87
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
88
+ return { outcome: 'unreadable' };
89
+ }
90
+ const config = parsed;
91
+ if ((_a = config.mcpServers) === null || _a === void 0 ? void 0 : _a[DOCS_MCP_SERVER_NAME])
92
+ return { outcome: 'present' };
93
+ writeJson(targetDir, file, Object.assign(Object.assign({}, config), { mcpServers: Object.assign(Object.assign({}, config.mcpServers), { [DOCS_MCP_SERVER_NAME]: DOCS_MCP_SERVER }) }));
94
+ return { outcome: 'merged' };
95
+ }
96
+ /** Whether this folder already has the docs server — `agents`/`catalog` suggest `agents init` when not. */
97
+ export function hasDocsServer(targetDir) {
98
+ var _a;
99
+ try {
100
+ const parsed = JSON.parse(fs.readFileSync(path.join(targetDir, '.mcp.json'), 'utf8'));
101
+ return Boolean((_a = parsed.mcpServers) === null || _a === void 0 ? void 0 : _a[DOCS_MCP_SERVER_NAME]);
102
+ }
103
+ catch (_b) {
104
+ return false;
105
+ }
106
+ }
107
+ function writeJson(root, file, value) {
108
+ writeFileNoFollow(root, file, `${JSON.stringify(value, null, 2)}\n`);
109
+ }
110
+ /** Suggest `ekanos agents init` (skills + the docs MCP server) where this folder lacks it. */
111
+ export function withDocsHint(ui, dir, summary) {
112
+ if (hasDocsServer(dir))
113
+ return summary;
114
+ return [
115
+ summary,
116
+ ui.hint('Designing agents with Claude? "ekanos agents init" adds the build-agent skill and the Ekanos docs MCP server to this folder.'),
117
+ ].join('\n');
118
+ }
119
+ //# sourceMappingURL=agents-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents-init.js","sourceRoot":"","sources":["../../src/commands/agents-init.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport {\n type ClaudeDocsOutcome,\n claudeDocsRow,\n DOCS_MCP_SERVER_NAME,\n DOCS_MCP_SERVER_URL,\n offerDocsToClaude,\n} from '../claude-docs';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { readFileNoFollow, writeFileNoFollow } from '../safe-fs';\nimport { templatesDir } from '../templates';\nimport type { ResultRow, Ui } from '../ui';\nimport { copyBundledSkillTrees, type SkippedFile } from './init';\n\n/** The skills a folder that only manages agents needs — not the integration-authoring ones. */\nexport const AGENT_SKILLS = ['ekanos-platform', 'build-agent'] as const;\n\nconst DOCS_MCP_SERVER = { type: 'http', url: DOCS_MCP_SERVER_URL } as const;\n\nexport interface AgentsInitArgs {\n dir: string;\n force: boolean;\n /** `--claude` / `--no-claude`: add the docs to Claude Code without asking, or don't. */\n claude?: boolean;\n canPrompt: boolean;\n confirm: (question: string) => Promise<boolean>;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `ekanos agents init` — make a folder ready for Claude to design agents: the\n * `ekanos-platform` and `build-agent` skills, the docs MCP server in\n * `.mcp.json`, and a short CLAUDE.md. Unlike `ekanos init` it scaffolds no\n * integration: an agent is data in Ekanos, not code in this folder. Local,\n * no session needed, and idempotent: existing files are kept (merged, for\n * `.mcp.json`) unless `--force`. Where Claude Code is installed it also offers\n * to add the docs server for every project, not just this folder.\n */\nexport async function runAgentsInit(\n ctx: CliContext,\n args: AgentsInitArgs,\n): Promise<ExitCode> {\n const targetDir = path.resolve(args.dir);\n fs.mkdirSync(targetDir, { recursive: true });\n\n const skills = copyBundledSkillTrees(\n targetDir,\n args.force,\n undefined,\n 'throw',\n AGENT_SKILLS,\n );\n const created = [...skills.created];\n const skipped: SkippedFile[] = [...skills.skipped];\n\n const mcp = ensureDocsServer(targetDir);\n if (mcp.outcome === 'created' || mcp.outcome === 'merged') {\n created.push('.mcp.json');\n } else if (mcp.outcome === 'unreadable') {\n skipped.push({\n file: '.mcp.json',\n reason: 'it is not valid JSON',\n hint: `Add \"${DOCS_MCP_SERVER_NAME}\": ${JSON.stringify(DOCS_MCP_SERVER)} under \"mcpServers\" yourself.`,\n });\n }\n\n const claudeFile = path.join(targetDir, 'CLAUDE.md');\n if (args.force || readFileNoFollow(targetDir, claudeFile) === null) {\n writeFileNoFollow(\n targetDir,\n claudeFile,\n fs.readFileSync(\n path.join(templatesDir(), 'agents-claude.md.tmpl'),\n 'utf8',\n ),\n );\n created.push('CLAUDE.md');\n } else {\n skipped.push({\n file: 'CLAUDE.md',\n reason: 'it already exists',\n hint: 'Point it at the build-agent and ekanos-platform skills, or re-run with --force to replace it.',\n });\n }\n\n const claudeDocs: ClaudeDocsOutcome = await offerDocsToClaude({\n choice: args.claude,\n canPrompt: args.canPrompt,\n confirm: args.confirm,\n env: args.env,\n });\n const claudeRow = claudeDocsRow(claudeDocs);\n\n const rows: ResultRow[] = [\n ['Created', created.length > 0 ? created : ['nothing new']],\n ...(skipped.length > 0\n ? ([\n ['Kept', skipped.map((entry) => `${entry.file} (${entry.reason})`)],\n ] as const)\n : []),\n ...(claudeRow ? [claudeRow] : []),\n ];\n return ctx.succeed(\n { dir: targetDir, created, skipped, mcpServer: mcp.outcome, claudeDocs },\n [\n ctx.ui.result({ title: 'Ready to build agents', rows }),\n ctx.ui.hint(\n 'Run \"claude\" here and describe the agent you want — e.g. \"a support agent that triages new tickets\".',\n ),\n ].join('\\n'),\n );\n}\n\ntype DocsServerOutcome = 'created' | 'merged' | 'present' | 'unreadable';\n\n/** Create `.mcp.json` with the docs server, or add it to one that lacks it. */\nexport function ensureDocsServer(targetDir: string): {\n outcome: DocsServerOutcome;\n} {\n const file = path.join(targetDir, '.mcp.json');\n const existing = readFileNoFollow(targetDir, file);\n if (existing === null) {\n writeJson(targetDir, file, {\n mcpServers: { [DOCS_MCP_SERVER_NAME]: DOCS_MCP_SERVER },\n });\n return { outcome: 'created' };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(existing);\n } catch {\n return { outcome: 'unreadable' };\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { outcome: 'unreadable' };\n }\n\n const config = parsed as { mcpServers?: Record<string, unknown> };\n if (config.mcpServers?.[DOCS_MCP_SERVER_NAME]) return { outcome: 'present' };\n writeJson(targetDir, file, {\n ...config,\n mcpServers: {\n ...config.mcpServers,\n [DOCS_MCP_SERVER_NAME]: DOCS_MCP_SERVER,\n },\n });\n return { outcome: 'merged' };\n}\n\n/** Whether this folder already has the docs server — `agents`/`catalog` suggest `agents init` when not. */\nexport function hasDocsServer(targetDir: string): boolean {\n try {\n const parsed = JSON.parse(\n fs.readFileSync(path.join(targetDir, '.mcp.json'), 'utf8'),\n ) as { mcpServers?: Record<string, unknown> };\n return Boolean(parsed.mcpServers?.[DOCS_MCP_SERVER_NAME]);\n } catch {\n return false;\n }\n}\n\nfunction writeJson(root: string, file: string, value: unknown): void {\n writeFileNoFollow(root, file, `${JSON.stringify(value, null, 2)}\\n`);\n}\n\n/** Suggest `ekanos agents init` (skills + the docs MCP server) where this folder lacks it. */\nexport function withDocsHint(ui: Ui, dir: string, summary: string): string {\n if (hasDocsServer(dir)) return summary;\n return [\n summary,\n ui.hint(\n 'Designing agents with Claude? \"ekanos agents init\" adds the build-agent skill and the Ekanos docs MCP server to this folder.',\n ),\n ].join('\\n');\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { type AgentTemplateBody } from '@ekanos/integration-schema';
2
2
  import type { CliContext } from '../context.js';
3
3
  import type { ExitCode } from '../exit-codes.js';
4
- export declare const AGENTS_SUBCOMMANDS: readonly ['list', 'show', 'pull', 'apply', 'publish', 'rollback'];
4
+ export declare const AGENTS_SUBCOMMANDS: readonly ['init', 'list', 'show', 'pull', 'apply', 'publish', 'rollback'];
5
5
  /** The file `pull` writes and `apply` reads, inside an agent's directory. */
6
6
  export declare const AGENT_FILE = "agent.json";
7
7
  /** Instructions as Markdown, beside it. Used when `agent.json` has none. */
@@ -18,6 +18,10 @@ export interface AgentsArgs {
18
18
  overwrite: boolean;
19
19
  out?: string;
20
20
  yes: boolean;
21
+ /** `init`: replace the skills and CLAUDE.md even when they exist. */
22
+ force?: boolean;
23
+ /** `init`: `--claude` adds the docs to Claude Code without asking; `--no-claude` skips it. */
24
+ claude?: boolean;
21
25
  dir: string;
22
26
  env: Record<string, string | undefined>;
23
27
  stdinIsTTY?: boolean;
@@ -18,8 +18,10 @@ import { authRequiredError, notFoundError, usageError, validationError, } from '
18
18
  import { classifyPath } from '../safe-fs.js';
19
19
  import { resolveSourceTarget } from '../source-target.js';
20
20
  import { pluralize } from '../ui.js';
21
+ import { runAgentsInit, withDocsHint } from './agents-init.js';
21
22
  import { promptYesNo } from './publish.js';
22
23
  export const AGENTS_SUBCOMMANDS = [
24
+ 'init',
23
25
  'list',
24
26
  'show',
25
27
  'pull',
@@ -40,8 +42,19 @@ export const AGENT_INSTRUCTIONS_FILE = 'AGENT.md';
40
42
  * layout back.
41
43
  */
42
44
  export async function runAgents(ctx, args) {
43
- var _a, _b, _c;
45
+ var _a, _b, _c, _d, _e;
44
46
  const subcommand = parseSubcommand(args.subcommand);
47
+ // Local only: no host, no session.
48
+ if (subcommand === 'init') {
49
+ return runAgentsInit(ctx, {
50
+ dir: path.resolve(args.dir, (_a = args.target) !== null && _a !== void 0 ? _a : '.'),
51
+ force: args.force === true,
52
+ claude: args.claude,
53
+ canPrompt: canPrompt(ctx, args),
54
+ confirm: (_b = args.confirm) !== null && _b !== void 0 ? _b : promptYesNo,
55
+ env: args.env,
56
+ });
57
+ }
45
58
  // Read the local input before any network call, so a bad file fails fast.
46
59
  const local = subcommand === 'apply' ? readAgentInput(args.target, args.dir) : null;
47
60
  const env = resolveAuthEnvironment(ctx, args.host, args.env, {
@@ -53,7 +66,7 @@ export async function runAgents(ctx, args) {
53
66
  switch (subcommand) {
54
67
  case 'list': {
55
68
  const list = await client.call((token) => listAgentTemplates(env.host, token, source));
56
- return ctx.succeed(Object.assign({ host: env.host }, list), listSummary(ctx.ui, list));
69
+ return ctx.succeed(Object.assign({ host: env.host }, list), withDocsHint(ctx.ui, args.dir, listSummary(ctx.ui, list)));
57
70
  }
58
71
  case 'show': {
59
72
  const key = requireKey(args.target, 'show');
@@ -63,7 +76,7 @@ export async function runAgents(ctx, args) {
63
76
  case 'pull': {
64
77
  const key = requireKey(args.target, 'pull');
65
78
  const shown = await client.call((token) => showAgentTemplate(env.host, token, source, key));
66
- const outDir = path.resolve(args.dir, (_a = args.out) !== null && _a !== void 0 ? _a : path.join('agents', key));
79
+ const outDir = path.resolve(args.dir, (_c = args.out) !== null && _c !== void 0 ? _c : path.join('agents', key));
67
80
  const written = writeAgentFiles(outDir, shown.template);
68
81
  return ctx.succeed({
69
82
  host: env.host,
@@ -80,7 +93,7 @@ export async function runAgents(ctx, args) {
80
93
  }
81
94
  case 'apply': {
82
95
  const input = local;
83
- const baseVersion = (_b = parseVersionFlag(args.baseVersion, '--base-version')) !== null && _b !== void 0 ? _b : input.baseVersion;
96
+ const baseVersion = (_d = parseVersionFlag(args.baseVersion, '--base-version')) !== null && _d !== void 0 ? _d : input.baseVersion;
84
97
  const result = await client.call((token) => applyAgentTemplate(env.host, token, {
85
98
  source,
86
99
  key: input.key,
@@ -96,7 +109,7 @@ export async function runAgents(ctx, args) {
96
109
  case 'publish':
97
110
  case 'rollback': {
98
111
  const key = requireKey(args.target, subcommand);
99
- let version = (_c = parseVersionFlag(args.to, '--to')) !== null && _c !== void 0 ? _c : undefined;
112
+ let version = (_e = parseVersionFlag(args.to, '--to')) !== null && _e !== void 0 ? _e : undefined;
100
113
  if (subcommand === 'rollback' && version === undefined) {
101
114
  throw usageError('rollback needs the version to go back to.', `Pass "--to <n>". "ekanos agents show ${key}" lists them.`);
102
115
  }