@indemn/cli 1.6.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 +109 -38
  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 +105 -39
  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,7 +1,10 @@
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';
@@ -82,19 +85,19 @@ function buildAuthorization(opts: ConfigSchemaOptions): Record<string, unknown>
82
85
  if (!opts.authKey || !opts.authValue) {
83
86
  throw new Error('--auth-key and --auth-value are required when --auth-type is api-key');
84
87
  }
85
- return { type: 'api-key', key: opts.authKey, value: opts.authValue };
88
+ return { type: 'api-key', key: opts.authKey, value: resolveSecretArg('--auth-value', opts.authValue) };
86
89
  }
87
90
  if (authType === 'basic-auth') {
88
91
  if (!opts.authUsername || !opts.authPassword) {
89
92
  throw new Error('--auth-username and --auth-password are required when --auth-type is basic-auth');
90
93
  }
91
- return { type: 'basic-auth', username: opts.authUsername, password: opts.authPassword };
94
+ return { type: 'basic-auth', username: opts.authUsername, password: resolveSecretArg('--auth-password', opts.authPassword) };
92
95
  }
93
96
  if (authType === 'bearer-token') {
94
97
  if (!opts.authToken) {
95
98
  throw new Error('--auth-token is required when --auth-type is bearer-token');
96
99
  }
97
- return { type: 'bearer-token', token: opts.authToken };
100
+ return { type: 'bearer-token', token: resolveSecretArg('--auth-token', opts.authToken) };
98
101
  }
99
102
  return undefined;
100
103
  }
@@ -159,7 +162,8 @@ function buildConfigurationSchema(opts: ConfigSchemaOptions, type: string): Reco
159
162
  if (type === TRANSFER_CALL_TYPE) {
160
163
  // Server's validateTransferCallPayload requires `handoffNumber` (the destination
161
164
  // phone number for the call transfer). copilot-server/services/botToolsValidationService.js:52
162
- if (opts.handoffNumber !== undefined) result.handoffNumber = opts.handoffNumber;
165
+ // Saved the way the dashboard saves it (phoneNumber + intlTelObj, or sipUri via --config).
166
+ if (opts.handoffNumber !== undefined) result.phoneNumber = opts.handoffNumber;
163
167
  if (opts.outputInstructions !== undefined) result.outputInstructions = opts.outputInstructions;
164
168
  }
165
169
 
@@ -178,16 +182,16 @@ function applyConfigSchemaOptions(cmd: Command): Command {
178
182
  .option('--no-craft-response', 'Return data directly without LLM crafting')
179
183
  .option('--auth-type <type>', 'Auth type: api-key | basic-auth | bearer-token | no-auth')
180
184
  .option('--auth-key <name>', 'Header name for api-key auth (e.g. X-API-Key)')
181
- .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')
182
186
  .option('--auth-username <user>', 'Username for basic-auth')
183
- .option('--auth-password <pass>', 'Password for basic-auth')
184
- .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)')
185
189
  .option('--handoff-number <number>', 'Destination phone number for --type transfer_call (E.164, e.g. +15551234567)')
186
190
  .option('--config <path>', 'JSON file with full configurationSchema (individual flags override file values)');
187
191
  }
188
192
 
189
193
  export function registerFunctionCommands(program: Command): void {
190
- 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)');
191
195
 
192
196
  functions
193
197
  .command('list')
