@indemn/cli 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +9 -4
  3. package/.github/workflows/ci.yml +10 -0
  4. package/.github/workflows/publish.yml +2 -2
  5. package/.mcp.json +1 -4
  6. package/CONTRIBUTING.md +3 -1
  7. package/README.md +69 -176
  8. package/dist/cli/agents.d.ts.map +1 -1
  9. package/dist/cli/agents.js +226 -18
  10. package/dist/cli/agents.js.map +1 -1
  11. package/dist/cli/analytics.d.ts +3 -0
  12. package/dist/cli/analytics.d.ts.map +1 -0
  13. package/dist/cli/analytics.js +133 -0
  14. package/dist/cli/analytics.js.map +1 -0
  15. package/dist/cli/auth.d.ts.map +1 -1
  16. package/dist/cli/auth.js +62 -17
  17. package/dist/cli/auth.js.map +1 -1
  18. package/dist/cli/billing.d.ts +3 -0
  19. package/dist/cli/billing.d.ts.map +1 -0
  20. package/dist/cli/billing.js +23 -0
  21. package/dist/cli/billing.js.map +1 -0
  22. package/dist/cli/campaigns.d.ts +3 -0
  23. package/dist/cli/campaigns.d.ts.map +1 -0
  24. package/dist/cli/campaigns.js +34 -0
  25. package/dist/cli/campaigns.js.map +1 -0
  26. package/dist/cli/chat.d.ts.map +1 -1
  27. package/dist/cli/chat.js +69 -102
  28. package/dist/cli/chat.js.map +1 -1
  29. package/dist/cli/config.d.ts.map +1 -1
  30. package/dist/cli/config.js +71 -27
  31. package/dist/cli/config.js.map +1 -1
  32. package/dist/cli/conversations.d.ts +3 -0
  33. package/dist/cli/conversations.d.ts.map +1 -0
  34. package/dist/cli/conversations.js +129 -0
  35. package/dist/cli/conversations.js.map +1 -0
  36. package/dist/cli/distribution.d.ts +3 -0
  37. package/dist/cli/distribution.d.ts.map +1 -0
  38. package/dist/cli/distribution.js +122 -0
  39. package/dist/cli/distribution.js.map +1 -0
  40. package/dist/cli/eval.d.ts.map +1 -1
  41. package/dist/cli/eval.js +315 -71
  42. package/dist/cli/eval.js.map +1 -1
  43. package/dist/cli/functions.d.ts.map +1 -1
  44. package/dist/cli/functions.js +125 -35
  45. package/dist/cli/functions.js.map +1 -1
  46. package/dist/cli/index.js +63 -54
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/init.d.ts +27 -0
  49. package/dist/cli/init.d.ts.map +1 -0
  50. package/dist/cli/init.js +152 -0
  51. package/dist/cli/init.js.map +1 -0
  52. package/dist/cli/kb.d.ts.map +1 -1
  53. package/dist/cli/kb.js +141 -15
  54. package/dist/cli/kb.js.map +1 -1
  55. package/dist/cli/link.d.ts +21 -0
  56. package/dist/cli/link.d.ts.map +1 -0
  57. package/dist/cli/link.js +112 -0
  58. package/dist/cli/link.js.map +1 -0
  59. package/dist/cli/org.d.ts.map +1 -1
  60. package/dist/cli/org.js +79 -67
  61. package/dist/cli/org.js.map +1 -1
  62. package/dist/cli/platform.d.ts +3 -0
  63. package/dist/cli/platform.d.ts.map +1 -0
  64. package/dist/cli/platform.js +133 -0
  65. package/dist/cli/platform.js.map +1 -0
  66. package/dist/cli/tickets.d.ts +3 -0
  67. package/dist/cli/tickets.d.ts.map +1 -0
  68. package/dist/cli/tickets.js +125 -0
  69. package/dist/cli/tickets.js.map +1 -0
  70. package/dist/cli/utils/output.d.ts +15 -9
  71. package/dist/cli/utils/output.d.ts.map +1 -1
  72. package/dist/cli/utils/output.js +53 -46
  73. package/dist/cli/utils/output.js.map +1 -1
  74. package/dist/cli/voice.d.ts +3 -0
  75. package/dist/cli/voice.d.ts.map +1 -0
  76. package/dist/cli/voice.js +206 -0
  77. package/dist/cli/voice.js.map +1 -0
  78. package/dist/core/diff.d.ts +16 -0
  79. package/dist/core/diff.d.ts.map +1 -0
  80. package/dist/core/diff.js +57 -0
  81. package/dist/core/diff.js.map +1 -0
  82. package/dist/core/org-resolver.d.ts +37 -0
  83. package/dist/core/org-resolver.d.ts.map +1 -0
  84. package/dist/core/org-resolver.js +73 -0
  85. package/dist/core/org-resolver.js.map +1 -0
  86. package/dist/core/runtime.d.ts +36 -0
  87. package/dist/core/runtime.d.ts.map +1 -0
  88. package/dist/core/runtime.js +62 -0
  89. package/dist/core/runtime.js.map +1 -0
  90. package/dist/core/scope.d.ts +33 -0
  91. package/dist/core/scope.d.ts.map +1 -0
  92. package/dist/core/scope.js +37 -0
  93. package/dist/core/scope.js.map +1 -0
  94. package/dist/mcp/registry.d.ts +67 -0
  95. package/dist/mcp/registry.d.ts.map +1 -0
  96. package/dist/mcp/registry.js +195 -0
  97. package/dist/mcp/registry.js.map +1 -0
  98. package/dist/mcp/server.d.ts +9 -0
  99. package/dist/mcp/server.d.ts.map +1 -1
  100. package/dist/mcp/server.js +87 -2
  101. package/dist/mcp/server.js.map +1 -1
  102. package/dist/mcp/tool-meta.d.ts +21 -0
  103. package/dist/mcp/tool-meta.d.ts.map +1 -0
  104. package/dist/mcp/tool-meta.js +203 -0
  105. package/dist/mcp/tool-meta.js.map +1 -0
  106. package/dist/mcp/tools.d.ts +2 -1
  107. package/dist/mcp/tools.d.ts.map +1 -1
  108. package/dist/mcp/tools.js +1462 -215
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/sdk/agent-bundle.d.ts +99 -0
  111. package/dist/sdk/agent-bundle.d.ts.map +1 -0
  112. package/dist/sdk/agent-bundle.js +211 -0
  113. package/dist/sdk/agent-bundle.js.map +1 -0
  114. package/dist/sdk/agents.d.ts +54 -1
  115. package/dist/sdk/agents.d.ts.map +1 -1
  116. package/dist/sdk/agents.js +59 -5
  117. package/dist/sdk/agents.js.map +1 -1
  118. package/dist/sdk/analytics.d.ts +158 -0
  119. package/dist/sdk/analytics.d.ts.map +1 -0
  120. package/dist/sdk/analytics.js +185 -0
  121. package/dist/sdk/analytics.js.map +1 -0
  122. package/dist/sdk/auth.d.ts +34 -12
  123. package/dist/sdk/auth.d.ts.map +1 -1
  124. package/dist/sdk/auth.js +60 -77
  125. package/dist/sdk/auth.js.map +1 -1
  126. package/dist/sdk/billing.d.ts +25 -0
  127. package/dist/sdk/billing.d.ts.map +1 -0
  128. package/dist/sdk/billing.js +61 -0
  129. package/dist/sdk/billing.js.map +1 -0
  130. package/dist/sdk/campaigns.d.ts +35 -0
  131. package/dist/sdk/campaigns.d.ts.map +1 -0
  132. package/dist/sdk/campaigns.js +59 -0
  133. package/dist/sdk/campaigns.js.map +1 -0
  134. package/dist/sdk/chat.d.ts +25 -2
  135. package/dist/sdk/chat.d.ts.map +1 -1
  136. package/dist/sdk/chat.js +37 -10
  137. package/dist/sdk/chat.js.map +1 -1
  138. package/dist/sdk/client.d.ts +93 -5
  139. package/dist/sdk/client.d.ts.map +1 -1
  140. package/dist/sdk/client.js +362 -100
  141. package/dist/sdk/client.js.map +1 -1
  142. package/dist/sdk/config-change.d.ts +74 -0
  143. package/dist/sdk/config-change.d.ts.map +1 -0
  144. package/dist/sdk/config-change.js +117 -0
  145. package/dist/sdk/config-change.js.map +1 -0
  146. package/dist/sdk/config.d.ts.map +1 -1
  147. package/dist/sdk/config.js +2 -1
  148. package/dist/sdk/config.js.map +1 -1
  149. package/dist/sdk/conversations.d.ts +101 -0
  150. package/dist/sdk/conversations.d.ts.map +1 -0
  151. package/dist/sdk/conversations.js +155 -0
  152. package/dist/sdk/conversations.js.map +1 -0
  153. package/dist/sdk/credentials.d.ts +25 -0
  154. package/dist/sdk/credentials.d.ts.map +1 -0
  155. package/dist/sdk/credentials.js +44 -0
  156. package/dist/sdk/credentials.js.map +1 -0
  157. package/dist/sdk/distribution.d.ts +94 -0
  158. package/dist/sdk/distribution.d.ts.map +1 -0
  159. package/dist/sdk/distribution.js +100 -0
  160. package/dist/sdk/distribution.js.map +1 -0
  161. package/dist/sdk/eval.d.ts +81 -0
  162. package/dist/sdk/eval.d.ts.map +1 -1
  163. package/dist/sdk/eval.js +117 -0
  164. package/dist/sdk/eval.js.map +1 -1
  165. package/dist/sdk/functions.d.ts +14 -1
  166. package/dist/sdk/functions.d.ts.map +1 -1
  167. package/dist/sdk/functions.js +48 -5
  168. package/dist/sdk/functions.js.map +1 -1
  169. package/dist/sdk/index.d.ts +2 -0
  170. package/dist/sdk/index.d.ts.map +1 -1
  171. package/dist/sdk/index.js +2 -0
  172. package/dist/sdk/index.js.map +1 -1
  173. package/dist/sdk/jarvis-chat.d.ts +28 -0
  174. package/dist/sdk/jarvis-chat.d.ts.map +1 -0
  175. package/dist/sdk/jarvis-chat.js +117 -0
  176. package/dist/sdk/jarvis-chat.js.map +1 -0
  177. package/dist/sdk/kb.d.ts +69 -1
  178. package/dist/sdk/kb.d.ts.map +1 -1
  179. package/dist/sdk/kb.js +111 -24
  180. package/dist/sdk/kb.js.map +1 -1
  181. package/dist/sdk/orgs.d.ts +13 -0
  182. package/dist/sdk/orgs.d.ts.map +1 -0
  183. package/dist/sdk/orgs.js +59 -0
  184. package/dist/sdk/orgs.js.map +1 -0
  185. package/dist/sdk/platform.d.ts +62 -0
  186. package/dist/sdk/platform.d.ts.map +1 -0
  187. package/dist/sdk/platform.js +95 -0
  188. package/dist/sdk/platform.js.map +1 -0
  189. package/dist/sdk/session.d.ts +25 -0
  190. package/dist/sdk/session.d.ts.map +1 -0
  191. package/dist/sdk/session.js +69 -0
  192. package/dist/sdk/session.js.map +1 -0
  193. package/dist/sdk/skills.d.ts +46 -0
  194. package/dist/sdk/skills.d.ts.map +1 -0
  195. package/dist/sdk/skills.js +203 -0
  196. package/dist/sdk/skills.js.map +1 -0
  197. package/dist/sdk/test-chat.d.ts +52 -0
  198. package/dist/sdk/test-chat.d.ts.map +1 -0
  199. package/dist/sdk/test-chat.js +134 -0
  200. package/dist/sdk/test-chat.js.map +1 -0
  201. package/dist/sdk/tickets.d.ts +98 -0
  202. package/dist/sdk/tickets.d.ts.map +1 -0
  203. package/dist/sdk/tickets.js +91 -0
  204. package/dist/sdk/tickets.js.map +1 -0
  205. package/dist/sdk/types.d.ts +91 -6
  206. package/dist/sdk/types.d.ts.map +1 -1
  207. package/dist/sdk/voice.d.ts +147 -0
  208. package/dist/sdk/voice.d.ts.map +1 -0
  209. package/dist/sdk/voice.js +236 -0
  210. package/dist/sdk/voice.js.map +1 -0
  211. package/package.json +22 -3
  212. package/scripts/check-skills.mjs +25 -0
  213. package/skills/agent-builder/SKILL.md +89 -252
  214. package/skills/evaluations/SKILL.md +23 -234
  215. package/skills/evaluations/references/analysis.md +95 -0
  216. package/skills/evaluations/references/cli-reference.md +28 -0
  217. package/skills/evaluations/references/json-formats.md +100 -0
  218. package/src/cli/agents.ts +202 -22
  219. package/src/cli/analytics.ts +138 -0
  220. package/src/cli/auth.ts +56 -18
  221. package/src/cli/billing.ts +25 -0
  222. package/src/cli/campaigns.ts +33 -0
  223. package/src/cli/chat.ts +66 -115
  224. package/src/cli/config.ts +65 -31
  225. package/src/cli/conversations.ts +133 -0
  226. package/src/cli/distribution.ts +116 -0
  227. package/src/cli/eval.ts +254 -70
  228. package/src/cli/functions.ts +122 -36
  229. package/src/cli/index.ts +58 -55
  230. package/src/cli/init.ts +154 -0
  231. package/src/cli/kb.ts +120 -17
  232. package/src/cli/link.ts +117 -0
  233. package/src/cli/org.ts +81 -75
  234. package/src/cli/platform.ts +128 -0
  235. package/src/cli/tickets.ts +130 -0
  236. package/src/cli/utils/output.ts +51 -42
  237. package/src/cli/voice.ts +204 -0
  238. package/src/core/diff.ts +52 -0
  239. package/src/core/org-resolver.ts +113 -0
  240. package/src/core/runtime.ts +73 -0
  241. package/src/core/scope.ts +64 -0
  242. package/src/mcp/registry.ts +236 -0
  243. package/src/mcp/server.ts +84 -2
  244. package/src/mcp/tool-meta.ts +241 -0
  245. package/src/mcp/tools.ts +1529 -265
  246. package/src/sdk/agent-bundle.ts +245 -0
  247. package/src/sdk/agents.ts +74 -5
  248. package/src/sdk/analytics.ts +220 -0
  249. package/src/sdk/auth.ts +87 -107
  250. package/src/sdk/billing.ts +59 -0
  251. package/src/sdk/campaigns.ts +60 -0
  252. package/src/sdk/chat.ts +44 -10
  253. package/src/sdk/client.ts +373 -102
  254. package/src/sdk/config-change.ts +148 -0
  255. package/src/sdk/config.ts +2 -1
  256. package/src/sdk/conversations.ts +199 -0
  257. package/src/sdk/credentials.ts +53 -0
  258. package/src/sdk/distribution.ts +132 -0
  259. package/src/sdk/eval.ts +140 -0
  260. package/src/sdk/functions.ts +52 -6
  261. package/src/sdk/index.ts +2 -0
  262. package/src/sdk/jarvis-chat.ts +112 -0
  263. package/src/sdk/kb.ts +116 -29
  264. package/src/sdk/orgs.ts +62 -0
  265. package/src/sdk/platform.ts +115 -0
  266. package/src/sdk/session.ts +90 -0
  267. package/src/sdk/skills.ts +188 -0
  268. package/src/sdk/test-chat.ts +151 -0
  269. package/src/sdk/tickets.ts +120 -0
  270. package/src/sdk/types.ts +88 -6
  271. package/src/sdk/voice.ts +262 -0
  272. package/test/cli/entry.test.ts +34 -0
  273. package/test/cli/init.test.ts +66 -0
  274. package/test/cli/link.test.ts +57 -0
  275. package/test/core/org-resolver.test.ts +92 -0
  276. package/test/core/runtime.test.ts +30 -0
  277. package/test/core/scope.test.ts +93 -0
  278. package/test/harness.test.ts +24 -0
  279. package/test/helpers/config-home.ts +22 -0
  280. package/test/helpers/fake-middleware.ts +56 -0
  281. package/test/helpers/fetch-recorder.ts +75 -0
  282. package/test/helpers/jwt.ts +9 -0
  283. package/test/helpers/mcp-pair.ts +38 -0
  284. package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
  285. package/test/mcp/askers.test.ts +60 -0
  286. package/test/mcp/elicit-timeout.test.ts +29 -0
  287. package/test/mcp/registry.test.ts +169 -0
  288. package/test/mcp/shaping.test.ts +41 -0
  289. package/test/mcp/tools-snapshot.test.ts +42 -0
  290. package/test/sdk/agents.test.ts +232 -0
  291. package/test/sdk/analytics.test.ts +89 -0
  292. package/test/sdk/auth.test.ts +70 -0
  293. package/test/sdk/billing.test.ts +47 -0
  294. package/test/sdk/campaigns.test.ts +43 -0
  295. package/test/sdk/client.test.ts +80 -0
  296. package/test/sdk/config-change.test.ts +142 -0
  297. package/test/sdk/conversations.test.ts +80 -0
  298. package/test/sdk/credentials.test.ts +33 -0
  299. package/test/sdk/distribution.test.ts +55 -0
  300. package/test/sdk/eval.test.ts +155 -0
  301. package/test/sdk/kb-upload.test.ts +50 -0
  302. package/test/sdk/kb.test.ts +95 -0
  303. package/test/sdk/orgs.test.ts +48 -0
  304. package/test/sdk/platform.test.ts +89 -0
  305. package/test/sdk/renewal.test.ts +115 -0
  306. package/test/sdk/session.test.ts +60 -0
  307. package/test/sdk/skills.test.ts +235 -0
  308. package/test/sdk/test-chat.test.ts +112 -0
  309. package/test/sdk/tickets.test.ts +42 -0
  310. package/test/sdk/voice.test.ts +147 -0
  311. package/vitest.config.ts +11 -0
