@ekanos/cli 0.13.0 → 0.14.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 (173) hide show
  1. package/README.md +25 -8
  2. package/dist/agents-api.d.ts +11 -2
  3. package/dist/agents-api.js +39 -3
  4. package/dist/agents-api.js.map +1 -1
  5. package/dist/apps-api.js +2 -2
  6. package/dist/apps-api.js.map +1 -1
  7. package/dist/auth/ekanos-api.d.ts +16 -2
  8. package/dist/auth/ekanos-api.js +101 -15
  9. package/dist/auth/ekanos-api.js.map +1 -1
  10. package/dist/auth/host-trust.d.ts +46 -0
  11. package/dist/auth/host-trust.js +79 -0
  12. package/dist/auth/host-trust.js.map +1 -0
  13. package/dist/auth/session.d.ts +10 -1
  14. package/dist/auth/session.js +28 -28
  15. package/dist/auth/session.js.map +1 -1
  16. package/dist/commands/agents-edit.d.ts +6 -2
  17. package/dist/commands/agents-edit.js +57 -26
  18. package/dist/commands/agents-edit.js.map +1 -1
  19. package/dist/commands/agents.d.ts +13 -2
  20. package/dist/commands/agents.js +148 -36
  21. package/dist/commands/agents.js.map +1 -1
  22. package/dist/commands/apps.d.ts +2 -0
  23. package/dist/commands/apps.js +16 -12
  24. package/dist/commands/apps.js.map +1 -1
  25. package/dist/commands/catalog.js +7 -6
  26. package/dist/commands/catalog.js.map +1 -1
  27. package/dist/commands/knowledge.d.ts +19 -0
  28. package/dist/commands/knowledge.js +119 -27
  29. package/dist/commands/knowledge.js.map +1 -1
  30. package/dist/commands/login.d.ts +4 -0
  31. package/dist/commands/login.js +30 -2
  32. package/dist/commands/login.js.map +1 -1
  33. package/dist/commands/logout.d.ts +9 -1
  34. package/dist/commands/logout.js +56 -7
  35. package/dist/commands/logout.js.map +1 -1
  36. package/dist/commands/publish.js +3 -3
  37. package/dist/commands/publish.js.map +1 -1
  38. package/dist/commands/reviewers.d.ts +2 -0
  39. package/dist/commands/reviewers.js +7 -3
  40. package/dist/commands/reviewers.js.map +1 -1
  41. package/dist/commands/routes.d.ts +2 -0
  42. package/dist/commands/routes.js +42 -9
  43. package/dist/commands/routes.js.map +1 -1
  44. package/dist/commands/runs.d.ts +13 -2
  45. package/dist/commands/runs.js +96 -15
  46. package/dist/commands/runs.js.map +1 -1
  47. package/dist/commands/scope-notice.d.ts +21 -0
  48. package/dist/commands/scope-notice.js +35 -0
  49. package/dist/commands/scope-notice.js.map +1 -0
  50. package/dist/commands/skills.d.ts +7 -1
  51. package/dist/commands/skills.js +80 -24
  52. package/dist/commands/skills.js.map +1 -1
  53. package/dist/commands/sources.d.ts +1 -1
  54. package/dist/commands/sources.js +126 -12
  55. package/dist/commands/sources.js.map +1 -1
  56. package/dist/commands/status.js +5 -4
  57. package/dist/commands/status.js.map +1 -1
  58. package/dist/commands/team-agent-readiness.d.ts +59 -0
  59. package/dist/commands/team-agent-readiness.js +341 -0
  60. package/dist/commands/team-agent-readiness.js.map +1 -0
  61. package/dist/commands/team-agents-agents.d.ts +24 -3
  62. package/dist/commands/team-agents-agents.js +336 -35
  63. package/dist/commands/team-agents-agents.js.map +1 -1
  64. package/dist/commands/team-agents-mcp.js +32 -3
  65. package/dist/commands/team-agents-mcp.js.map +1 -1
  66. package/dist/commands/team-agents-triggers.js +230 -55
  67. package/dist/commands/team-agents-triggers.js.map +1 -1
  68. package/dist/commands/team-agents.d.ts +3 -1
  69. package/dist/commands/team-agents.js +27 -7
  70. package/dist/commands/team-agents.js.map +1 -1
  71. package/dist/commands/teams-channel-install.d.ts +24 -0
  72. package/dist/commands/teams-channel-install.js +94 -0
  73. package/dist/commands/teams-channel-install.js.map +1 -0
  74. package/dist/commands/teams-connect.d.ts +41 -0
  75. package/dist/commands/teams-connect.js +212 -57
  76. package/dist/commands/teams-connect.js.map +1 -1
  77. package/dist/commands/teams-events.d.ts +12 -0
  78. package/dist/commands/teams-events.js +138 -0
  79. package/dist/commands/teams-events.js.map +1 -0
  80. package/dist/commands/teams-onboard.d.ts +11 -3
  81. package/dist/commands/teams-onboard.js +60 -7
  82. package/dist/commands/teams-onboard.js.map +1 -1
  83. package/dist/commands/teams-overview.d.ts +11 -0
  84. package/dist/commands/teams-overview.js +160 -0
  85. package/dist/commands/teams-overview.js.map +1 -0
  86. package/dist/commands/teams-scope.d.ts +10 -0
  87. package/dist/commands/teams-scope.js +109 -0
  88. package/dist/commands/teams-scope.js.map +1 -0
  89. package/dist/commands/teams.d.ts +4 -2
  90. package/dist/commands/teams.js +113 -14
  91. package/dist/commands/teams.js.map +1 -1
  92. package/dist/commands/upgrade.js +12 -2
  93. package/dist/commands/upgrade.js.map +1 -1
  94. package/dist/commands/use.js +2 -2
  95. package/dist/commands/use.js.map +1 -1
  96. package/dist/commands/whoami.js +1 -1
  97. package/dist/commands/whoami.js.map +1 -1
  98. package/dist/connections-api.js +1 -1
  99. package/dist/connections-api.js.map +1 -1
  100. package/dist/delegate.js +11 -4
  101. package/dist/delegate.js.map +1 -1
  102. package/dist/harness-scaffold.js +12 -0
  103. package/dist/harness-scaffold.js.map +1 -1
  104. package/dist/index.d.ts +6 -0
  105. package/dist/index.js +683 -167
  106. package/dist/index.js.map +1 -1
  107. package/dist/knowledge-api.d.ts +46 -0
  108. package/dist/knowledge-api.js +32 -1
  109. package/dist/knowledge-api.js.map +1 -1
  110. package/dist/open-url.d.ts +16 -0
  111. package/dist/open-url.js +73 -0
  112. package/dist/open-url.js.map +1 -0
  113. package/dist/parse-argv.d.ts +2 -0
  114. package/dist/parse-argv.js.map +1 -1
  115. package/dist/publish-api.d.ts +2 -1
  116. package/dist/publish-api.js +4 -3
  117. package/dist/publish-api.js.map +1 -1
  118. package/dist/pull-write.d.ts +27 -0
  119. package/dist/pull-write.js +60 -0
  120. package/dist/pull-write.js.map +1 -0
  121. package/dist/routes-api.d.ts +3 -0
  122. package/dist/routes-api.js +9 -4
  123. package/dist/routes-api.js.map +1 -1
  124. package/dist/runs-api.d.ts +7 -1
  125. package/dist/runs-api.js +21 -6
  126. package/dist/runs-api.js.map +1 -1
  127. package/dist/safe-fs.d.ts +2 -0
  128. package/dist/safe-fs.js +12 -6
  129. package/dist/safe-fs.js.map +1 -1
  130. package/dist/secret-file.d.ts +17 -0
  131. package/dist/secret-file.js +69 -0
  132. package/dist/secret-file.js.map +1 -0
  133. package/dist/skills-api.d.ts +10 -2
  134. package/dist/skills-api.js +37 -2
  135. package/dist/skills-api.js.map +1 -1
  136. package/dist/source-products-api.d.ts +4 -0
  137. package/dist/source-products-api.js +14 -4
  138. package/dist/source-products-api.js.map +1 -1
  139. package/dist/source-target.js +2 -1
  140. package/dist/source-target.js.map +1 -1
  141. package/dist/sources-api.d.ts +2 -1
  142. package/dist/sources-api.js +3 -2
  143. package/dist/sources-api.js.map +1 -1
  144. package/dist/team-connect-api.d.ts +44 -2
  145. package/dist/team-connect-api.js +19 -7
  146. package/dist/team-connect-api.js.map +1 -1
  147. package/dist/teams-api.d.ts +41 -1
  148. package/dist/teams-api.js +97 -19
  149. package/dist/teams-api.js.map +1 -1
  150. package/dist/toolchain-api.d.ts +9 -0
  151. package/dist/toolchain-api.js +17 -2
  152. package/dist/toolchain-api.js.map +1 -1
  153. package/dist/toolchain-resolve.d.ts +3 -0
  154. package/dist/toolchain-resolve.js +15 -1
  155. package/dist/toolchain-resolve.js.map +1 -1
  156. package/dist/toolchain.d.ts +6 -0
  157. package/dist/toolchain.js +19 -0
  158. package/dist/toolchain.js.map +1 -1
  159. package/dist/update-notice.js +2 -1
  160. package/dist/update-notice.js.map +1 -1
  161. package/package.json +2 -2
  162. package/templates/AGENTS.md.tmpl +6 -1
  163. package/templates/claude-skill.md.tmpl +5 -0
  164. package/templates/package.json.tmpl +4 -4
  165. package/templates/skills/build-agent/SKILL.md +103 -17
  166. package/templates/skills/build-agent/references/agent-json.md +4 -2
  167. package/templates/skills/build-agent/references/rollout.md +27 -4
  168. package/templates/skills/build-agent/references/set-up-an-integration.md +55 -7
  169. package/templates/skills/build-agent/references/skills.md +20 -1
  170. package/templates/skills/build-agent/references/triggers.md +46 -11
  171. package/templates/skills/build-integration/references/widget-design.md +3 -3
  172. package/templates/skills/ekanos-platform/SKILL.md +43 -16
  173. package/templates/skills/polish-widgets/references/patterns.md +2 -2
