@ekanos/cli 0.6.0 → 0.8.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.
@@ -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"]}
@@ -3,14 +3,14 @@ import type { ExitCode } from '../exit-codes.js';
3
3
  import type { Ui } from '../ui.js';
4
4
  /** The skills a folder that only manages agents needs — not the integration-authoring ones. */
5
5
  export declare const AGENT_SKILLS: readonly ['ekanos-platform', 'build-agent'];
6
- export declare const DOCS_MCP_SERVER_NAME = "ekanos-sdk-docs";
7
- export declare const DOCS_MCP_SERVER: {
8
- readonly type: 'http';
9
- readonly url: 'https://sdk.ekanos.dev/mcp';
10
- };
11
6
  export interface AgentsInitArgs {
12
7
  dir: string;
13
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
14
  }
15
15
  /**
16
16
  * `ekanos agents init` — make a folder ready for Claude to design agents: the
@@ -18,9 +18,10 @@ export interface AgentsInitArgs {
18
18
  * `.mcp.json`, and a short CLAUDE.md. Unlike `ekanos init` it scaffolds no
19
19
  * integration: an agent is data in Ekanos, not code in this folder. Local,
20
20
  * no session needed, and idempotent: existing files are kept (merged, for
21
- * `.mcp.json`) unless `--force`.
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.
22
23
  */
23
- export declare function runAgentsInit(ctx: CliContext, args: AgentsInitArgs): ExitCode;
24
+ export declare function runAgentsInit(ctx: CliContext, args: AgentsInitArgs): Promise<ExitCode>;
24
25
  type DocsServerOutcome = 'created' | 'merged' | 'present' | 'unreadable';
25
26
  /** Create `.mcp.json` with the docs server, or add it to one that lacks it. */