@@ -1,13 +1,17 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
- import { readFileSync, existsSync } from 'node:fs';
4
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
5
+ import { confirmOrExit } from './link.js';
6
+ import { CliError, EXIT } from '../core/runtime.js';
7
+ import { proposedSkills, resolveSecretArg, transferDestination } from '../sdk/skills.js';
5
8
  import { IndemnClient } from '../sdk/client.js';
6
9
  import { FunctionsSDK } from '../sdk/functions.js';
7
10
  import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js';
8
11
 
9
12
  const REST_API_TYPE = 'rest_api';
10
13
  const CUSTOM_TOOL_TYPE = 'custom_tool';
14
+ const TRANSFER_CALL_TYPE = 'transfer_call';
11
15
  // Server expects kebab-case auth types (see copilot-server/utils/constants.js
12
16
  // AI_FUNCTIONS_REST_API_HEADERS). Snake_case values pass validation but silently
13
17
  // produce no Authorization header.
@@ -29,6 +33,7 @@ interface ConfigSchemaOptions {
29
33
  authUsername?: string;
30
34
  authPassword?: string;
31
35
  authToken?: string;
36
+ handoffNumber?: string;
32
37
  }
33
38
 
34
39
  function loadConfigFile(filePath: string): Record<string, unknown> {
@@ -80,19 +85,19 @@ function buildAuthorization(opts: ConfigSchemaOptions): Record<string, unknown>
80
85
  if (!opts.authKey || !opts.authValue) {
81
86
  throw new Error('--auth-key and --auth-value are required when --auth-type is api-key');
82
87
  }
83
- return { type: 'api-key', key: opts.authKey, value: opts.authValue };
88
+ return { type: 'api-key', key: opts.authKey, value: resolveSecretArg('--auth-value', opts.authValue) };
84
89
  }