package/dist/runs-api.js CHANGED
@@ -1,11 +1,11 @@
1
- import { AgentApprovalDecidedSchema, AgentApprovalListSchema, AgentRunDetailSchema, AgentRunListSchema, AgentRunStartedSchema, } from '@ekanos/integration-schema';
1
+ import { AgentApprovalDecidedSchema, AgentApprovalListSchema, AgentRunCancelledSchema, AgentRunDetailSchema, AgentRunListSchema, AgentRunStartedSchema, } from '@ekanos/integration-schema';
2
2
  import { request } from './auth/ekanos-api.js';
3
3
  import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, } from './errors.js';
4
4
  export function listRuns(host, accessToken, target) {
5
5
  return call(host, accessToken, AgentRunListSchema, {
6
6
  method: 'GET',
7
7
  path: `${teamPath(target)}/runs`,
8
- query: Object.assign({ source: target.source }, (target.agent ? { agent: target.agent } : {})),
8
+ query: Object.assign(Object.assign(Object.assign({ source: target.source }, (target.agent ? { agent: target.agent } : {})), (target.status ? { status: target.status } : {})), (target.since ? { since: target.since } : {})),
9
9
  });
10
10
  }
11
11
  export function startRun(host, accessToken, target, body) {
@@ -23,6 +23,13 @@ export function showRun(host, accessToken, target) {
23
23
  query: Object.assign({ source: target.source }, (target.after ? { after: String(target.after) } : {})),
24
24
  });
25
25
  }