26
27
  export declare function ensureDocsServer(targetDir: string): {
@@ -1,24 +1,22 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { claudeDocsRow, DOCS_MCP_SERVER_NAME, DOCS_MCP_SERVER_URL, offerDocsToClaude, } from '../claude-docs.js';
3
4
  import { readFileNoFollow, writeFileNoFollow } from '../safe-fs.js';
4
5
  import { templatesDir } from '../templates.js';
5
6
  import { copyBundledSkillTrees } from './init.js';
6
7
  /** The skills a folder that only manages agents needs — not the integration-authoring ones. */
7
8
  export const AGENT_SKILLS = ['ekanos-platform', 'build-agent'];
8
- export const DOCS_MCP_SERVER_NAME = 'ekanos-sdk-docs';
9
- export const DOCS_MCP_SERVER = {
10
- type: 'http',
11
- url: 'https://sdk.ekanos.dev/mcp',
12
- };
9
+ const DOCS_MCP_SERVER = { type: 'http', url: DOCS_MCP_SERVER_URL };
13
10
  /**
14
11
  * `ekanos agents init` — make a folder ready for Claude to design agents: the
15
12
  * `ekanos-platform` and `build-agent` skills, the docs MCP server in
16
13
  * `.mcp.json`, and a short CLAUDE.md. Unlike `ekanos init` it scaffolds no
17
14
  * integration: an agent is data in Ekanos, not code in this folder. Local,
18
15
  * no session needed, and idempotent: existing files are kept (merged, for
19
- * `.mcp.json`) unless `--force`.
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.
20
18
  */
21
- export function runAgentsInit(ctx, args) {
19
+ export async function runAgentsInit(ctx, args) {
22
20
  const targetDir = path.resolve(args.dir);
23
21
  fs.mkdirSync(targetDir, { recursive: true });
24
22
  const skills = copyBundledSkillTrees(targetDir, args.force, undefined, 'throw', AGENT_SKILLS);
@@ -47,6 +45,13 @@ export function runAgentsInit(ctx, args) {
47
45
  hint: 'Point it at the build-agent and ekanos-platform skills, or re-run with --force to replace it.',
48
46
  });
49
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);
50
55
  const rows = [
51
56
  ['Created', created.length > 0 ? created : ['nothing new']],
52
57
  ...(skipped.length > 0
@@ -54,8 +59,9 @@ export function runAgentsInit(ctx, args) {
54
59
  ['Kept', skipped.map((entry) => `${entry.file} (${entry.reason})`)],
55
60
  ]
56
61
  : []),
62
+ ...(claudeRow ? [claudeRow] : []),
57
63
  ];
58
- return ctx.succeed({ dir: targetDir, created, skipped, mcpServer: mcp.outcome }, [
64
+ return ctx.succeed({ dir: targetDir, created, skipped, mcpServer: mcp.outcome, claudeDocs }, [
59
65
  ctx.ui.result({ title: 'Ready to build agents', rows }),
60
66
  ctx.ui.hint('Run "claude" here and describe the agent you want — e.g. "a support agent that triages new tickets".'),
61
67
  ].join('\n'));
@@ -1 +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 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\nexport const DOCS_MCP_SERVER_NAME = 'ekanos-sdk-docs';\nexport const DOCS_MCP_SERVER = {\n type: 'http',\n url: 'https://sdk.ekanos.dev/mcp',\n} as const;\n\nexport interface AgentsInitArgs {\n dir: string;\n force: boolean;\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`.\n */\nexport function runAgentsInit(ctx: CliContext, args: AgentsInitArgs): 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 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 ];\n return ctx.succeed(\n { dir: targetDir, created, skipped, mcpServer: mcp.outcome },\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
+ {"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"]}
@@ -20,6 +20,8 @@ export interface AgentsArgs {
20
20
  yes: boolean;
21
21
  /** `init`: replace the skills and CLAUDE.md even when they exist. */
22
22
  force?: boolean;
23
+ /** `init`: `--claude` adds the docs to Claude Code without asking; `--no-claude` skips it. */
24
+ claude?: boolean;
23
25
  dir: string;
24
26
  env: Record<string, string | undefined>;
25
27
  stdinIsTTY?: boolean;
@@ -42,13 +42,17 @@ export const AGENT_INSTRUCTIONS_FILE = 'AGENT.md';
42
42
  * layout back.
43
43
  */
44
44
  export async function runAgents(ctx, args) {
45
- var _a, _b, _c, _d;
45
+ var _a, _b, _c, _d, _e;
46
46
  const subcommand = parseSubcommand(args.subcommand);
47
47
  // Local only: no host, no session.
48
48
  if (subcommand === 'init') {
49
49
  return runAgentsInit(ctx, {
50
50
  dir: path.resolve(args.dir, (_a = args.target) !== null && _a !== void 0 ? _a : '.'),
51
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,
52
56
  });
53
57
  }
54
58
  // Read the local input before any network call, so a bad file fails fast.
@@ -72,7 +76,7 @@ export async function runAgents(ctx, args) {
72
76
  case 'pull': {
73
77
  const key = requireKey(args.target, 'pull');
74
78
  const shown = await client.call((token) => showAgentTemplate(env.host, token, source, key));
75
- const outDir = path.resolve(args.dir, (_b = args.out) !== null && _b !== void 0 ? _b : path.join('agents', key));
79
+ const outDir = path.resolve(args.dir, (_c = args.out) !== null && _c !== void 0 ? _c : path.join('agents', key));
76
80
  const written = writeAgentFiles(outDir, shown.template);
77
81
  return ctx.succeed({
78
82
  host: env.host,
@@ -89,7 +93,7 @@ export async function runAgents(ctx, args) {
89
93
  }
90
94
  case 'apply': {
91
95
  const input = local;
92
- const baseVersion = (_c = parseVersionFlag(args.baseVersion, '--base-version')) !== null && _c !== void 0 ? _c : input.baseVersion;
96
+ const baseVersion = (_d = parseVersionFlag(args.baseVersion, '--base-version')) !== null && _d !== void 0 ? _d : input.baseVersion;
93
97
  const result = await client.call((token) => applyAgentTemplate(env.host, token, {
94
98
  source,
95
99
  key: input.key,
@@ -105,7 +109,7 @@ export async function runAgents(ctx, args) {
105
109
  case 'publish':
106
110
  case 'rollback': {
107
111
  const key = requireKey(args.target, subcommand);
108
- let version = (_d = parseVersionFlag(args.to, '--to')) !== null && _d !== void 0 ? _d : undefined;
112
+ let version = (_e = parseVersionFlag(args.to, '--to')) !== null && _e !== void 0 ? _e : undefined;
109
113
  if (subcommand === 'rollback' && version === undefined) {
110
114
  throw usageError('rollback needs the version to go back to.', `Pass "--to <n>". "ekanos agents show ${key}" lists them.`);
111
115
  }