85
90
  if (authType === 'basic-auth') {
86
91
  if (!opts.authUsername || !opts.authPassword) {
87
92
  throw new Error('--auth-username and --auth-password are required when --auth-type is basic-auth');
88
93
  }
89
- return { type: 'basic-auth', username: opts.authUsername, password: opts.authPassword };
94
+ return { type: 'basic-auth', username: opts.authUsername, password: resolveSecretArg('--auth-password', opts.authPassword) };
90
95
  }
91
96
  if (authType === 'bearer-token') {
92
97
  if (!opts.authToken) {
93
98
  throw new Error('--auth-token is required when --auth-type is bearer-token');
94
99
  }
95
- return { type: 'bearer-token', token: opts.authToken };
100
+ return { type: 'bearer-token', token: resolveSecretArg('--auth-token', opts.authToken) };
96
101
  }
97
102
  return undefined;
98
103
  }
@@ -119,6 +124,9 @@ function validateTypeFlagCompatibility(type: string, opts: ConfigSchemaOptions):
119
124
  }
120
125
  }
121
126
  }
127
+ if (type !== TRANSFER_CALL_TYPE && opts.handoffNumber !== undefined) {
128
+ throw new Error('--handoff-number is only valid for --type transfer_call. Use --config for other function types.');
129
+ }
122
130
  if (opts.method !== undefined) {
123
131
  const upper = opts.method.toUpperCase();
124
132
  if (!(VALID_HTTP_METHODS as readonly string[]).includes(upper)) {
@@ -151,6 +159,14 @@ function buildConfigurationSchema(opts: ConfigSchemaOptions, type: string): Reco
151
159
  if (opts.craftResponse !== undefined) result.craftResponse = !!opts.craftResponse;
152
160
  }
153
161
 
162
+ if (type === TRANSFER_CALL_TYPE) {
163
+ // Server's validateTransferCallPayload requires `handoffNumber` (the destination
164
+ // phone number for the call transfer). copilot-server/services/botToolsValidationService.js:52
165
+ // Saved the way the dashboard saves it (phoneNumber + intlTelObj, or sipUri via --config).
166
+ if (opts.handoffNumber !== undefined) result.phoneNumber = opts.handoffNumber;
167
+ if (opts.outputInstructions !== undefined) result.outputInstructions = opts.outputInstructions;
168
+ }
169
+
154
170
  return Object.keys(result).length > 0 ? result : undefined;
155
171
  }
