@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
package/src/cli/kb.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { Command } from 'commander';
2
+ import { readFileSync } from 'node:fs';
3
+ import { confirmOrExit } from './link.js';
4
+ import { CliError } from '../core/runtime.js';
5
+ import chalk from 'chalk';
2
6
  import ora from 'ora';
3
7
  import { IndemnClient } from '../sdk/client.js';
4
8
  import { KBSDK } from '../sdk/kb.js';
@@ -138,22 +142,24 @@ export function registerKBCommands(program: Command): void {
138
142
 
139
143
  kb.command('delete')
140
144
  .argument('<kb-id>', 'Knowledge base ID')
141
- .description('Delete a knowledge base')
142
- .action(async (kbId: string) => {
145
+ .option('--dry-run', 'Show what would be deleted and stop')
146
+ .option('--yes', 'Do not ask for confirmation')
147
+ .description('Delete a knowledge base and all its sources (unlink it from agents first)')
148
+ .action(async (kbId: string, options) => {
143
149
  try {
144
- const confirmed = await confirm(`Delete knowledge base ${kbId}? This cannot be undone.`);
145
- if (!confirmed) {
146
- console.log('Cancelled.');
147
- return;
148
- }
149
-
150
150
  const client = new IndemnClient();
151
151
  const sdk = new KBSDK(client);
152
+ const target = await sdk.getKB(kbId) as unknown as { name?: string; totalDataSourceCount?: number; aiAgents?: Array<{ name?: string }> };
153
+ const agents = (target.aiAgents ?? []).map((a) => a.name).filter(Boolean);
154
+ console.log(`Would delete "${target.name ?? kbId}" with ${target.totalDataSourceCount ?? '?'} source(s).`);
155
+ if (agents.length) {
156
+ throw new CliError(`It is linked to ${agents.join(', ')}. Unlink it first: indemn kb unlink <agent-id> ${kbId}`);
157
+ }
158
+ if (options.dryRun) return;
159
+ await confirmOrExit(`Delete knowledge base "${target.name ?? kbId}"? Its vectors are removed too.`, Boolean(options.yes));
152
160
  const spinner = ora('Deleting knowledge base...').start();
153
-
154
161
  await sdk.deleteKB(kbId);
155
162
  spinner.stop();
156
-
157
163
  printSuccess(`Knowledge base ${kbId} deleted.`);
158
164
  } catch (err) {
159
165
  printError(err);
@@ -289,13 +295,10 @@ export function registerKBCommands(program: Command): void {
289
295
  .argument('<kb-id>', 'Knowledge base ID')
290
296
  .argument('<source-id>', 'Data source ID')
291
297
  .description('Delete a data source')
292
- .action(async (kbId: string, sourceId: string) => {
298
+ .option('--yes', 'Do not ask for confirmation')
299
+ .action(async (kbId: string, sourceId: string, options) => {
293
300
  try {
294
- const confirmed = await confirm(`Delete data source ${sourceId}? This cannot be undone.`);
295
- if (!confirmed) {
296
- console.log('Cancelled.');
297
- return;
298
- }
301
+ await confirmOrExit(`Delete data source ${sourceId} from knowledge base ${kbId}? Its vectors are removed too.`, Boolean(options.yes));
299
302
 
300
303
  const client = new IndemnClient();
301
304
  const sdk = new KBSDK(client);
@@ -311,6 +314,102 @@ export function registerKBCommands(program: Command): void {
311
314
  }
312
315
  });
313
316
 
317
+ data.command('chunks')
318
+ .argument('<kb-id>', 'Knowledge base ID')
319
+ .argument('<source-id>', 'Data source ID')
320
+ .option('--json', 'Output as JSON')
321
+ .description('Show the text chunks the agent actually retrieves from a source')
322
+ .action(async (kbId: string, sourceId: string, options) => {
323
+ try {
324
+ const res = await new KBSDK(new IndemnClient()).chunks(kbId, sourceId);
325
+ if (options.json) { printJson(res); return; }
326
+ console.log(chalk.bold(`${res.source ?? sourceId}`) + chalk.dim(` ${res.status ?? ''} · ${res.chunks.length} chunk(s)`));
327
+ res.chunks.forEach((c, i) => console.log(`${chalk.dim(`#${i + 1}`)} ${c}\n`));
328
+ } catch (err) {
329
+ printError(err);
330
+ process.exit(1);
331
+ }
332
+ });
333
+
334
+ data.command('reimport')
335
+ .argument('<kb-id>', 'Knowledge base ID')
336
+ .argument('<source-id>', 'URL data source ID')
337
+ .option('--js-script <file>', 'Script to run on each page before reading it')
338
+ .option('--yes', 'Do not ask for confirmation')
339
+ .description('Crawl a URL source again after the website changed (runs in the background; see `kb status`)')
340
+ .action(async (kbId: string, sourceId: string, options) => {
341
+ try {
342
+ await confirmOrExit(`Re-crawl source ${sourceId}? Its chunks are replaced when the crawl finishes.`, Boolean(options.yes));
343
+ const js = options.jsScript ? readFileSync(options.jsScript, 'utf8') : undefined;
344
+ await new KBSDK(new IndemnClient()).reimport(kbId, sourceId, js);
345
+ printSuccess(`Re-import started. Follow it with: indemn kb status ${kbId}`);
346
+ } catch (err) {
347
+ printError(err);
348
+ process.exit(1);
349
+ }
350
+ });
351
+
352
+ kb.command('status')
353
+ .argument('<kb-id>', 'Knowledge base ID')
354
+ .option('--json', 'Output as JSON')
355
+ .description('Progress of website crawls and file imports')
356
+ .action(async (kbId: string, options) => {
357
+ try {
358
+ const rows = await new KBSDK(new IndemnClient()).scrapeStatus(kbId);
359
+ if (options.json) { printJson(rows); return; }
360
+ if (!rows.length) { console.log('No website or file sources yet.'); return; }
361
+ console.log(formatTable(['Source', 'Status', 'Job', 'Error'], rows.map((r) => [r.source ?? '', r.status ?? '', r.job_status ?? '', r.error ?? ''])));
362
+ } catch (err) {
363
+ printError(err);
364
+ process.exit(1);
365
+ }
366
+ });
367
+
368
+ kb.command('gaps')
369
+ .option('--agent <id>', 'Only questions asked to this agent')
370
+ .option('--kb <id>', 'Only questions for this knowledge base')
371
+ .option('--themes', 'Group into themes instead of listing questions')
372
+ .option('--days <n>', 'With --themes: look back this many days')
373
+ .option('--include-test', 'Include questions from test conversations')
374
+ .option('--page <n>', 'Page, starting at 0', '0')
375
+ .option('--limit <n>', 'Rows per page (at most 100)', '20')
376
+ .option('--json', 'Output as JSON')
377
+ .description('Questions the agents could not answer: what to add to the knowledge base next')
378
+ .action(async (options) => {
379
+ try {
380
+ const sdk = new KBSDK(new IndemnClient());
381
+ if (options.themes) {
382
+ printJson(await sdk.gapClusters({ days: options.days ? Number(options.days) : undefined, page: Number(options.page), limit: Number(options.limit) }));
383
+ return;
384
+ }
385
+ const res = await sdk.gaps({ agentId: options.agent, kbId: options.kb, includeTest: options.includeTest, page: Number(options.page), limit: Number(options.limit) }) as { responses?: Array<Record<string, unknown>>; total?: number };
386
+ if (options.json) { printJson(res); return; }
387
+ const rows = res.responses ?? [];
388
+ if (!rows.length) { console.log('No unanswered questions.'); return; }
389
+ console.log(formatTable(['Question', 'Status', 'Channel'], rows.map((r) => [String(r.user_query ?? '').slice(0, 80), String(r.status ?? ''), String(r.channel ?? '')])));
390
+ console.log(chalk.dim(`${res.total ?? rows.length} in total.`));
391
+ } catch (err) {
392
+ printError(err);
393
+ process.exit(1);
394
+ }
395
+ });
396
+
397
+ kb.command('check')
398
+ .argument('<question>', 'A question a customer might ask')
399
+ .option('--json', 'Output as JSON')
400
+ .description('Could the org\'s knowledge answer this? Shows the best matching chunks')
401
+ .action(async (question: string, options) => {
402
+ try {
403
+ const res = await new KBSDK(new IndemnClient()).check(question);
404
+ if (options.json) { printJson(res); return; }
405
+ console.log(res.answerable ? chalk.green(`Answerable (score ${res.top_score})`) : chalk.yellow(`Not answerable (best score ${res.top_score ?? 0})`));
406
+ for (const src of (res.sources ?? []).slice(0, 3) as Array<{ text?: string; score?: number }>) console.log(chalk.dim(`- (${src.score}) ${String(src.text ?? '').slice(0, 200)}`));
407
+ } catch (err) {
408
+ printError(err);
409
+ process.exit(1);
410
+ }
411
+ });
412
+
314
413
  // Link/unlink commands
315
414
  kb.command('link')
316
415
  .argument('<agent-id>', 'Agent ID')
@@ -336,8 +435,10 @@ export function registerKBCommands(program: Command): void {
336
435
  .argument('<agent-id>', 'Agent ID')
337
436
  .argument('<kb-id>', 'Knowledge base ID')
338
437
  .description('Unlink a knowledge base from an agent')
339
- .action(async (agentId: string, kbId: string) => {
438
+ .option('--yes', 'Do not ask for confirmation')
439
+ .action(async (agentId: string, kbId: string, options) => {
340
440
  try {
441
+ await confirmOrExit(`Unlink knowledge base ${kbId} from agent ${agentId}? The agent stops using it immediately.`, Boolean(options.yes));
341
442
  const client = new IndemnClient();
342
443
  const sdk = new KBSDK(client);
343
444
  const spinner = ora('Unlinking knowledge base...').start();
@@ -376,8 +477,10 @@ export function registerKBCommands(program: Command): void {
376
477
  .argument('<kb-id>', 'Knowledge base ID')
377
478
  .description('Import QNA pairs from a JSON file into a knowledge base')
378
479
  .requiredOption('--file <path>', 'Path to JSON file with array of {question, answer} pairs')
480
+ .option('--yes', 'Do not ask for confirmation')
379
481
  .action(async (kbId: string, options) => {
380
482
  try {
483
+ await confirmOrExit(`Import into knowledge base ${kbId}? The agent starts answering from the new content once processed.`, Boolean(options.yes));
381
484
  const fs = await import('fs');
382
485
  const filePath = options.file as string;
383
486
 
@@ -0,0 +1,117 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import { IndemnClient } from '../sdk/client.js';
7
+ import { runWithOrg } from '../core/scope.js';
8
+ import { CliError, EXIT, isNonInteractive } from '../core/runtime.js';
9
+ import { findLinkFile, LINK_RELATIVE_PATH } from '../core/org-resolver.js';
10
+ import { confirm, printError, printSuccess } from './utils/output.js';
11
+
12
+ export interface LinkResult {
13
+ path: string;
14
+ org_id: string;
15
+ org_name?: string;
16
+ environment: string;
17
+ agents: string[];
18
+ }
19
+
20
+ /**
21
+ * Pin `dir` to one org (and optionally a list of its agents). Every agent is
22
+ * checked against copilot first; nothing is written unless all pass.
23
+ */
24
+ export async function writeLink(opts: { dir: string; orgFlag?: string; agents: string[] }): Promise<LinkResult> {
25
+ const dir = resolve(opts.dir);
26
+ const real = (path: string) => { try { return realpathSync(path); } catch { return resolve(path); } };
27
+ const homes = new Set([real(homedir()), real(process.env.HOME ?? homedir())]);
28
+ if (homes.has(real(dir))) {
29
+ throw new CliError('Refusing to link your home folder: every folder under it would be pinned. Run this inside a project folder.');
30
+ }
31
+ // Relinking may change the org, so the existing link must not decide it.
32
+ const client = new IndemnClient({ ignoreLink: true });
33
+ const org = await client.resolveOrg(opts.orgFlag);
34
+ const memberships = await client.memberships().catch(() => []);
35
+ const orgName = org.name ?? memberships.find((o) => o.id === org.id)?.name;
36
+
37
+ await runWithOrg(org.id, async () => {
38
+ for (const agentId of opts.agents) {
39
+ try {
40
+ await client.copilotGet(`/organization/:org_id/ai-studio/v2/bots/${agentId}`);
41
+ } catch (err) {
42
+ const why = err instanceof Error ? err.message : String(err);
43
+ throw new CliError(`Agent ${agentId} is not reachable in org ${orgName ?? org.id} (${why}). Nothing was linked.`);
44
+ }
45
+ }
46
+ });
47
+
48
+ const path = join(dir, LINK_RELATIVE_PATH);
49
+ const content = {
50
+ org_id: org.id,
51
+ org_name: orgName,
52
+ environment: client.getEnvironment(),
53
+ agents: opts.agents,
54
+ linked_at: new Date().toISOString(),
55
+ };
56
+ mkdirSync(join(dir, '.indemn'), { recursive: true });
57
+ writeFileSync(path, JSON.stringify(content, null, 2) + '\n');
58
+ return { path, org_id: org.id, org_name: orgName, environment: content.environment, agents: opts.agents };
59
+ }
60
+
61
+ export function registerLinkCommands(program: Command): void {
62
+ program
63
+ .command('link')
64
+ .description('Pin this folder to one org (and optionally its agents); commands here then never touch another org')
65
+ .argument('[agent-ids...]', 'Agents allowed from this folder (empty: any agent of the org)')
66
+ .option('--json', 'Output as JSON')
67
+ .action(async (agentIds: string[], options, cmd: Command) => {
68
+ try {
69
+ const result = await writeLink({ dir: process.cwd(), orgFlag: cmd.optsWithGlobals().org, agents: agentIds });
70
+ if (options.json) {
71
+ console.log(JSON.stringify(result, null, 2));
72
+ return;
73
+ }
74
+ printSuccess(`Linked ${process.cwd()} to ${result.org_name ?? result.org_id} on ${result.environment}.`);
75
+ console.log(chalk.dim(` File: ${result.path}`));
76
+ console.log(chalk.dim(` Agents: ${result.agents.length ? result.agents.join(', ') : 'any agent in this org'}`));
77
+ } catch (err) {
78
+ printError(err);
79
+ process.exit(1);
80
+ }
81
+ });
82
+
83
+ program
84
+ .command('unlink')
85
+ .description('Remove the org link from this folder')
86
+ .action(() => {
87
+ try {
88
+ const here = join(process.cwd(), LINK_RELATIVE_PATH);
89
+ if (existsSync(here)) {
90
+ rmSync(here);
91
+ printSuccess(`Removed ${here}.`);
92
+ return;
93
+ }
94
+ const inherited = findLinkFile();
95
+ if (inherited) {
96
+ throw new CliError(`This folder has no link of its own; it inherits ${inherited.path}. Run \`indemn unlink\` in that folder.`);
97
+ }
98
+ console.log('This folder is not linked.');
99
+ } catch (err) {
100
+ printError(err);
101
+ process.exit(1);
102
+ }
103
+ });
104
+ }
105
+
106
+ /** Ask before pinning; non-interactive callers must pass --yes. */
107
+ export async function confirmOrExit(question: string, yes: boolean): Promise<void> {
108
+ if (yes) return;
109
+ if (isNonInteractive()) {
110
+ printError(new CliError(`${question} Re-run with --yes to confirm.`, EXIT.NEEDS_INPUT));
111
+ process.exit(EXIT.NEEDS_INPUT);
112
+ }
113
+ if (!(await confirm(question))) {
114
+ console.log('Cancelled.');
115
+ process.exit(0);
116
+ }
117
+ }
package/src/cli/org.ts CHANGED
@@ -2,75 +2,69 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { IndemnClient } from '../sdk/client.js';
5
- import { printError, printJson } from './utils/output.js';
5
+ import { OrgsSDK } from '../sdk/orgs.js';
6
+ import { getAskers, setAskers } from '../core/scope.js';
7
+ import type { OrgContext } from '../sdk/client.js';
8
+ import { confirmOrExit, writeLink } from './link.js';
9
+ import { formatTable, printError, printJson, printSuccess } from './utils/output.js';
10
+
11
+ const HOW: Record<OrgContext['source'], string> = {
12
+ flag: 'from --org',
13
+ env: 'from INDEMN_ORG_ID',
14
+ link: 'linked folder',
15
+ single: 'your only org',
16
+ asked: 'chosen',
17
+ };
6
18
 
7
19
  export function registerOrgCommands(program: Command): void {
8
- const org = program.command('org').description('Manage organizations');
20
+ const org = program.command('org').description('Organizations you belong to');
9
21
 
10
22
  org
11
23
  .command('list')
12
- .description('List organizations you have access to')
24
+ .description('List organizations you belong to')
13
25
  .option('--json', 'Output as JSON')
14
26
  .action(async (options) => {
15
27
  try {
16
28
  const client = new IndemnClient();
17
29
  const spinner = ora('Fetching organizations...').start();
18
-
19
- // Get all project memberships
20
- const res = await fetch(client.getHosts().copilot + '/projects', {
21
- headers: {
22
- 'Authorization': `Bearer ${client.getApiKey()}`,
23
- 'Content-Type': 'application/json',
24
- },
25
- });
26
- if (!res.ok) throw new Error(`Failed to fetch projects (${res.status})`);
27
- const memberships = await res.json() as Array<{ id_organization?: string }>;
28
-
29
- // Get unique org IDs
30
- const orgIds = [...new Set(memberships.map(m => m.id_organization).filter(Boolean))] as string[];
31
-
32
- // Fetch org details
33
- const orgs: Array<{ _id: string; name: string; active: boolean }> = [];
34
- for (const orgId of orgIds) {
35
- try {
36
- const orgRes = await fetch(client.getHosts().copilot + '/organization/' + orgId, {
37
- headers: {
38
- 'Authorization': `Bearer ${client.getApiKey()}`,
39
- 'Content-Type': 'application/json',
40
- },
41
- });
42
- if (orgRes.ok) {
43
- const data = await orgRes.json() as { organization: { _id: string; name: string; active: boolean } };
44
- orgs.push(data.organization);
45
- }
46
- } catch {
47
- // Skip orgs we can't fetch
48
- }
49
- }
50
-
30
+ const orgs = await new OrgsSDK(client).listOrganizations();
51
31
  spinner.stop();
32
+ const linked = client.getLink()?.org_id;
52
33
 
53
34
  if (options.json) {
54
- printJson(orgs.map(o => ({ id: o._id, name: o.name, active: o.active })));
35
+ printJson(orgs.map((o) => ({ ...o, linked_here: o.id === linked })));
55
36
  return;
56
37
  }
57
-
58
- const currentOrgId = client.getOrgId();
59
-
60
- // Opportunistically sync the cached org_name while we have fresh data.
61
- const activeOrg = orgs.find(o => o._id === currentOrgId);
62
- if (activeOrg) {
63
- IndemnClient.saveConfig({ org_name: activeOrg.name });
38
+ if (orgs.length === 0) {
39
+ console.log('You are not a member of any organization on this platform.');
40
+ return;
64
41
  }
42
+ const rows = orgs
43
+ .sort((a, b) => a.name.localeCompare(b.name))
44
+ .map((o) => [o.id, o.name + (o.id === linked ? chalk.green(' (linked here)') : '')]);
45
+ console.log(formatTable(['ID', 'Name'], rows));
46
+ console.log(chalk.dim(`${orgs.length} organizations on ${client.getEnvironment()}. Pick one per command with --org <id>, or pin a folder with \`indemn link\`.`));
47
+ } catch (err) {
48
+ printError(err);
49
+ process.exit(1);
50
+ }
51
+ });
65
52
 
66
- console.log(chalk.bold('Organizations'));
67
- console.log('');
68
- for (const o of orgs.sort((a, b) => a.name.localeCompare(b.name))) {
69
- const marker = o._id === currentOrgId ? chalk.green(' (active)') : '';
70
- console.log(` ${o._id} ${o.name}${marker}`);
53
+ org
54
+ .command('current')
55
+ .description('Show which org commands in this folder act on, and why (never asks)')
56
+ .option('--json', 'Output as JSON')
57
+ .action(async (options) => {
58
+ try {
59
+ setAskers({ ...getAskers(), chooseOrg: undefined });
60
+ const client = new IndemnClient();
61
+ const resolved = await client.resolveOrg();
62
+ const out = { id: resolved.id, name: resolved.name, source: resolved.source, link: resolved.link?.path, environment: client.getEnvironment() };
63
+ if (options.json) {
64
+ printJson(out);
65
+ return;
71
66
  }
72
- console.log('');
73
- console.log(chalk.dim(`${orgs.length} organizations. Switch with: indemn org switch <id>`));
67
+ console.log(`${out.name ?? '(unnamed org)'} ${chalk.dim(out.id)} ${chalk.dim(`${HOW[resolved.source]}${out.link ? `: ${out.link}` : ''} · ${out.environment}`)}`);
74
68
  } catch (err) {
75
69
  printError(err);
76
70
  process.exit(1);
@@ -78,34 +72,46 @@ export function registerOrgCommands(program: Command): void {
78
72
  });
79
73
 
80
74
  org
81
- .command('switch')
82
- .description('Switch active organization')
83
- .argument('<org-id>', 'Organization ID to switch to')
84
- .action(async (orgId: string) => {
75
+ .command('members')
76
+ .description('List owners, admins and agents of the org')
77
+ .option('--json', 'Output as JSON')
78
+ .action(async (options) => {
85
79
  try {
86
80
  const client = new IndemnClient();
87
- const spinner = ora('Switching organization...').start();
88
-
89
- // Verify the user has access to this org
90
- const orgRes = await fetch(client.getHosts().copilot + '/organization/' + orgId, {
91
- headers: {
92
- 'Authorization': `Bearer ${client.getApiKey()}`,
93
- 'Content-Type': 'application/json',
94
- },
95
- });
96
-
97
- if (!orgRes.ok) {
98
- spinner.fail('Organization not found or you don\'t have access.');
99
- process.exit(1);
81
+ const members = await new OrgsSDK(client).listMembers();
82
+ if (options.json) {
83
+ printJson(members);
84
+ return;
100
85
  }
86
+ if (members.length === 0) {
87
+ console.log('No owners, admins or agents found in this org.');
88
+ return;
89
+ }
90
+ console.log(formatTable(
91
+ ['Email', 'Name', 'Role', 'Status', 'Projects'],
92
+ members.map((m) => [m.email ?? '', m.name ?? '', m.role ?? '', m.status ?? '', m.projects.join(', ')]),
93
+ ));
94
+ } catch (err) {
95
+ printError(err);
96
+ process.exit(1);
97
+ }
98
+ });
101
99
 
102
- const data = await orgRes.json() as { organization: { _id: string; name: string } };
103
- const orgName = data.organization.name;
104
-
105
- // Update config — cache both org_id and org_name so the banner can render without a lookup.
106
- IndemnClient.saveConfig({ org_id: orgId, org_name: orgName });
107
-
108
- spinner.succeed(`Switched to ${orgName} (${orgId})`);
100
+ org
101
+ .command('switch')
102
+ .description('Pin this folder to an org (there is no global default org any more)')
103
+ .argument('<org-id>', 'Organization ID')
104
+ .option('--yes', 'Do not ask for confirmation')
105
+ .action(async (orgId: string, options) => {
106
+ try {
107
+ const client = new IndemnClient({ ignoreLink: true });
108
+ const orgs = await client.memberships();
109
+ const target = orgs.find((o) => o.id === orgId);
110
+ if (!target) throw new Error(`You are not a member of ${orgId} on ${client.getEnvironment()}. See \`indemn org list\`.`);
111
+ console.log(chalk.dim('The CLI no longer keeps one default org. Commands use --org, the folder link, or ask.'));
112
+ await confirmOrExit(`Pin ${process.cwd()} to ${target.name}?`, Boolean(options.yes));
113
+ const result = await writeLink({ dir: process.cwd(), orgFlag: orgId, agents: [] });
114
+ printSuccess(`Linked ${process.cwd()} to ${result.org_name ?? result.org_id}. File: ${result.path}`);
109
115
  } catch (err) {
110
116
  printError(err);
111
117
  process.exit(1);
@@ -0,0 +1,128 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { readFileSync } from 'node:fs';
4
+ import { IndemnClient } from '../sdk/client.js';
5
+ import { PlatformSDK, type PlatformResource } from '../sdk/platform.js';
6
+ import { jarvisChat } from '../sdk/jarvis-chat.js';
7
+ import { CliError, EXIT } from '../core/runtime.js';
8
+ import { confirmOrExit } from './link.js';
9
+ import { printError, printJson, printSuccess } from './utils/output.js';
10
+
11
+ function run(fn: () => Promise<void>): Promise<void> {
12
+ return fn().catch((err) => {
13
+ printError(err);
14
+ process.exit(1);
15
+ });
16
+ }
17
+ const sdk = () => new PlatformSDK(new IndemnClient());
18
+ const readJson = (path: string) => JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
19
+
20
+ const RESOURCES: Array<{ cmd: string; resource: PlatformResource; writable: boolean; what: string }> = [
21
+ { cmd: 'agents', resource: 'agents', writable: true, what: 'V2 agents built from templates' },
22
+ { cmd: 'templates', resource: 'templates', writable: true, what: 'V2 agent templates (graphs)' },
23
+ { cmd: 'components', resource: 'components', writable: false, what: 'building blocks of templates (shared catalog, read-only)' },
24
+ { cmd: 'connectors', resource: 'connectors', writable: false, what: 'actions agents can call (shared catalog, read-only)' },
25
+ { cmd: 'bindings', resource: 'connector-bindings', writable: true, what: 'connector credentials and settings of the org' },
26
+ { cmd: 'scenarios', resource: 'scenarios', writable: true, what: 'scripted conversations for simulations' },
27
+ ];
28
+
29
+ export function registerPlatformCommands(program: Command): void {
30
+ const platform = program.command('platform').description('The V2 platform (Darwin): V2 agents, templates, connectors, scenarios, simulations');
31
+
32
+ for (const r of RESOURCES) {
33
+ const g = platform.command(r.cmd).description(r.what);
34
+ g.command('list').option('--agent <id>', 'Only for this agent (scenarios)').action((options) => run(async () => {
35
+ printJson(await sdk().list(r.resource, options.agent ? { agent_id: options.agent } : undefined));
36
+ }));
37
+ g.command('get').argument('<id>').action((id: string) => run(async () => printJson(await sdk().get(r.resource, id))));
38
+ if (!r.writable) continue;
39
+ g.command('create').requiredOption('--file <path>', 'JSON body').option('--yes', 'Do not ask for confirmation').action((options) => run(async () => {
40
+ const body = readJson(options.file);
41
+ await confirmOrExit(`Create ${r.cmd.replace(/s$/, '')} "${String(body.name ?? body.connector_id ?? '')}" on the V2 platform?`, Boolean(options.yes));
42
+ printJson(await sdk().create(r.resource, body));
43
+ }));
44
+ g.command('update').argument('<id>').requiredOption('--file <path>', 'JSON with the fields to change').option('--yes', 'Do not ask for confirmation').action((id: string, options) => run(async () => {
45
+ const body = readJson(options.file);
46
+ await confirmOrExit(`Change ${r.cmd.replace(/s$/, '')} ${id} (${Object.keys(body).join(', ')})?`, Boolean(options.yes));
47
+ printJson(await sdk().update(r.resource, id, body));
48
+ }));
49
+ g.command('delete').argument('<id>').option('--yes', 'Do not ask for confirmation').action((id: string, options) => run(async () => {
50
+ await confirmOrExit(`Delete ${r.cmd.replace(/s$/, '')} ${id} from the V2 platform?`, Boolean(options.yes));
51
+ await sdk().remove(r.resource, id);
52
+ printSuccess(`Deleted ${id}.`);
53
+ }));
54
+ }
55
+
56
+ platform.commands.find((c) => c.name() === 'templates')!.command('validate').argument('<id>').description('Check the template graph').action((id: string) => run(async () => {
57
+ const res = await sdk().validateTemplate(id);
58
+ printJson(res);
59
+ if (!res.valid) process.exit(1);
60
+ }));
61
+ platform.commands.find((c) => c.name() === 'connectors')!.command('test').argument('<id>').requiredOption('--input <file>', 'JSON input')
62
+ .description('Validate input against the connector and preview the request (does not call it)')
63
+ .action((id: string, options) => run(async () => printJson(await sdk().testConnector(id, readJson(options.input)))));
64
+
65
+ const sims = platform.command('simulations').description('Run scenarios against a V2 agent');
66
+ sims.command('run').requiredOption('--agent <id>', 'V2 agent').option('--scenarios <ids>', 'Comma-separated scenario ids (default: all of the agent)')
67
+ .option('--wait', 'Wait for the result').option('--yes', 'Do not ask for confirmation')
68
+ .action((options) => run(async () => {
69
+ await confirmOrExit(`Run simulations for agent ${options.agent}? They use paid model calls.`, Boolean(options.yes));
70
+ const s = sdk();
71
+ const job = await s.runSimulation(options.agent, options.scenarios ? options.scenarios.split(',') : []) as { job_id: string };
72
+ if (!options.wait) { printJson(job); return; }
73
+ for (;;) {
74
+ const j = await s.simulationJob(job.job_id) as { status: string; result_id?: string };
75
+ if (j.status === 'completed' || j.status === 'failed') {
76
+ printJson(j.result_id ? await s.simulationResults({ resultId: j.result_id }) : j);
77
+ if (j.status === 'failed') process.exit(1);
78
+ return;
79
+ }
80
+ await new Promise((r) => setTimeout(r, 5_000));
81
+ }
82
+ }));
83
+ sims.command('jobs').option('--agent <id>').option('--status <status>').action((options) => run(async () => printJson(await sdk().simulationJobs({ agentId: options.agent, status: options.status }))));
84
+ sims.command('results').option('--agent <id>').option('--id <result-id>').option('--limit <n>', 'At most this many', '10')
85
+ .action((options) => run(async () => printJson(await sdk().simulationResults({ agentId: options.agent, resultId: options.id, limit: Number(options.limit) }))));
86
+
87
+ platform.command('org-evaluations').description('Which agents have a baseline evaluation, and which are stale').action(() => run(async () => printJson(await sdk().orgEvaluations())));
88
+
89
+ const jarvis = program.command('jarvis').description('Jarvis, the agent-building assistant of the V2 platform');
90
+ const jobs = jarvis.command('jobs').description('Background Jarvis jobs');
91
+ jobs.command('create')
92
+ .requiredOption('--bots <ids>', 'Comma-separated agent ids (1 to 50) to generate baseline evaluations for')
93
+ .option('--wait', 'Wait until the job completes')
94
+ .option('--yes', 'Do not ask for confirmation')
95
+ .action((options) => run(async () => {
96
+ const ids = String(options.bots).split(',').map((s: string) => s.trim()).filter(Boolean);
97
+ await confirmOrExit(`Ask Jarvis to generate baseline evaluations (rubric and test set) for ${ids.length} agent(s)? It uses paid model calls.`, Boolean(options.yes));
98
+ const s = sdk();
99
+ const job = await s.createJarvisJob(ids);
100
+ if (!options.wait) { printJson(job); return; }
101
+ const done = await s.waitForJarvisJob(job.job_id, { onProgress: (j) => process.stderr.write(chalk.dim(` ${j.status} ${j.progress?.completed ?? 0}/${j.progress?.total ?? ids.length}\n`)) });
102
+ printJson(done);
103
+ if ((done.progress?.results ?? []).some((r) => !r.success)) process.exit(1);
104
+ }));
105
+ jobs.command('list').option('--status <status>').action((options) => run(async () => printJson(await sdk().jarvisJobs(options.status))));
106
+ jobs.command('get').argument('<job-id>').action((id: string) => run(async () => printJson(await sdk().jarvisJob(id))));
107
+
108
+ jarvis.command('chat')
109
+ .argument('<message>', 'What to ask Jarvis')
110
+ .option('--conversation <id>', 'Continue a conversation (id printed after each reply)')
111
+ .option('--agent <v1-agent-id>', 'The V1 agent this is about (as when Jarvis is opened from its page)')
112
+ .option('--yes', 'Do not ask for confirmation')
113
+ .option('--json', 'Print { conversation_id, reply, tools } as JSON')
114
+ .description('One turn with Jarvis. Jarvis can create and change templates, agents and connectors, so this asks first')
115
+ .action((message: string, options) => run(async () => {
116
+ if (!message.trim()) throw new CliError('Give a message.', EXIT.NEEDS_INPUT);
117
+ await confirmOrExit('Send this to Jarvis? It may create or change templates, agents or connectors in the org.', Boolean(options.yes));
118
+ const res = await jarvisChat(new IndemnClient(), message, {
119
+ conversationId: options.conversation,
120
+ context: options.agent ? { federation_bot_id: options.agent } : undefined,
121
+ onText: options.json ? undefined : (t) => process.stdout.write(t),
122
+ onTool: options.json ? undefined : (t) => process.stderr.write(chalk.dim(`\n [${t.name}: ${t.status}]\n`)),
123
+ });
124
+ if (options.json) { printJson(res); return; }
125
+ process.stdout.write('\n');
126
+ console.log(chalk.dim(`Conversation ${res.conversation_id}. Continue with --conversation ${res.conversation_id}.`));
127
+ }));
128
+ }