@@ -263,8 +267,8 @@ export function registerFunctionCommands(program: Command): void {
263
267
 
264
268
  // For transfer_call on create, require handoffNumber (server validation rejects without it).
265
269
  if (options.type === TRANSFER_CALL_TYPE) {
266
- if (!configurationSchema || !configurationSchema.handoffNumber) {
267
- throw new Error('--handoff-number (or --config with handoffNumber) is required for --type transfer_call');
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');
268
272
  }
269
273
  }
270
274
 
@@ -288,7 +292,7 @@ export function registerFunctionCommands(program: Command): void {
288
292
  finalFunc = await sdk.updateFunction(agentId, func._id, {
289
293
  type: options.type,
290
294
  description: options.description || '',
291
- configurationSchema,
295
+ configurationSchema: options.type === TRANSFER_CALL_TYPE ? transferDestination({}, configurationSchema) : configurationSchema,
292
296
  });
293
297
  } catch (err) {
294
298
  spinner.fail(`Function created (${func._id}) but configuration step failed.`);
@@ -347,10 +351,10 @@ export function registerFunctionCommands(program: Command): void {
347
351
  // otherwise `update --endpoint X` would wipe everything else.
348
352
  const patch = buildConfigurationSchema(options, current.type);
349
353
  if (patch) {
350
- input.configurationSchema = {
351
- ...(current.configurationSchema || {}),
352
- ...patch,
353
- };
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 };
354
358
  }
355
359
 
356
360
  // Anything actually changing? type+description are always set; require at least one explicit option.
@@ -377,24 +381,20 @@ export function registerFunctionCommands(program: Command): void {
377
381
  functions
378
382
  .command('delete')
379
383
  .argument('<agent-id>', 'Agent ID')
380
- .argument('<func-id>', 'Function ID')
381
- .description('Delete a function')
382
- .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) => {
383
388
  try {
384
- const confirmed = await confirm(`Delete function ${funcId}? This cannot be undone.`);
385
- if (!confirmed) {
386
- console.log('Cancelled.');
387
- return;
388
- }
389
-
390
389
  const client = new IndemnClient();
391
390
  const sdk = new FunctionsSDK(client);
392
- const spinner = ora('Deleting function...').start();
393
-
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();
394
395
  await sdk.deleteFunction(agentId, funcId);
395
396
  spinner.stop();
396
-
397
- printSuccess(`Function ${funcId} deleted.`);
397
+ printSuccess(`Skill ${funcId} deleted.`);
398
398
  } catch (err) {
399
399
  printError(err);
400
400
  process.exit(1);
@@ -466,18 +466,23 @@ export function registerFunctionCommands(program: Command): void {
466
466
  functions
467
467
  .command('import')
468
468
  .argument('<agent-id>', 'Agent ID')
469
- .description('Import functions from a URL')
470
- .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')
471
472
  .action(async (agentId: string, options) => {
472
473
  try {
473
474
  const client = new IndemnClient();
474
475
  const sdk = new FunctionsSDK(client);
475
- const spinner = ora('Importing functions...').start();
476
-
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();
477
483
  const result = await sdk.importFunctions(agentId, options.url);
478
484
  spinner.stop();
479
-
480
- printSuccess('Functions imported successfully.');
485
+ printSuccess('Skills imported.');
481
486
  printJson(result);
482
487
  } catch (err) {
483
488
  printError(err);
@@ -485,6 +490,63 @@ export function registerFunctionCommands(program: Command): void {
485
490
  }
486
491
  });
487
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
+
488
550
  const params = functions.command('params').description('Manage function parameters');
489
551
 
490
552
  params
@@ -516,6 +578,7 @@ export function registerFunctionCommands(program: Command): void {
516
578
  .requiredOption('--type <type>', 'Parameter type (e.g. string, number, boolean)')
517
579
  .option('--description <desc>', 'Parameter description')
518
580
  .option('--required', 'Mark as required')
581
+ .option('--allowed-values <list>', 'Fixed choices, comma-separated (shown as buttons)')
519
582
  .action(async (agentId: string, funcId: string, options) => {
520
583
  try {
521
584
  const client = new IndemnClient();
@@ -524,6 +587,7 @@ export function registerFunctionCommands(program: Command): void {
524
587
  const result = await sdk.addParameter(agentId, funcId, {
525
588
  name: options.name, type: options.type,
526
589
  description: options.description, required: options.required || false,
590
+ allowedValues: options.allowedValues ? String(options.allowedValues).split(',').map((v: string) => v.trim()).filter(Boolean) : undefined,
527
591
  });
528
592
  spinner.stop();
529
593
  // Server returns array of upserted params; extract the first one
@@ -542,6 +606,7 @@ export function registerFunctionCommands(program: Command): void {
542
606
  .option('--type <type>', 'Parameter type')
543
607
  .option('--description <desc>', 'Parameter description')
544
608
  .option('--required', 'Mark as required')
609
+ .option('--allowed-values <list>', 'New fixed choices, comma-separated ("" removes them)')
545
610
  .action(async (agentId: string, funcId: string, paramId: string, options) => {
546
611
  try {
547
612
  const input: Record<string, unknown> = {};
@@ -549,6 +614,7 @@ export function registerFunctionCommands(program: Command): void {
549
614
  if (options.type) input.type = options.type;
550
615
  if (options.description) input.description = options.description;
551
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);
552
618
  if (Object.keys(input).length === 0) { printError('No options provided.'); process.exit(1); }
553
619
  const client = new IndemnClient();
554
620
  const sdk = new FunctionsSDK(client);
@@ -564,11 +630,11 @@ export function registerFunctionCommands(program: Command): void {
564
630
  .argument('<agent-id>', 'Agent ID')
565
631
  .argument('<func-id>', 'Function ID')
566
632
  .argument('<param-id>', 'Parameter ID')
567
- .description('Delete a function parameter')
568
- .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) => {
569
636
  try {
570
- const confirmed = await confirm(`Delete parameter ${paramId}?`);
571
- if (!confirmed) { console.log('Cancelled.'); return; }
637
+ await confirmOrExit(`Delete parameter ${paramId} of skill ${funcId}?`, Boolean(options.yes));
572
638
  const client = new IndemnClient();
573
639
  const sdk = new FunctionsSDK(client);
574
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
+ }