156
172
 
@@ -166,15 +182,16 @@ function applyConfigSchemaOptions(cmd: Command): Command {
166
182
  .option('--no-craft-response', 'Return data directly without LLM crafting')
167
183
  .option('--auth-type <type>', 'Auth type: api-key | basic-auth | bearer-token | no-auth')
168
184
  .option('--auth-key <name>', 'Header name for api-key auth (e.g. X-API-Key)')
169
- .option('--auth-value <value>', 'Header value for api-key auth')
185
+ .option('--auth-value <value>', 'Header value for api-key auth: @file, env:NAME or - (stdin) keep it out of shell history')
170
186
  .option('--auth-username <user>', 'Username for basic-auth')
171
- .option('--auth-password <pass>', 'Password for basic-auth')
172
- .option('--auth-token <token>', 'Token for bearer-token auth')
187
+ .option('--auth-password <pass>', 'Password for basic-auth: @file, env:NAME or - (stdin)')
188
+ .option('--auth-token <token>', 'Token for bearer-token auth: @file, env:NAME or - (stdin)')
189
+ .option('--handoff-number <number>', 'Destination phone number for --type transfer_call (E.164, e.g. +15551234567)')
173
190
  .option('--config <path>', 'JSON file with full configurationSchema (individual flags override file values)');
174
191
  }
175
192
 
176
193
  export function registerFunctionCommands(program: Command): void {
177
- const functions = program.command('functions').description('Manage agent functions/tools');
194
+ const functions = program.command('skills').alias('functions').description('Manage agent skills, the actions an agent can take (alias: functions)');
178
195
 
179
196
  functions
180
197
  .command('list')
@@ -248,6 +265,13 @@ export function registerFunctionCommands(program: Command): void {
248
265
  }
249
266
  }
250
267
 
268
+ // For transfer_call on create, require handoffNumber (server validation rejects without it).
269
+ if (options.type === TRANSFER_CALL_TYPE) {
270
+ if (!configurationSchema || !(configurationSchema.phoneNumber || configurationSchema.handoffNumber || configurationSchema.sipUri)) {
271
+ throw new Error('--handoff-number (or --config with phoneNumber or sipUri) is required for --type transfer_call');
272
+ }
273
+ }
274
+
251
275
  const client = new IndemnClient();
252
276
  const sdk = new FunctionsSDK(client);
253
277
  const spinner = ora('Creating function...').start();