26
+ export function cancelRun(host, accessToken, target) {
27
+ return call(host, accessToken, AgentRunCancelledSchema, {
28
+ method: 'POST',
29
+ path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}/cancel`,
30
+ query: { source: target.source },
31
+ });
32
+ }
26
33
  export function listApprovals(host, accessToken, target) {
27
34
  return call(host, accessToken, AgentApprovalListSchema, {
28
35
  method: 'GET',
@@ -62,7 +69,9 @@ async function call(host, accessToken, schema, init) {
62
69
  if (response.status === 403) {
63
70
  throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
64
71
  ? "Only a Source admin runs its teams' agents and decides their approvals."
65
- : 'Run "ekanos sources" to list the sources you hold a seat on.');
72
+ : code === 'NOT_A_TEAM_APPROVER'
73
+ ? "A team's own agent's approvals are the team's: a member who manages its agents decides them, in the app."
74
+ : 'Run "ekanos sources" to list the sources you hold a seat on.');
66
75
  }
67
76
  if (response.status === 404) {
68
77
  throw notFoundError(message !== null && message !== void 0 ? message : 'Not found.', code === 'TEAM_NOT_FOUND'
@@ -78,9 +87,15 @@ async function call(host, accessToken, schema, init) {
78
87
  if (response.status === 409 || response.status === 502) {
79
88
  throw invalidStateError(message !== null && message !== void 0 ? message : 'The host refused the request in its current state.', code === 'ALREADY_DECIDED'
80
89
  ? 'Run "ekanos approvals list --team <slug>" for what is still waiting.'
81
- : code === 'RESUME_FAILED'
82
- ? 'The decision is saved; the run picks it up shortly. Check with "ekanos runs show <run-id>".'
83
- : 'Check the agent in the app: it may be paused, need a connection, or Agents may be off for this Source.');
90
+ : code === 'RUN_FINISHED'
91
+ ? 'Nothing to stop. "ekanos runs show <run-id> --team <slug>" shows how it ended.'
92
+ : code === 'CANCEL_FAILED'
93
+ ? 'Nothing changed. Retry "ekanos runs cancel" shortly.'
94
+ : code === 'RESUME_FAILED'
95
+ ? 'The decision is saved; the run picks it up shortly. Check with "ekanos runs show <run-id>".'
96
+ : code === 'AGENTS_EXPERIENCE_OFF'
97
+ ? 'Ask Ekanos to turn on the Agents Experience for this Source: only a super-admin can ("ekanos sources products" shows it as off).'
98
+ : 'Run "ekanos teams agents list <team>": the agent may be paused or still need a connection.');
84
99
  }
85
100
  if (response.status === 400) {
86
101
  throw usageError(message !== null && message !== void 0 ? message : 'The host refused the request.', code === 'NOT_EDITABLE'
@@ -1 +1 @@
1
- {"version":3,"file":"runs-api.js","sourceRoot":"","sources":["../src/runs-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentApprovalDecideRequest,\n type AgentApprovalDecided,\n AgentApprovalDecidedSchema,\n type AgentApprovalList,\n AgentApprovalListSchema,\n type AgentRunDetail,\n AgentRunDetailSchema,\n type AgentRunList,\n AgentRunListSchema,\n type AgentRunStartRequest,\n type AgentRunStarted,\n AgentRunStartedSchema,\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 team's agent runs and approvals — `ekanos runs`,\n * `ekanos agents test` and `ekanos approvals`.\n *\n * GET /api/partner/teams/<team>/runs?source=<slug>[&agent=<key>] → AgentRunList\n * POST /api/partner/teams/<team>/runs?source=<slug> → AgentRunStarted\n * GET /api/partner/teams/<team>/runs/<id>?source=<slug>[&after=] → AgentRunDetail\n * GET /api/partner/teams/<team>/approvals?source=<slug> → AgentApprovalList\n * POST /api/partner/teams/<team>/approvals/<id>?source=<slug> → AgentApprovalDecided\n */\n\nexport type RunsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\ninterface TeamTarget {\n source: string;\n team: string;\n}\n\nexport function listRuns(\n host: string,\n accessToken: string,\n target: TeamTarget & { agent?: string },\n): Promise<RunsApiResult<AgentRunList>> {\n return call(host, accessToken, AgentRunListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs`,\n query: {\n source: target.source,\n ...(target.agent ? { agent: target.agent } : {}),\n },\n });\n}\n\nexport function startRun(\n host: string,\n accessToken: string,\n target: TeamTarget,\n body: AgentRunStartRequest,\n): Promise<RunsApiResult<AgentRunStarted>> {\n return call(host, accessToken, AgentRunStartedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs`,\n query: { source: target.source },\n body,\n });\n}\n\nexport function showRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string; after?: number },\n): Promise<RunsApiResult<AgentRunDetail>> {\n return call(host, accessToken, AgentRunDetailSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}`,\n query: {\n source: target.source,\n ...(target.after ? { after: String(target.after) } : {}),\n },\n });\n}\n\nexport function listApprovals(\n host: string,\n accessToken: string,\n target: TeamTarget,\n): Promise<RunsApiResult<AgentApprovalList>> {\n return call(host, accessToken, AgentApprovalListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/approvals`,\n query: { source: target.source },\n });\n}\n\nexport function decideApproval(\n host: string,\n accessToken: string,\n target: TeamTarget & { approvalId: string },\n body: AgentApprovalDecideRequest,\n): Promise<RunsApiResult<AgentApprovalDecided>> {\n return call(host, accessToken, AgentApprovalDecidedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/approvals/${encodeURIComponent(target.approvalId)}`,\n query: { source: target.source },\n body,\n });\n}\n\nfunction teamPath(target: TeamTarget): string {\n return `/api/partner/teams/${encodeURIComponent(target.team)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n query: Record<string, string>;\n body?: unknown;\n },\n): Promise<RunsApiResult<T>> {\n const response = await request(\n `${host}${init.path}?${new URLSearchParams(init.query)}`,\n {\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\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 runs its teams' agents and decides their approvals.\"\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 ?? 'Not found.',\n code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the teams on this Source.'\n : code === 'AGENT_NOT_FOUND' || code === 'TEMPLATE_NOT_FOUND'\n ? 'Run \"ekanos agents list\" for the Source\\'s agents; a team runs one once it is turned on there.'\n : code === 'RUN_NOT_FOUND'\n ? 'Run \"ekanos runs list --team <slug>\" for the team\\'s runs.'\n : code === 'NOT_FOUND'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is waiting.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 || response.status === 502) {\n throw invalidStateError(\n message ?? 'The host refused the request in its current state.',\n code === 'ALREADY_DECIDED'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is still waiting.'\n : code === 'RESUME_FAILED'\n ? 'The decision is saved; the run picks it up shortly. Check with \"ekanos runs show <run-id>\".'\n : 'Check the agent in the app: it may be paused, need a connection, or Agents may be off for this Source.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n code === 'NOT_EDITABLE'\n ? 'Only a reply draft can be edited; approve or reject it instead.'\n : 'See \"ekanos runs --help\" / \"ekanos approvals --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path} 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 runs routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${init.path} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the runs 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"]}
1
+ {"version":3,"file":"runs-api.js","sourceRoot":"","sources":["../src/runs-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentApprovalDecideRequest,\n type AgentApprovalDecided,\n AgentApprovalDecidedSchema,\n type AgentApprovalList,\n AgentApprovalListSchema,\n type AgentRunCancelled,\n AgentRunCancelledSchema,\n type AgentRunDetail,\n AgentRunDetailSchema,\n type AgentRunList,\n AgentRunListSchema,\n type AgentRunStartRequest,\n type AgentRunStarted,\n AgentRunStartedSchema,\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 team's agent runs and approvals — `ekanos runs`,\n * `ekanos agents test` and `ekanos approvals`.\n *\n * GET /api/partner/teams/<team>/runs?source=<slug>[&agent=<key>] → AgentRunList\n * POST /api/partner/teams/<team>/runs?source=<slug> → AgentRunStarted\n * GET /api/partner/teams/<team>/runs/<id>?source=<slug>[&after=] → AgentRunDetail\n * POST /api/partner/teams/<team>/runs/<id>/cancel?source=<slug> → AgentRunCancelled\n * GET /api/partner/teams/<team>/approvals?source=<slug> → AgentApprovalList\n * POST /api/partner/teams/<team>/approvals/<id>?source=<slug> → AgentApprovalDecided\n */\n\nexport type RunsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\ninterface TeamTarget {\n source: string;\n team: string;\n}\n\nexport function listRuns(\n host: string,\n accessToken: string,\n target: TeamTarget & { agent?: string; status?: string; since?: string },\n): Promise<RunsApiResult<AgentRunList>> {\n return call(host, accessToken, AgentRunListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs`,\n query: {\n source: target.source,\n ...(target.agent ? { agent: target.agent } : {}),\n ...(target.status ? { status: target.status } : {}),\n ...(target.since ? { since: target.since } : {}),\n },\n });\n}\n\nexport function startRun(\n host: string,\n accessToken: string,\n target: TeamTarget,\n body: AgentRunStartRequest,\n): Promise<RunsApiResult<AgentRunStarted>> {\n return call(host, accessToken, AgentRunStartedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs`,\n query: { source: target.source },\n body,\n });\n}\n\nexport function showRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string; after?: number },\n): Promise<RunsApiResult<AgentRunDetail>> {\n return call(host, accessToken, AgentRunDetailSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}`,\n query: {\n source: target.source,\n ...(target.after ? { after: String(target.after) } : {}),\n },\n });\n}\n\nexport function cancelRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string },\n): Promise<RunsApiResult<AgentRunCancelled>> {\n return call(host, accessToken, AgentRunCancelledSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}/cancel`,\n query: { source: target.source },\n });\n}\n\nexport function listApprovals(\n host: string,\n accessToken: string,\n target: TeamTarget,\n): Promise<RunsApiResult<AgentApprovalList>> {\n return call(host, accessToken, AgentApprovalListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/approvals`,\n query: { source: target.source },\n });\n}\n\nexport function decideApproval(\n host: string,\n accessToken: string,\n target: TeamTarget & { approvalId: string },\n body: AgentApprovalDecideRequest,\n): Promise<RunsApiResult<AgentApprovalDecided>> {\n return call(host, accessToken, AgentApprovalDecidedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/approvals/${encodeURIComponent(target.approvalId)}`,\n query: { source: target.source },\n body,\n });\n}\n\nfunction teamPath(target: TeamTarget): string {\n return `/api/partner/teams/${encodeURIComponent(target.team)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n query: Record<string, string>;\n body?: unknown;\n },\n): Promise<RunsApiResult<T>> {\n const response = await request(\n `${host}${init.path}?${new URLSearchParams(init.query)}`,\n {\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\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 runs its teams' agents and decides their approvals.\"\n : code === 'NOT_A_TEAM_APPROVER'\n ? \"A team's own agent's approvals are the team's: a member who manages its agents decides them, in the app.\"\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 ?? 'Not found.',\n code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the teams on this Source.'\n : code === 'AGENT_NOT_FOUND' || code === 'TEMPLATE_NOT_FOUND'\n ? 'Run \"ekanos agents list\" for the Source\\'s agents; a team runs one once it is turned on there.'\n : code === 'RUN_NOT_FOUND'\n ? 'Run \"ekanos runs list --team <slug>\" for the team\\'s runs.'\n : code === 'NOT_FOUND'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is waiting.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 || response.status === 502) {\n throw invalidStateError(\n message ?? 'The host refused the request in its current state.',\n code === 'ALREADY_DECIDED'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is still waiting.'\n : code === 'RUN_FINISHED'\n ? 'Nothing to stop. \"ekanos runs show <run-id> --team <slug>\" shows how it ended.'\n : code === 'CANCEL_FAILED'\n ? 'Nothing changed. Retry \"ekanos runs cancel\" shortly.'\n : code === 'RESUME_FAILED'\n ? 'The decision is saved; the run picks it up shortly. Check with \"ekanos runs show <run-id>\".'\n : code === 'AGENTS_EXPERIENCE_OFF'\n ? 'Ask Ekanos to turn on the Agents Experience for this Source: only a super-admin can (\"ekanos sources products\" shows it as off).'\n : 'Run \"ekanos teams agents list <team>\": the agent may be paused or still need a connection.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n code === 'NOT_EDITABLE'\n ? 'Only a reply draft can be edited; approve or reject it instead.'\n : 'See \"ekanos runs --help\" / \"ekanos approvals --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path} 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 runs routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${init.path} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the runs 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"]}
package/dist/safe-fs.d.ts CHANGED
@@ -45,6 +45,8 @@ export declare function classifyPath(destPath: string): PathKind;
45
45
  * there is no window between deciding and reading.
46
46
  */
47
47
  export declare function readFileNoFollow(root: string, filePath: string): string | null;
48
+ /** `readFileNoFollow`, as bytes — for comparing a binary file exactly. */
49
+ export declare function readBytesNoFollow(root: string, filePath: string): Buffer | null;
48
50
  /**
49
51
  * Write a file, creating parent directories, refusing every symlink on the
50
52
  * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe
package/dist/safe-fs.js CHANGED
@@ -31,11 +31,11 @@ export function isContained(root, target) {
31
31
  return !relative.startsWith('..') && !path.isAbsolute(relative);
32
32
  }
33
33
  function containmentError(root, target) {
34
- throw invalidStateError(`Refusing to write ${target}: it resolves outside the project root ${root}.`, 'Remove the symlink or path component that redirects it, then re-run ' +
35
- '"ekanos dev".');
34
+ throw invalidStateError(`Refusing to write ${target}: it resolves outside ${root}.`, 'Remove the symlink or path component that redirects it, then re-run ' +
35
+ 'the command.');
36
36
  }
37
37
  function symlinkError(link) {
38
- throw invalidStateError(`Refusing to write through the symlink ${link}.`, `"ekanos dev" only writes real files inside your project. Delete ${link} ` +
38
+ throw invalidStateError(`Refusing to write through the symlink ${link}.`, `The CLI only writes real files inside the folder it writes to. Delete ${link} ` +
39
39
  '(or the tree containing it) and re-run.');
40
40
  }
41
41
  /**
@@ -102,8 +102,8 @@ export function mkdirSafe(root, dir) {
102
102
  if (stats.isSymbolicLink())
103
103
  symlinkError(current);
104
104
  if (!stats.isDirectory()) {
105
- throw invalidStateError(`${current} exists and is not a directory, so the harness shell ` +
106
- 'cannot be written there.', `Move or delete ${current}, then re-run "ekanos dev".`);
105
+ throw invalidStateError(`${current} exists and is not a directory, so nothing can be ` +
106
+ 'written there.', `Move or delete ${current}, then re-run the command.`);
107
107
  }
108
108
  }
109
109
  }
@@ -131,6 +131,12 @@ export function classifyPath(destPath) {
131
131
  * there is no window between deciding and reading.
132
132
  */
133
133
  export function readFileNoFollow(root, filePath) {
134
+ var _a;
135
+ var _b;
136
+ return (_b = (_a = readBytesNoFollow(root, filePath)) === null || _a === void 0 ? void 0 : _a.toString('utf8')) !== null && _b !== void 0 ? _b : null;
137
+ }
138
+ /** `readFileNoFollow`, as bytes — for comparing a binary file exactly. */
139
+ export function readBytesNoFollow(root, filePath) {
134
140
  assertNoSymlinkedComponent(root, filePath);
135
141
  let fd;
136
142
  try {
@@ -145,7 +151,7 @@ export function readFileNoFollow(root, filePath) {
145
151
  throw error;
146
152
  }
147
153
  try {
148
- return fs.readFileSync(fd, 'utf8');
154
+ return fs.readFileSync(fd);
149
155
  }
150
156
  finally {
151
157
  fs.closeSync(fd);
@@ -1 +1 @@
1
- {"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside the project root ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n '\"ekanos dev\".',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `\"ekanos dev\" only writes real files inside your project. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so the harness shell ` +\n 'cannot be written there.',\n `Move or delete ${current}, then re-run \"ekanos dev\".`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
1
+ {"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n 'the command.',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `The CLI only writes real files inside the folder it writes to. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so nothing can be ` +\n 'written there.',\n `Move or delete ${current}, then re-run the command.`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n return readBytesNoFollow(root, filePath)?.toString('utf8') ?? null;\n}\n\n/** `readFileNoFollow`, as bytes — for comparing a binary file exactly. */\nexport function readBytesNoFollow(\n root: string,\n filePath: string,\n): Buffer | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
@@ -0,0 +1,17 @@
1
+ export declare const SECRETS_DIRNAME = "secrets";
2
+ /** Create (or tighten) `~/.ekanos/secrets` and return it; refuse if unusable. */
3
+ export declare function ensureSecretsDir(env: Record<string, string | undefined>): string;
4
+ export interface SecretFileInput {
5
+ /** The team (or, for a Source-wide secret, the Source) it belongs to. */
6
+ owner: string;
7
+ /** What it is, for the file name: `zendesk-webhook`, `triage-new-ticket-trigger`. */
8
+ what: string;
9
+ /** A line saying what the secret is for and where it goes. */
10
+ purpose: string;
11
+ /** `NAME=value` pairs, in order. */
12
+ values: Array<[name: string, value: string]>;
13
+ /** The command to run for a fresh one, should this file not be written. */
14
+ again: string;
15
+ }
16
+ /** Write the secret to a new 0600 file in `dir` and return its path. */
17
+ export declare function writeSecretFile(dir: string, input: SecretFileInput, now?: Date): string;
@@ -0,0 +1,69 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { resolveEkanosHome } from './auth/credential-store.js';
4
+ import { preconditionError } from './errors.js';
5
+ /**
6
+ * Where a secret the host shows once goes instead of the terminal: a new
7
+ * file under `~/.ekanos/secrets/`, readable only by you. A command's output
8
+ * — a person's terminal, and just as often a coding agent's context — names
9
+ * the file and what it is for, never the value. The same in a TTY and not,
10
+ * in human output and `--json` (which carries `secretFile`).
11
+ *
12
+ * The directory is 0700 and each file 0600, created exclusively (never
13
+ * overwriting, never through an existing path), the same discipline as the
14
+ * credentials file beside it. A command makes sure the directory is usable
15
+ * BEFORE it asks the host for the secret, so a secret is never minted (and
16
+ * an old one revoked) with nowhere to put it.
17
+ */
18
+ const DIR_MODE = 0o700;
19
+ const FILE_MODE = 0o600;
20
+ export const SECRETS_DIRNAME = 'secrets';
21
+ /** Create (or tighten) `~/.ekanos/secrets` and return it; refuse if unusable. */
22
+ export function ensureSecretsDir(env) {
23
+ const dir = path.join(resolveEkanosHome(env), '.ekanos', SECRETS_DIRNAME);
24
+ try {
25
+ fs.mkdirSync(dir, { recursive: true, mode: DIR_MODE });
26
+ fs.chmodSync(dir, DIR_MODE);
27
+ fs.accessSync(dir, fs.constants.W_OK);
28
+ }
29
+ catch (error) {
30
+ throw preconditionError(`${dir} is not usable for secrets: ${error instanceof Error ? error.message : String(error)}.`, 'Make it writable (or set EKANOS_HOME), then run the command again. Nothing was changed.');
31
+ }
32
+ return dir;
33
+ }
34
+ /** Write the secret to a new 0600 file in `dir` and return its path. */
35
+ export function writeSecretFile(dir, input, now = new Date()) {
36
+ const stamp = now.toISOString().replace(/[-:]/g, '').replace('.', '');
37
+ const base = `${part(input.owner)}-${part(input.what)}-${stamp}`;
38
+ const content = [
39
+ `# ${input.purpose}`,
40
+ `# Written by the ekanos CLI at ${now.toISOString()}. It is shown nowhere else.`,
41
+ '# Copy it from here yourself (not into a chat or an agent session), then delete this file.',
42
+ ...input.values.map(([name, value]) => `${name}=${value}`),
43
+ '',
44
+ ].join('\n');
45
+ let lastError = null;
46
+ for (let attempt = 1; attempt <= 20; attempt += 1) {
47
+ const file = path.join(dir, attempt === 1 ? base : `${base}-${attempt}`);
48
+ try {
49
+ fs.writeFileSync(file, content, { mode: FILE_MODE, flag: 'wx' });
50
+ fs.chmodSync(file, FILE_MODE);
51
+ return file;
52
+ }
53
+ catch (error) {
54
+ lastError = error;
55
+ if (error.code !== 'EEXIST')
56
+ break;
57
+ }
58
+ }
59
+ throw preconditionError(`The new secret could not be saved in ${dir}: ${lastError instanceof Error ? lastError.message : String(lastError)}. It was not shown.`, `Fix the directory, then make a fresh one: ${input.again}`);
60
+ }
61
+ /** A file-name-safe piece: lowercase letters, digits, dots and dashes. */
62
+ function part(value) {
63
+ return (value
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9.-]+/g, '-')
66
+ .replace(/^[-.]+|[-.]+$/g, '')
67
+ .slice(0, 60) || 'secret');
68
+ }
69
+ //# sourceMappingURL=secret-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-file.js","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { resolveEkanosHome } from './auth/credential-store';\nimport { preconditionError } from './errors';\n\n/**\n * Where a secret the host shows once goes instead of the terminal: a new\n * file under `~/.ekanos/secrets/`, readable only by you. A command's output\n * — a person's terminal, and just as often a coding agent's context — names\n * the file and what it is for, never the value. The same in a TTY and not,\n * in human output and `--json` (which carries `secretFile`).\n *\n * The directory is 0700 and each file 0600, created exclusively (never\n * overwriting, never through an existing path), the same discipline as the\n * credentials file beside it. A command makes sure the directory is usable\n * BEFORE it asks the host for the secret, so a secret is never minted (and\n * an old one revoked) with nowhere to put it.\n */\n\nconst DIR_MODE = 0o700;\nconst FILE_MODE = 0o600;\n\nexport const SECRETS_DIRNAME = 'secrets';\n\n/** Create (or tighten) `~/.ekanos/secrets` and return it; refuse if unusable. */\nexport function ensureSecretsDir(\n env: Record<string, string | undefined>,\n): string {\n const dir = path.join(resolveEkanosHome(env), '.ekanos', SECRETS_DIRNAME);\n try {\n fs.mkdirSync(dir, { recursive: true, mode: DIR_MODE });\n fs.chmodSync(dir, DIR_MODE);\n fs.accessSync(dir, fs.constants.W_OK);\n } catch (error) {\n throw preconditionError(\n `${dir} is not usable for secrets: ${error instanceof Error ? error.message : String(error)}.`,\n 'Make it writable (or set EKANOS_HOME), then run the command again. Nothing was changed.',\n );\n }\n return dir;\n}\n\nexport interface SecretFileInput {\n /** The team (or, for a Source-wide secret, the Source) it belongs to. */\n owner: string;\n /** What it is, for the file name: `zendesk-webhook`, `triage-new-ticket-trigger`. */\n what: string;\n /** A line saying what the secret is for and where it goes. */\n purpose: string;\n /** `NAME=value` pairs, in order. */\n values: Array<[name: string, value: string]>;\n /** The command to run for a fresh one, should this file not be written. */\n again: string;\n}\n\n/** Write the secret to a new 0600 file in `dir` and return its path. */\nexport function writeSecretFile(\n dir: string,\n input: SecretFileInput,\n now: Date = new Date(),\n): string {\n const stamp = now.toISOString().replace(/[-:]/g, '').replace('.', '');\n const base = `${part(input.owner)}-${part(input.what)}-${stamp}`;\n const content = [\n `# ${input.purpose}`,\n `# Written by the ekanos CLI at ${now.toISOString()}. It is shown nowhere else.`,\n '# Copy it from here yourself (not into a chat or an agent session), then delete this file.',\n ...input.values.map(([name, value]) => `${name}=${value}`),\n '',\n ].join('\\n');\n\n let lastError: unknown = null;\n for (let attempt = 1; attempt <= 20; attempt += 1) {\n const file = path.join(dir, attempt === 1 ? base : `${base}-${attempt}`);\n try {\n fs.writeFileSync(file, content, { mode: FILE_MODE, flag: 'wx' });\n fs.chmodSync(file, FILE_MODE);\n return file;\n } catch (error) {\n lastError = error;\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') break;\n }\n }\n throw preconditionError(\n `The new secret could not be saved in ${dir}: ${lastError instanceof Error ? lastError.message : String(lastError)}. It was not shown.`,\n `Fix the directory, then make a fresh one: ${input.again}`,\n );\n}\n\n/** A file-name-safe piece: lowercase letters, digits, dots and dashes. */\nfunction part(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, '-')\n .replace(/^[-.]+|[-.]+$/g, '')\n .slice(0, 60) || 'secret'\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type SourceSkillList, type SourceSkillPushRequest, type SourceSkillPushResult, type SourceSkillShow } from '@ekanos/integration-schema';
1
+ import { type SourceSkillArchiveResult, type SourceSkillList, type SourceSkillPushRequest, type SourceSkillPushResult, type SourceSkillShow } from '@ekanos/integration-schema';
2
2
  /**
3
3
  * The CLI's client for a Source's skills.
4
4
  *
@@ -9,12 +9,15 @@ import { type SourceSkillList, type SourceSkillPushRequest, type SourceSkillPush
9
9
  * GET /api/partner/skills?source=<s>[&team=<t>] → { ok, data: SourceSkillList }
10
10
  * GET /api/partner/skills/<slug>?source=<s>[&team=<t>] → { ok, data: SourceSkillShow }
11
11
  * POST /api/partner/skills SourceSkillPushRequest → { ok, data: SourceSkillPushResult }
12
+ * POST /api/partner/skills/<slug>/archive | unarchive { source, team? }
13
+ * → { ok, data: SourceSkillArchiveResult }
12
14
  *
13
15
  * → 401 / 3xx refresh once and retry
14
16
  * → 400 { code: "INVALID_SKILL", issues: { path, message }[] }
15
17
  * → 403 { code: "MFA_REQUIRED" | "FORBIDDEN" | "SOURCE_ADMIN_REQUIRED" }
16
18
  * → 404 { code: "SOURCE_NOT_FOUND" | "TEAM_NOT_FOUND" | "SKILL_NOT_FOUND" }
17
- * → 409 { code: "SKILL_NOT_PROMPT" }
19
+ * → 409 { code: "SKILL_NOT_PROMPT" | "SKILL_ARCHIVED" }
20
+ * → 409 { code: "SKILL_IN_USE", inUse: SourceSkillInUse } archive
18
21
  * ---------------------------------------------------------------------------
19
22
  */
20
23
  export type SkillsApiResult<T> = {
@@ -26,3 +29,8 @@ export type SkillsApiResult<T> = {
26
29
  export declare function listSkills(host: string, accessToken: string, source: string, team?: string): Promise<SkillsApiResult<SourceSkillList>>;
27
30
  export declare function showSkill(host: string, accessToken: string, source: string, slug: string, team?: string): Promise<SkillsApiResult<SourceSkillShow>>;
28
31
  export declare function pushSkill(host: string, accessToken: string, body: SourceSkillPushRequest): Promise<SkillsApiResult<SourceSkillPushResult>>;
32
+ export declare function archiveSkill(host: string, accessToken: string, action: 'archive' | 'unarchive', params: {
33
+ source: string;
34
+ slug: string;
35
+ team?: string;
36
+ }): Promise<SkillsApiResult<SourceSkillArchiveResult>>;
@@ -1,4 +1,4 @@
1
- import { SourceSkillListSchema, SourceSkillPushResultSchema, SourceSkillShowSchema, } from '@ekanos/integration-schema';
1
+ import { SourceSkillArchiveResultSchema, SourceSkillInUseSchema, SourceSkillListSchema, SourceSkillPushResultSchema, SourceSkillShowSchema, } from '@ekanos/integration-schema';
2
2
  import { request } from './auth/ekanos-api.js';
3
3
  import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, validationError, } from './errors.js';
4
4
  const ROUTE = '/api/partner/skills';
@@ -27,6 +27,15 @@ export function pushSkill(host, accessToken, body) {
27
27
  subject: body.slug,
28
28
  });
29
29
  }
30
+ export function archiveSkill(host, accessToken, action, params) {
31
+ return call(host, accessToken, SourceSkillArchiveResultSchema, {
32
+ method: 'POST',
33
+ path: `${ROUTE}/${encodeURIComponent(params.slug)}/${action}`,
34
+ body: Object.assign({ source: params.source }, (params.team ? { team: params.team } : {})),
35
+ subject: params.slug,
36
+ source: params.source,
37
+ });
38
+ }
30
39
  async function call(host, accessToken, schema, init) {
31
40
  const response = await request(`${host}${init.path}`, {
32
41
  method: init.method,
@@ -63,7 +72,7 @@ async function call(host, accessToken, schema, init) {
63
72
  }
64
73
  if (response.status === 403) {
65
74
  throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
66
- ? 'Only a Source admin pushes skills or reads team skills. Ask one to run it.'
75
+ ? 'Only a Source admin pushes or archives skills, or reads team skills. Ask one to run it.'
67
76
  : 'Run "ekanos sources" to list the sources you hold a seat on.');
68
77
  }
69
78
  if (response.status === 404) {
@@ -73,6 +82,12 @@ async function call(host, accessToken, schema, init) {
73
82
  ? 'Run "ekanos teams list" to see the Source\'s teams.'
74
83
  : 'Run "ekanos skills list" to see the skills.');
75
84
  }
85
+ if (response.status === 409 && code === 'SKILL_ARCHIVED') {
86
+ throw invalidStateError(message !== null && message !== void 0 ? message : `"${init.subject}" is archived.`, `Run "ekanos skills unarchive ${init.subject}" first, or push it under another slug.`);
87
+ }
88
+ if (response.status === 409 && code === 'SKILL_IN_USE') {
89
+ throw skillInUseError(message !== null && message !== void 0 ? message : `"${init.subject}" is still in use.`, body.inUse, init.subject, init.source);
90
+ }
76
91
  if (response.status === 409) {
77
92
  throw invalidStateError(message !== null && message !== void 0 ? message : `"${init.subject}" cannot be pushed.`, 'Rename the skill folder (its slug) and push again.');
78
93
  }
@@ -87,6 +102,26 @@ async function call(host, accessToken, schema, init) {
87
102
  }
88
103
  return { status: 'ok', data: parsed.data };
89
104
  }
105
+ /** What still uses the skill, and the command that stops each use. */
106
+ function skillInUseError(message, value, slug, source) {
107
+ const parsed = SourceSkillInUseSchema.safeParse(value);
108
+ const inUse = parsed.success ? parsed.data : { templates: [], agents: [] };
109
+ const on = source ? ` --source ${source}` : '';
110
+ return invalidStateError([
111
+ message,
112
+ ...inUse.templates.map((template) => ` agent ${template.key} (${template.name}) names it`),
113
+ ...inUse.agents.map((agent) => ` ${agent.team}: agent ${agent.agent} has it attached` +
114
+ (agent.fromTemplate
115
+ ? ' (from an older template version it is pinned to)'
116
+ : '')),
117
+ ].join('\n'), [
118
+ 'Stop each use, then archive again:',
119
+ ...inUse.templates.map((template) => ` ekanos agents pull ${template.key}${on}, drop "${slug}" from its skills, then agents apply and agents publish (or archive the agent: ekanos agents archive ${template.key}${on})`),
120
+ ...inUse.agents.map((agent) => agent.fromTemplate
121
+ ? ` ekanos teams agents unpin ${agent.team} ${agent.agent}${on}`
122
+ : ` ekanos teams agents skills ${agent.team} ${agent.agent} remove ${slug}${on}`),
123
+ ].join('\n'));
124
+ }
90
125
  async function readOptionalJson(response) {
91
126
  try {
92
127
  const parsed = await response.json();
@@ -1 +1 @@
1
- {"version":3,"file":"skills-api.js","sourceRoot":"","sources":["../src/skills-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type SourceSkillList,\n SourceSkillListSchema,\n type SourceSkillPushRequest,\n type SourceSkillPushResult,\n SourceSkillPushResultSchema,\n type SourceSkillShow,\n SourceSkillShowSchema,\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 validationError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's skills.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the `SourceSkill*` schemas in `@ekanos/integration-schema`,\n * which the host's `/api/partner/skills` routes build and this parses.\n *\n * GET /api/partner/skills?source=<s>[&team=<t>] → { ok, data: SourceSkillList }\n * GET /api/partner/skills/<slug>?source=<s>[&team=<t>] → { ok, data: SourceSkillShow }\n * POST /api/partner/skills SourceSkillPushRequest → { ok, data: SourceSkillPushResult }\n *\n * → 401 / 3xx refresh once and retry\n * → 400 { code: \"INVALID_SKILL\", issues: { path, message }[] }\n * → 403 { code: \"MFA_REQUIRED\" | \"FORBIDDEN\" | \"SOURCE_ADMIN_REQUIRED\" }\n * → 404 { code: \"SOURCE_NOT_FOUND\" | \"TEAM_NOT_FOUND\" | \"SKILL_NOT_FOUND\" }\n * → 409 { code: \"SKILL_NOT_PROMPT\" }\n * ---------------------------------------------------------------------------\n */\n\nexport type SkillsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst ROUTE = '/api/partner/skills';\n\nfunction query(source: string, team: string | undefined) {\n return new URLSearchParams(team ? { source, team } : { source });\n}\n\nexport function listSkills(\n host: string,\n accessToken: string,\n source: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillList>> {\n return call(host, accessToken, SourceSkillListSchema, {\n method: 'GET',\n path: `${ROUTE}?${query(source, team)}`,\n subject: team ?? source,\n });\n}\n\nexport function showSkill(\n host: string,\n accessToken: string,\n source: string,\n slug: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillShow>> {\n return call(host, accessToken, SourceSkillShowSchema, {\n method: 'GET',\n path: `${ROUTE}/${encodeURIComponent(slug)}?${query(source, team)}`,\n subject: slug,\n });\n}\n\nexport function pushSkill(\n host: string,\n accessToken: string,\n body: SourceSkillPushRequest,\n): Promise<SkillsApiResult<SourceSkillPushResult>> {\n return call(host, accessToken, SourceSkillPushResultSchema, {\n method: 'POST',\n path: ROUTE,\n body,\n subject: body.slug,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n body?: unknown;\n subject: string;\n },\n): Promise<SkillsApiResult<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 = str(body.code);\n const message = str(body.error);\n\n if (response.status === 400 && code === 'INVALID_SKILL') {\n const issues = Array.isArray(body.issues)\n ? body.issues.flatMap((issue: unknown) => {\n if (typeof issue !== 'object' || issue === null) return [];\n const { path, message: text } = issue as Record<string, unknown>;\n return [` ${String(path)}: ${String(text)}`];\n })\n : [];\n throw validationError(\n [message ?? 'The host refused the skill.', ...issues].join('\\n'),\n 'Fix the skill folder and push again.',\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw usageError(\n message ??\n (response.status === 413\n ? 'The skill folder is too large to send.'\n : 'The host refused the request.'),\n 'See \"ekanos skills --help\" for each subcommand\\'s arguments.',\n );\n }\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\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 pushes skills or reads team skills. Ask one to run it.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${init.subject}\" was not found.`,\n code === 'SOURCE_NOT_FOUND'\n ? 'Run \"ekanos sources\" to list the sources you hold a seat on.'\n : code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the Source\\'s teams.'\n : 'Run \"ekanos skills list\" to see the skills.',\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n message ?? `\"${init.subject}\" cannot be pushed.`,\n 'Rename the skill folder (its slug) and push again.',\n );\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 skills routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${ROUTE} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the skills contract.',\n );\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\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
1
+ {"version":3,"file":"skills-api.js","sourceRoot":"","sources":["../src/skills-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type SourceSkillArchiveResult,\n SourceSkillArchiveResultSchema,\n SourceSkillInUseSchema,\n type SourceSkillList,\n SourceSkillListSchema,\n type SourceSkillPushRequest,\n type SourceSkillPushResult,\n SourceSkillPushResultSchema,\n type SourceSkillShow,\n SourceSkillShowSchema,\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 validationError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's skills.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the `SourceSkill*` schemas in `@ekanos/integration-schema`,\n * which the host's `/api/partner/skills` routes build and this parses.\n *\n * GET /api/partner/skills?source=<s>[&team=<t>] → { ok, data: SourceSkillList }\n * GET /api/partner/skills/<slug>?source=<s>[&team=<t>] → { ok, data: SourceSkillShow }\n * POST /api/partner/skills SourceSkillPushRequest → { ok, data: SourceSkillPushResult }\n * POST /api/partner/skills/<slug>/archive | unarchive { source, team? }\n * → { ok, data: SourceSkillArchiveResult }\n *\n * → 401 / 3xx refresh once and retry\n * → 400 { code: \"INVALID_SKILL\", issues: { path, message }[] }\n * → 403 { code: \"MFA_REQUIRED\" | \"FORBIDDEN\" | \"SOURCE_ADMIN_REQUIRED\" }\n * → 404 { code: \"SOURCE_NOT_FOUND\" | \"TEAM_NOT_FOUND\" | \"SKILL_NOT_FOUND\" }\n * → 409 { code: \"SKILL_NOT_PROMPT\" | \"SKILL_ARCHIVED\" }\n * → 409 { code: \"SKILL_IN_USE\", inUse: SourceSkillInUse } archive\n * ---------------------------------------------------------------------------\n */\n\nexport type SkillsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst ROUTE = '/api/partner/skills';\n\nfunction query(source: string, team: string | undefined) {\n return new URLSearchParams(team ? { source, team } : { source });\n}\n\nexport function listSkills(\n host: string,\n accessToken: string,\n source: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillList>> {\n return call(host, accessToken, SourceSkillListSchema, {\n method: 'GET',\n path: `${ROUTE}?${query(source, team)}`,\n subject: team ?? source,\n });\n}\n\nexport function showSkill(\n host: string,\n accessToken: string,\n source: string,\n slug: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillShow>> {\n return call(host, accessToken, SourceSkillShowSchema, {\n method: 'GET',\n path: `${ROUTE}/${encodeURIComponent(slug)}?${query(source, team)}`,\n subject: slug,\n });\n}\n\nexport function pushSkill(\n host: string,\n accessToken: string,\n body: SourceSkillPushRequest,\n): Promise<SkillsApiResult<SourceSkillPushResult>> {\n return call(host, accessToken, SourceSkillPushResultSchema, {\n method: 'POST',\n path: ROUTE,\n body,\n subject: body.slug,\n });\n}\n\nexport function archiveSkill(\n host: string,\n accessToken: string,\n action: 'archive' | 'unarchive',\n params: { source: string; slug: string; team?: string },\n): Promise<SkillsApiResult<SourceSkillArchiveResult>> {\n return call(host, accessToken, SourceSkillArchiveResultSchema, {\n method: 'POST',\n path: `${ROUTE}/${encodeURIComponent(params.slug)}/${action}`,\n body: {\n source: params.source,\n ...(params.team ? { team: params.team } : {}),\n },\n subject: params.slug,\n source: params.source,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n body?: unknown;\n subject: string;\n /** For the commands a refusal suggests. */\n source?: string;\n },\n): Promise<SkillsApiResult<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 = str(body.code);\n const message = str(body.error);\n\n if (response.status === 400 && code === 'INVALID_SKILL') {\n const issues = Array.isArray(body.issues)\n ? body.issues.flatMap((issue: unknown) => {\n if (typeof issue !== 'object' || issue === null) return [];\n const { path, message: text } = issue as Record<string, unknown>;\n return [` ${String(path)}: ${String(text)}`];\n })\n : [];\n throw validationError(\n [message ?? 'The host refused the skill.', ...issues].join('\\n'),\n 'Fix the skill folder and push again.',\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw usageError(\n message ??\n (response.status === 413\n ? 'The skill folder is too large to send.'\n : 'The host refused the request.'),\n 'See \"ekanos skills --help\" for each subcommand\\'s arguments.',\n );\n }\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\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 pushes or archives skills, or reads team skills. Ask one to run it.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${init.subject}\" was not found.`,\n code === 'SOURCE_NOT_FOUND'\n ? 'Run \"ekanos sources\" to list the sources you hold a seat on.'\n : code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the Source\\'s teams.'\n : 'Run \"ekanos skills list\" to see the skills.',\n );\n }\n\n if (response.status === 409 && code === 'SKILL_ARCHIVED') {\n throw invalidStateError(\n message ?? `\"${init.subject}\" is archived.`,\n `Run \"ekanos skills unarchive ${init.subject}\" first, or push it under another slug.`,\n );\n }\n\n if (response.status === 409 && code === 'SKILL_IN_USE') {\n throw skillInUseError(\n message ?? `\"${init.subject}\" is still in use.`,\n body.inUse,\n init.subject,\n init.source,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n message ?? `\"${init.subject}\" cannot be pushed.`,\n 'Rename the skill folder (its slug) and push again.',\n );\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 skills routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${ROUTE} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the skills contract.',\n );\n }\n\n return { status: 'ok', data: parsed.data };\n}\n\n/** What still uses the skill, and the command that stops each use. */\nfunction skillInUseError(\n message: string,\n value: unknown,\n slug: string,\n source: string | undefined,\n) {\n const parsed = SourceSkillInUseSchema.safeParse(value);\n const inUse = parsed.success ? parsed.data : { templates: [], agents: [] };\n const on = source ? ` --source ${source}` : '';\n return invalidStateError(\n [\n message,\n ...inUse.templates.map(\n (template) => ` agent ${template.key} (${template.name}) names it`,\n ),\n ...inUse.agents.map(\n (agent) =>\n ` ${agent.team}: agent ${agent.agent} has it attached` +\n (agent.fromTemplate\n ? ' (from an older template version it is pinned to)'\n : ''),\n ),\n ].join('\\n'),\n [\n 'Stop each use, then archive again:',\n ...inUse.templates.map(\n (template) =>\n ` ekanos agents pull ${template.key}${on}, drop \"${slug}\" from its skills, then agents apply and agents publish (or archive the agent: ekanos agents archive ${template.key}${on})`,\n ),\n ...inUse.agents.map((agent) =>\n agent.fromTemplate\n ? ` ekanos teams agents unpin ${agent.team} ${agent.agent}${on}`\n : ` ekanos teams agents skills ${agent.team} ${agent.agent} remove ${slug}${on}`,\n ),\n ].join('\\n'),\n );\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\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
@@ -13,6 +13,7 @@ export declare const SourceProductSchema: z.ZodObject<{
13
13
  plan: "plan";
14
14
  }>;
15
15
  registered: z.ZodBoolean;
16
+ owned: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
16
17
  }, z.core.$strip>;
17
18
  export type SourceProduct = z.infer<typeof SourceProductSchema>;
18
19
  export type SourceProductsApiResult<T> = {
@@ -34,8 +35,10 @@ export declare function listSourceProducts(host: string, token: string, source:
34
35
  enabled: boolean;
35
36
  lever: "entitlement" | "flag" | "plan";
36
37
  registered: boolean;
38
+ owned: boolean;
37
39
  }[];
38
40
  canEnable: boolean;
41
+ canEnableOwned: boolean;
39
42
  }>>;
40
43
  export declare function setSourceProduct(host: string, token: string, input: {
41
44
  source: string;
@@ -54,6 +57,7 @@ export declare function setSourceProduct(host: string, token: string, input: {
54
57
  enabled: boolean;
55
58
  lever: "entitlement" | "flag" | "plan";
56
59
  registered: boolean;
60
+ owned: boolean;
57
61
  };
58
62
  changed: boolean;
59
63
  }>>;
@@ -5,14 +5,17 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
5
5
  * The CLI's client for a Source's products (`ekanos sources products`).
6
6
  *
7
7
  * GET /api/partner/sources/products?source=<slug|id>
8
- * → { source, products: [{ slug, name, kind, enabled, lever, registered }], canEnable }
8
+ * → { source, products: [{ slug, name, kind, enabled, lever, registered, owned }],
9
+ * canEnable, canEnableOwned }
9
10
  * PUT /api/partner/sources/products/<product>?source=<slug|id> enable
10
11
  * DELETE /api/partner/sources/products/<product>?source=<slug|id> disable
11
12
  * → { source, product, changed }
12
13
  *
13
14
  * → 401 / 3xx refresh once and retry
14
- * → 403 SUPER_ADMIN_REQUIRED enabling is a super-admin grant, which an
15
- * `ekanos login` session never carries
15
+ * → 403 SOURCE_ADMIN_REQUIRED no Source admin seat on the Source
16
+ * → 403 SUPER_ADMIN_REQUIRED not an integration the Source built: a
17
+ * super-admin grant, which an `ekanos login`
18
+ * session never carries
16
19
  * → 409 REFUSED a dependency or demo-only guard refused
17
20
  *
18
21
  * The shapes are read here rather than from `@ekanos/integration-schema`, so
@@ -30,11 +33,15 @@ export const SourceProductSchema = z.object({
30
33
  enabled: z.boolean(),
31
34
  lever: z.enum(['flag', 'plan', 'entitlement']),
32
35
  registered: z.boolean(),
36
+ /** The Source built it, so its Source admin may switch it. Absent on an
37
+ * older host, which allowed no Source admin to. */
38
+ owned: z.boolean().optional().default(false),
33
39
  });
34
40
  const ListSchema = z.object({
35
41
  source: SourceRefSchema,
36
42
  products: z.array(SourceProductSchema),
37
43
  canEnable: z.boolean(),
44
+ canEnableOwned: z.boolean().optional().default(false),
38
45
  });
39
46
  const ChangeSchema = z.object({
40
47
  source: SourceRefSchema,
@@ -72,7 +79,10 @@ async function call(host, accessToken, schema, init) {
72
79
  throw authRequiredError(message !== null && message !== void 0 ? message : `${host} requires multi-factor authentication.`, 'Complete MFA in the browser, then run "ekanos login" again.');
73
80
  }
74
81
  if (response.status === 403 && code === 'SUPER_ADMIN_REQUIRED') {
75
- throw forbiddenError(message !== null && message !== void 0 ? message : 'Only a super-admin enables a product for a Source.', 'A super-admin does it in the browser: /admin/sources → the Source → Products (Experiences are switched on the same page).');
82
+ throw forbiddenError(message !== null && message !== void 0 ? message : 'Only a super-admin enables this product for a Source.', 'A Source admin switches only the integrations their Source built. Anything else a super-admin does in the browser: /admin/sources → the Source → Products (Experiences are switched on the same page).');
83
+ }
84
+ if (response.status === 403 && code === 'SOURCE_ADMIN_REQUIRED') {
85
+ throw forbiddenError(message !== null && message !== void 0 ? message : 'Switching a product needs a Source admin seat on the Source.', 'Run "ekanos sources" to see your seats; a Source admin grants the seat.');
76
86
  }
77
87
  if (response.status === 403) {
78
88
  throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', 'Run "ekanos sources" to list the sources you hold a seat on.');