@@ -268,7 +292,7 @@ export function registerFunctionCommands(program: Command): void {
268
292
  finalFunc = await sdk.updateFunction(agentId, func._id, {
269
293
  type: options.type,
270
294
  description: options.description || '',
271
- configurationSchema,
295
+ configurationSchema: options.type === TRANSFER_CALL_TYPE ? transferDestination({}, configurationSchema) : configurationSchema,
272
296
  });
273
297
  } catch (err) {
274
298
  spinner.fail(`Function created (${func._id}) but configuration step failed.`);
@@ -327,10 +351,10 @@ export function registerFunctionCommands(program: Command): void {
327
351
  // otherwise `update --endpoint X` would wipe everything else.
328
352
  const patch = buildConfigurationSchema(options, current.type);
329
353
  if (patch) {
330
- input.configurationSchema = {
331
- ...(current.configurationSchema || {}),
332
- ...patch,
333
- };
354
+ // A new transfer destination must replace the saved one of either kind (copilot prefers sipUri, then phoneNumber).
355
+ input.configurationSchema = current.type === TRANSFER_CALL_TYPE
356
+ ? transferDestination(current.configurationSchema || {}, patch)
357
+ : { ...(current.configurationSchema || {}), ...patch };
334
358
  }
335
359
 
336
360
  // Anything actually changing? type+description are always set; require at least one explicit option.
@@ -357,24 +381,20 @@ export function registerFunctionCommands(program: Command): void {
357
381
  functions
358
382
  .command('delete')
359
383
  .argument('<agent-id>', 'Agent ID')
360
- .argument('<func-id>', 'Function ID')
361
- .description('Delete a function')
362
- .action(async (agentId: string, funcId: string) => {
384
+ .argument('<func-id>', 'Skill ID')
385
+ .option('--yes', 'Do not ask for confirmation')
386
+ .description('Delete a skill')
387
+ .action(async (agentId: string, funcId: string, options) => {
363
388
  try {
364
- const confirmed = await confirm(`Delete function ${funcId}? This cannot be undone.`);
365
- if (!confirmed) {
366
- console.log('Cancelled.');
367
- return;
368
- }
369
-
370
389
  const client = new IndemnClient();
371
390
  const sdk = new FunctionsSDK(client);
372
- const spinner = ora('Deleting function...').start();
373
-
391
+ const skill = (await sdk.listFunctions(agentId)).find((f) => f._id === funcId);
392
+ if (!skill) throw new Error(`Skill ${funcId} is not on agent ${agentId}.`);
393
+ await confirmOrExit(`Delete skill "${skill.name ?? skill.type}" (${funcId}) from agent ${agentId}?`, Boolean(options.yes));
394
+ const spinner = ora('Deleting skill...').start();
374
395
  await sdk.deleteFunction(agentId, funcId);
375
396
  spinner.stop();
376
-
377
- printSuccess(`Function ${funcId} deleted.`);
397
+ printSuccess(`Skill ${funcId} deleted.`);
378
398
  } catch (err) {
379
399
  printError(err);
380
400
  process.exit(1);
@@ -446,18 +466,23 @@ export function registerFunctionCommands(program: Command): void {
446
466
  functions
447
467
  .command('import')
448
468
  .argument('<agent-id>', 'Agent ID')
449
- .description('Import functions from a URL')
450
- .requiredOption('--url <url>', 'URL to import from')
469
+ .description('Import skills from a URL. REPLACES every skill of the agent, including the built-in handoff and FAQ retriever')
470
+ .requiredOption('--url <url>', 'URL of a skills JSON file (made by `skills export`); copilot downloads it itself')
471
+ .option('--yes', 'Do not ask for confirmation')
451
472
  .action(async (agentId: string, options) => {
452
473
  try {
453
474
  const client = new IndemnClient();
454
475
  const sdk = new FunctionsSDK(client);
455
- const spinner = ora('Importing functions...').start();
456
-
476
+ const current = await sdk.listFunctions(agentId);
477
+ const builtIn = current.filter((f) => ['human_handoff', 'faqs-retriever'].includes(f.type)).length;
478
+ await confirmOrExit(
479
+ `Replace all ${current.length} skill(s) of agent ${agentId} (${builtIn} built-in) with the file's skills? There is no undo; export first with \`indemn skills export ${agentId}\`.`,
480
+ Boolean(options.yes),
481
+ );
482
+ const spinner = ora('Importing skills...').start();
457
483
  const result = await sdk.importFunctions(agentId, options.url);
458
484
  spinner.stop();
459
-
460
- printSuccess('Functions imported successfully.');
485
+ printSuccess('Skills imported.');
461
486
  printJson(result);
462
487
  } catch (err) {
463
488
  printError(err);
@@ -465,6 +490,63 @@ export function registerFunctionCommands(program: Command): void {
465
490
  }
466
491
  });
467
492
 
493
+ functions
494
+ .command('generate')
495
+ .argument('<agent-id>', 'Agent ID')
496
+ .description('Propose skills from an API document or pasted text. Creates nothing unless --create')
497
+ .option('--file <path>', 'Document to read (for example an API description)')
498
+ .option('--text <text>', 'Text to read instead of a file')
499
+ .option('-o, --output <file>', 'Save the proposal to review and edit, then use `skills bulk-create --file`')
500
+ .option('--create', 'Create the proposed skills right away')
501
+ .option('--yes', 'With --create: do not ask for confirmation')
502
+ .action(async (agentId: string, options) => {
503
+ try {
504
+ if (!options.file && !options.text) throw new CliError('Give --file <path> or --text <text>.', EXIT.NEEDS_INPUT);
505
+ const client = new IndemnClient();
506
+ const sdk = new FunctionsSDK(client);
507
+ const spinner = ora('Reading the document...').start();
508
+ const proposal = await sdk.generateFromDoc(agentId, { filePath: options.file, text: options.text });
509
+ spinner.stop();
510
+ const skills = proposedSkills(proposal);
511
+ if (options.output) {
512
+ writeFileSync(options.output, JSON.stringify({ functions: skills ?? proposal }, null, 2) + '\n');
513
+ printSuccess(`Wrote ${options.output} (${skills?.length ?? 'unknown number of'} skill(s)).`);
514
+ }
515
+ if (!options.create) {
516
+ if (!options.output) printJson(proposal);
517
+ return;
518
+ }
519
+ if (!skills?.length) throw new CliError('Could not find a list of skills in the answer. Save it with -o, fix it, then use `skills bulk-create --file`.');
520
+ await confirmOrExit(`Create ${skills.length} skill(s) on agent ${agentId}?`, Boolean(options.yes));
521
+ const created = await sdk.bulkCreate(agentId, skills);
522
+ printSuccess(`Created ${created.length} skill(s).`);
523
+ } catch (err) {
524
+ printError(err);
525
+ process.exit(1);
526
+ }
527
+ });
528
+
529
+ functions
530
+ .command('bulk-create')
531
+ .argument('<agent-id>', 'Agent ID')
532
+ .requiredOption('--file <path>', 'JSON: { "functions": [...] } or a list of skills')
533
+ .option('--yes', 'Do not ask for confirmation')
534
+ .description('Create several skills at once (names are checked first; a failure part-way can leave some created)')
535
+ .action(async (agentId: string, options) => {
536
+ try {
537
+ const raw = JSON.parse(readFileSync(options.file, 'utf8')) as unknown;
538
+ const skills = proposedSkills(raw);
539
+ if (!skills?.length) throw new CliError(`${options.file} has no list of skills.`);
540
+ const sdk = new FunctionsSDK(new IndemnClient());
541
+ await confirmOrExit(`Create ${skills.length} skill(s) on agent ${agentId}?`, Boolean(options.yes));
542
+ const created = await sdk.bulkCreate(agentId, skills);
543
+ printSuccess(`Created ${created.length} skill(s).`);
544
+ } catch (err) {
545
+ printError(err);
546
+ process.exit(1);
547
+ }
548
+ });
549
+
468
550
  const params = functions.command('params').description('Manage function parameters');
469
551
 
470
552
  params
@@ -496,6 +578,7 @@ export function registerFunctionCommands(program: Command): void {
496
578
  .requiredOption('--type <type>', 'Parameter type (e.g. string, number, boolean)')
497
579
  .option('--description <desc>', 'Parameter description')
498
580
  .option('--required', 'Mark as required')
581
+ .option('--allowed-values <list>', 'Fixed choices, comma-separated (shown as buttons)')
499
582
  .action(async (agentId: string, funcId: string, options) => {
500
583
  try {
501
584
  const client = new IndemnClient();
@@ -504,6 +587,7 @@ export function registerFunctionCommands(program: Command): void {
504
587
  const result = await sdk.addParameter(agentId, funcId, {
505
588
  name: options.name, type: options.type,
506
589
  description: options.description, required: options.required || false,
590
+ allowedValues: options.allowedValues ? String(options.allowedValues).split(',').map((v: string) => v.trim()).filter(Boolean) : undefined,
507
591
  });
508
592
  spinner.stop();
509
593
  // Server returns array of upserted params; extract the first one
@@ -522,6 +606,7 @@ export function registerFunctionCommands(program: Command): void {
522
606
  .option('--type <type>', 'Parameter type')
523
607
  .option('--description <desc>', 'Parameter description')
524
608
  .option('--required', 'Mark as required')
609
+ .option('--allowed-values <list>', 'New fixed choices, comma-separated ("" removes them)')
525
610
  .action(async (agentId: string, funcId: string, paramId: string, options) => {
526
611
  try {
527
612
  const input: Record<string, unknown> = {};
@@ -529,6 +614,7 @@ export function registerFunctionCommands(program: Command): void {
529
614
  if (options.type) input.type = options.type;
530
615
  if (options.description) input.description = options.description;
531
616
  if (options.required !== undefined) input.required = options.required;
617
+ if (options.allowedValues !== undefined) input.allowedValues = String(options.allowedValues).split(',').map((v: string) => v.trim()).filter(Boolean);
532
618
  if (Object.keys(input).length === 0) { printError('No options provided.'); process.exit(1); }
533
619
  const client = new IndemnClient();
534
620
  const sdk = new FunctionsSDK(client);
@@ -544,11 +630,11 @@ export function registerFunctionCommands(program: Command): void {
544
630
  .argument('<agent-id>', 'Agent ID')
545
631
  .argument('<func-id>', 'Function ID')
546
632
  .argument('<param-id>', 'Parameter ID')
547
- .description('Delete a function parameter')
548
- .action(async (agentId: string, funcId: string, paramId: string) => {
633
+ .description('Delete a skill parameter')
634
+ .option('--yes', 'Do not ask for confirmation')
635
+ .action(async (agentId: string, funcId: string, paramId: string, options) => {
549
636
  try {
550
- const confirmed = await confirm(`Delete parameter ${paramId}?`);
551
- if (!confirmed) { console.log('Cancelled.'); return; }
637
+ await confirmOrExit(`Delete parameter ${paramId} of skill ${funcId}?`, Boolean(options.yes));
552
638
  const client = new IndemnClient();
553
639
  const sdk = new FunctionsSDK(client);
554
640
  const spinner = ora('Deleting parameter...').start();
package/src/cli/index.ts CHANGED
@@ -11,16 +11,29 @@ import { registerKBCommands } from './kb.js';
11
11
  import { registerEvalCommands } from './eval.js';
12
12
  import { registerChatCommand } from './chat.js';
13
13
  import { registerOrgCommands } from './org.js';
14
- import { printBanner } from './utils/output.js';
14
+ import { registerLinkCommands } from './link.js';
15
+ import { registerConversationCommands } from './conversations.js';
16
+ import { registerVoiceCommands } from './voice.js';
17
+ import { registerDistributionCommands } from './distribution.js';
18
+ import { registerAnalyticsCommands } from './analytics.js';
19
+ import { registerTicketCommands } from './tickets.js';
20
+ import { registerBillingCommands } from './billing.js';
21
+ import { registerCampaignCommands } from './campaigns.js';
22
+ import { registerPlatformCommands } from './platform.js';
23
+ import { registerInitCommand } from './init.js';
24
+ import { askMfaInTerminal, askOrgInTerminal, installBanner } from './utils/output.js';
25
+ import { setAskers, setOrgFlag } from '../core/scope.js';
26
+ import { cliVersion, isNonInteractive } from '../core/runtime.js';
15
27
 
16
28
  const program = new Command();
17
29
 
18
30
  program
19
31
  .name('indemn')
20
32
  .description('CLI for managing Indemn AI agents')
21
- .version('1.5.0');
33
+ .version(cliVersion());
22
34
 
23
- program.option('--env <environment>', 'Environment (dev or production)');
35
+ program.option('--debug', 'Print every request and response status to stderr');
36
+ program.option('--org <id>', 'Organization to act on (otherwise: linked folder, your only org, or you are asked)');
24
37
  program.option('--no-banner', 'Suppress the active-org banner');
25
38
 
26
39
  // Commands that already surface org identity themselves — no banner needed.
@@ -30,7 +43,10 @@ const BANNER_SKIP = new Set([
30
43
  'whoami',
31
44
  'init',
32
45
  'org list',
46
+ 'org current',
33
47
  'org switch',
48
+ 'link',
49
+ 'unlink',
34
50
  'mcp serve',
35
51
  ]);
36
52
 
@@ -47,8 +63,15 @@ function fullCommandName(cmd: Command): string {
47
63
  // Hook runs before any command action
48
64
  program.hook('preAction', async (thisCommand, actionCommand) => {
49
65
  const globalOpts = thisCommand.opts();
50
- if (globalOpts.env) {
51
- process.env.INDEMN_ENV = globalOpts.env;
66
+ // The environment is fixed by `indemn login --env`; there is no per-command switch.
67
+ if (globalOpts.debug) {
68
+ process.env.INDEMN_DEBUG = '1';
69
+ }
70
+ setOrgFlag(globalOpts.org);
71
+
72
+ // Ask in the terminal only when a person can answer; agents and CI get exit code 2 instead.
73
+ if (fullCommandName(actionCommand) !== 'mcp serve' && !isNonInteractive()) {
74
+ setAskers({ chooseOrg: askOrgInTerminal, mfaCode: askMfaInTerminal });
52
75
  }
53
76
 
54
77
  // Banner suppression: global flag, env var, command opts-out, or JSON output mode.
@@ -58,18 +81,45 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
58
81
  if (actionOpts.json) return;
59
82
  if (BANNER_SKIP.has(fullCommandName(actionCommand))) return;
60
83
 
61
- await printBanner();
84
+ installBanner();
62
85
  });
63
86
 
64
87
  // Register all command groups
65
88
  registerAuthCommands(program);
89
+ // Groups other modules extend (org, agents) are registered first.
90
+ registerOrgCommands(program);
91
+ registerLinkCommands(program);
66
92
  registerAgentCommands(program);
67
93
  registerConfigCommands(program);
68
94
  registerFunctionCommands(program);
69
95
  registerKBCommands(program);
70
96
  registerEvalCommands(program);
71
97
  registerChatCommand(program);
72
- registerOrgCommands(program);
98
+ registerConversationCommands(program);
99
+ registerVoiceCommands(program);
100
+ registerDistributionCommands(program);
101
+ registerAnalyticsCommands(program);
102
+ registerTicketCommands(program);
103
+ registerBillingCommands(program);
104
+ registerCampaignCommands(program);
105
+ registerPlatformCommands(program);
106
+
107
+ program
108
+ .command('channels')
109
+ .description('Channels an agent can be published on')
110
+ .command('list')
111
+ .description('List channels')
112
+ .option('--json', 'Output as JSON')
113
+ .action(async (options) => {
114
+ const { IndemnClient } = await import('../sdk/client.js');
115
+ const { OrgsSDK } = await import('../sdk/orgs.js');
116
+ const { formatTable, printJson, printError } = await import('./utils/output.js');
117
+ try {
118
+ const channels = await new OrgsSDK(new IndemnClient()).listChannels();
119
+ if (options.json) { printJson(channels); return; }
120
+ console.log(formatTable(['ID', 'Name', 'Type', 'Description'], channels.map((c) => [c._id, c.name, c.type ?? '', c.description ?? ''])));
121
+ } catch (err) { printError(err); process.exit(1); }
122
+ });
73
123
 
74
124
  // MCP serve command
75
125
  const mcp = program.command('mcp').description('MCP server commands');
@@ -102,53 +152,6 @@ program
102
152
  } catch (err) { printError(err); process.exit(1); }
103
153
  });
104
154
 
105
- // Init command — set up Claude Code integration
106
- program
107
- .command('init')
108
- .description('Set up Claude Code skills in the current project')
109
- .option('--force', 'Overwrite existing files')
110
- .action(async (options) => {
111
- const __filename = fileURLToPath(import.meta.url);
112
- const __dirname = dirname(__filename);
113
- const packageRoot = resolve(__dirname, '..', '..');
114
- const skillsSource = join(packageRoot, 'skills');
115
- const cwd = process.cwd();
116
- const skillsTarget = join(cwd, '.claude', 'skills', 'indemn');
117
- const rulesTarget = join(cwd, '.claude', 'rules');
118
-
119
- if (existsSync(skillsTarget) && !options.force) {
120
- console.log('Indemn skills already installed. Use --force to overwrite.');
121
- return;
122
- }
123
-
124
- // Copy skills
125
- mkdirSync(skillsTarget, { recursive: true });
126
- cpSync(skillsSource, skillsTarget, { recursive: true });
127
-
128
- // Write rules file
129
- mkdirSync(rulesTarget, { recursive: true });
130
- const rulesPath = join(rulesTarget, 'indemn.md');
131
- if (!existsSync(rulesPath) || options.force) {
132
- writeFileSync(rulesPath, [
133
- '# Indemn CLI',
134
- '',
135
- 'The `indemn` CLI is available for managing AI agents. Run `indemn whoami` to verify authentication.',
136
- '',
137
- '## Skills',
138
- '',
139
- '- **agent-builder** — Create, configure, and test agents (prompts, functions, knowledge bases, chat)',
140
- '- **evaluations** — Create rubrics and test sets, run evaluations, analyze results, export reports',
141
- '',
142
- 'Use these skills for any agent management or evaluation task. They contain the complete command reference.',
143
- '',
144
- ].join('\n'));
145
- }
146
-
147
- console.log('Indemn CLI integration set up:');
148
- console.log(` Skills: ${skillsTarget}/`);
149
- console.log(` Rules: ${rulesPath}`);
150
- console.log('');
151
- console.log('Claude Code will now discover the indemn skills automatically.');
152
- });
155
+ registerInitCommand(program);
153
156
 
154
157
  program.parse();
@@ -0,0 +1,154 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { TOOL_META } from '../mcp/tool-meta.js';
7
+ import { IndemnClient } from '../sdk/client.js';
8
+ import { findLinkFile } from '../core/org-resolver.js';
9
+ import { printError, printSuccess } from './utils/output.js';
10
+
11
+ export const MCP_SERVER_ENTRY = { command: 'npx', args: ['-y', '@indemn/cli', 'mcp', 'serve'] };
12
+
13
+ /** Claude Code permission rules: read-only tools run without asking, anything that changes the platform asks every time. */
14
+ export function permissionBlock(): { allow: string[]; ask: string[] } {
15
+ const allow: string[] = [];
16
+ const ask: string[] = [];
17
+ for (const [name, meta] of Object.entries(TOOL_META)) {
18
+ const rule = `mcp__indemn__${name}`;
19
+ if (meta.annotations.readOnlyHint && !meta.guard) allow.push(rule);
20
+ else ask.push(rule);
21
+ }
22
+ return { allow: allow.sort(), ask: ask.sort() };
23
+ }
24
+
25
+ function readJson(path: string): Record<string, any> {
26
+ if (!existsSync(path)) return {};
27
+ try {
28
+ return JSON.parse(readFileSync(path, 'utf8'));
29
+ } catch {
30
+ throw new Error(`${path} is not valid JSON; fix it or move it away, then run init again.`);
31
+ }
32
+ }
33
+
34
+ const RULES = `# Indemn CLI
35
+
36
+ The \`indemn\` CLI and its MCP server manage Indemn AI agents. Run \`indemn whoami\` to see the platform (dev or production), the user and which org commands here act on.
37
+
38
+ - Every chat with an agent is a test conversation: never billed, never in analytics, shown with the tester's name.
39
+ - Actions that change or delete something are approved by the user first. When an approval form appears, that form is the question: call the tool and do not ask again in chat. When a tool returns a dry_run instead, show it and pass confirm: true only after the user agreed to that exact change.
40
+ - If a tool returns needs_input (for example which org), ask the user and call again with their answer.
41
+ - Talk in names: call agents, knowledge bases, test sets and runs by their names; show ids only when asked or to tell two items apart. Plain words, short answers, details on request.
42
+
43
+ ## Skills
44
+
45
+ - **agent-builder**: create, configure and test agents (prompt, skills, knowledge bases, voice, chat).
46
+ - **evaluations**: rubrics, test sets, runs, CI gates, reports.
47
+ `;
48
+
49
+ export interface InitReport {
50
+ skills: string;
51
+ rules: string;
52
+ mcp: 'added' | 'kept' | 'skipped';
53
+ permissions: 'added' | 'merged' | 'skipped';
54
+ }
55
+
56
+ /** Set up Claude Code in `cwd`. Existing files are merged, never overwritten, unless force is set for skills and rules. */
57
+ export function initProject(cwd: string, opts: { force?: boolean; mcp?: boolean; permissions?: boolean; packageRoot?: string } = {}): InitReport {
58
+ const packageRoot = opts.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
59
+ // Claude Code finds project skills at .claude/skills/<name>/SKILL.md, one folder per skill.
60
+ const skillsTarget = join(cwd, '.claude', 'skills');
61
+ for (const skill of readdirSync(join(packageRoot, 'skills'), { withFileTypes: true }).filter((d) => d.isDirectory())) {
62
+ const target = join(skillsTarget, skill.name);
63
+ if (!existsSync(target) || opts.force) {
64
+ mkdirSync(target, { recursive: true });
65
+ cpSync(join(packageRoot, 'skills', skill.name), target, { recursive: true });
66
+ }
67
+ }
68
+ const rulesPath = join(cwd, '.claude', 'rules', 'indemn.md');
69
+ if (!existsSync(rulesPath) || opts.force) {
70
+ mkdirSync(dirname(rulesPath), { recursive: true });
71
+ writeFileSync(rulesPath, RULES);
72
+ }
73
+
74
+ let mcp: InitReport['mcp'] = 'skipped';
75
+ if (opts.mcp !== false) {
76
+ const path = join(cwd, '.mcp.json');
77
+ const cfg = readJson(path);
78
+ cfg.mcpServers = cfg.mcpServers ?? {};
79
+ if (cfg.mcpServers.indemn) {
80
+ mcp = 'kept';
81
+ } else {
82
+ cfg.mcpServers.indemn = MCP_SERVER_ENTRY;
83
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
84
+ mcp = 'added';
85
+ }
86
+ }
87
+
88
+ let permissions: InitReport['permissions'] = 'skipped';
89
+ if (opts.permissions !== false) {
90
+ const path = join(cwd, '.claude', 'settings.json');
91
+ const cfg = readJson(path);
92
+ const had = Boolean(cfg.permissions);
93
+ cfg.permissions = cfg.permissions ?? {};
94
+ const block = permissionBlock();
95
+ const denied = new Set<string>(cfg.permissions.deny ?? []);
96
+ const askSet = new Set<string>([...(cfg.permissions.ask ?? []), ...block.ask]);
97
+ // A tool the user already restricted is never loosened.
98
+ const allowSet = new Set<string>([...(cfg.permissions.allow ?? []), ...block.allow.filter((r) => !askSet.has(r) && !denied.has(r))]);
99
+ cfg.permissions.allow = [...allowSet].sort();
100
+ cfg.permissions.ask = [...askSet].filter((r) => !denied.has(r)).sort();
101
+ mkdirSync(dirname(path), { recursive: true });
102
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n');
103
+ permissions = had ? 'merged' : 'added';
104
+ }
105
+ return { skills: skillsTarget, rules: rulesPath, mcp, permissions };
106
+ }
107
+
108
+ /** What is set up in `cwd`, for `indemn init --status`. */
109
+ export function initStatus(cwd: string): Array<[string, boolean, string]> {
110
+ const mcp = readJson(join(cwd, '.mcp.json'));
111
+ const settings = readJson(join(cwd, '.claude', 'settings.json'));
112
+ const login = IndemnClient.loadConfig();
113
+ let link: string | null = null;
114
+ try { link = findLinkFile(cwd)?.path ?? null; } catch { link = null; }
115
+ return [
116
+ ['Skills', existsSync(join(cwd, '.claude', 'skills', 'agent-builder', 'SKILL.md')), '.claude/skills/agent-builder, evaluations'],
117
+ ['Rules', existsSync(join(cwd, '.claude', 'rules', 'indemn.md')), '.claude/rules/indemn.md'],
118
+ ['MCP server', Boolean(mcp.mcpServers?.indemn), '.mcp.json'],
119
+ ['Permissions', (settings.permissions?.ask ?? []).some((r: string) => r.startsWith('mcp__indemn__')), '.claude/settings.json'],
120
+ ['Logged in', Boolean(login), login ? `${login.email ?? login.user_id} on ${login.environment}` : 'run `indemn login`'],
121
+ ['Folder linked', Boolean(link), link ?? 'optional: `indemn link --org <id>`'],
122
+ ];
123
+ }
124
+
125
+ export function registerInitCommand(program: Command): void {
126
+ program
127
+ .command('init')
128
+ .description('Set up Claude Code here: skills, rules, the MCP server in .mcp.json, and permissions (reads allowed, changes ask)')
129
+ .option('--force', 'Overwrite the installed skills and rules')
130
+ .option('--no-mcp', 'Do not add the MCP server to .mcp.json')
131
+ .option('--no-permissions', 'Do not add permission rules to .claude/settings.json')
132
+ .option('--status', 'Show what is set up and stop')
133
+ .action((options) => {
134
+ try {
135
+ const cwd = process.cwd();
136
+ if (options.status) {
137
+ for (const [what, ok, detail] of initStatus(cwd)) console.log(`${ok ? chalk.green('✓') : chalk.yellow('·')} ${what.padEnd(14)} ${chalk.dim(detail)}`);
138
+ return;
139
+ }
140
+ const r = initProject(cwd, { force: options.force, mcp: options.mcp, permissions: options.permissions });
141
+ printSuccess('Indemn CLI set up for Claude Code:');
142
+ console.log(` Skills: ${r.skills}`);
143
+ console.log(` Rules: ${r.rules}`);
144
+ console.log(` MCP server: ${r.mcp === 'added' ? 'added to .mcp.json' : r.mcp === 'kept' ? 'already in .mcp.json (kept)' : 'skipped'}`);
145
+ console.log(` Permissions: ${r.permissions === 'skipped' ? 'skipped' : `${r.permissions} in .claude/settings.json (reads allowed, changes ask)`}`);
146
+ const login = IndemnClient.loadConfig();
147
+ if (!login) console.log(chalk.yellow('Next: run `indemn login --env dev` (or production).'));
148
+ else if ((login.orgs?.length ?? 0) > 1 && !findLinkFile(cwd)) console.log(chalk.dim('Tip: pin this folder to one org with `indemn link --org <id>`.'));
149
+ } catch (err) {
150
+ printError(err);
151
+ process.exit(1);
152
+ }
153
+ });
154
+ }