@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
package/dist/cli/eval.js CHANGED
@@ -2,45 +2,60 @@ import * as fs from 'fs';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { IndemnClient } from '../sdk/client.js';
5
- import { EvalSDK } from '../sdk/eval.js';
5
+ import { EvalSDK, gateVerdict } from '../sdk/eval.js';
6
+ import { CliError, EXIT } from '../core/runtime.js';
7
+ import { confirmOrExit } from './link.js';
6
8
  import { RubricsSDK } from '../sdk/rubrics.js';
7
9
  import { TestSetsSDK } from '../sdk/test-sets.js';
8
10
  import { exportEvalMarkdown, exportEvalReport } from '../report/export.js';
9
- import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js';
11
+ import { formatTable, printJson, printError, printSuccess } from './utils/output.js';
10
12
  export function registerEvalCommands(program) {
11
13
  const eval_ = program.command('eval').description('Run and manage evaluations');
12
14
  eval_
13
15
  .command('run')
14
16
  .argument('<agent-id>', 'Agent ID')
15
- .description('Run an evaluation')
16
- .requiredOption('--test-set <id>', 'Test set ID')
17
+ .description('Run an evaluation (costs model calls; asks first). With a threshold it becomes a CI gate')
18
+ .requiredOption('--test-set <id>', 'Test set ID (must belong to this agent)')
17
19
  .option('--rubric <id>', 'Rubric ID')
18
20
  .option('--concurrency <n>', 'Concurrency level')
19
21
  .option('--wait', 'Wait for evaluation to complete')
22
+ .option('--timeout <seconds>', 'With --wait: give up after this long', '1800')
20
23
  .option('--eval-model <model>', 'Model for evaluation judging')
21
- .option('--keep', 'Keep evaluation data after completion')
22
24
  .option('--limit <n>', 'Limit number of items to evaluate')
23
25
  .option('--component-scope <scope>', 'Component scope filter')
24
26
  .option('--component-ids <ids>', 'Comma-separated component IDs')
25
- .option('--conversation-ids <ids>', 'Comma-separated conversation IDs')
26
- .option('--mode <mode>', 'Evaluation mode (e.g. "transcript")')
27
+ .option('--conversation-ids <ids>', 'Comma-separated conversation IDs (transcript mode)')
28
+ .option('--date-from <date>', 'Grade real conversations from this date (transcript mode)')
29
+ .option('--date-to <date>', 'Up to this date, exclusive (needs --date-from)')
30
+ .option('--mode <mode>', 'Evaluation mode: "transcript" grades real conversations')
31
+ .option('--webhook-url <url>', 'Called when the run finishes')
32
+ .option('--min-pass-rate <rate>', 'CI gate: exit 1 when passed/total is below this (0..1). Implies --wait')
33
+ .option('--max-p95-ms <ms>', 'CI gate: exit 1 when p95 latency is above this. Implies --wait')
34
+ .option('--error-policy <policy>', 'CI gate: block (a failed run fails the gate) or warn', 'block')
35
+ .option('--fail-on-error', 'CI gate: also exit 1 when any case had an infrastructure error. Implies --wait')
36
+ .option('--allow-other-agent', 'Run a test set written for another agent')
37
+ .option('--yes', 'Do not ask for confirmation')
38
+ .option('--json', 'Print the final run as JSON')
27
39
  .action(async (agentId, options) => {
28
40
  try {
29
41
  const client = new IndemnClient();
30
42
  const sdk = new EvalSDK(client);
31
- const spinner = ora('Triggering evaluation...').start();
32
- const input = {
33
- bot_id: agentId,
34
- test_set_id: options.testSet,
35
- };
43
+ await sdk.assertAgentInOrg(agentId);
44
+ const testSet = await new TestSetsSDK(client).getTestSet(options.testSet);
45
+ const owner = testSet.agent_id;
46
+ if (owner && owner !== agentId && !options.allowOtherAgent) {
47
+ throw new CliError(`Test set ${options.testSet} was written for agent ${owner}, not ${agentId}. Pass --allow-other-agent to run it anyway.`, EXIT.NEEDS_INPUT);
48
+ }
49
+ const input = { bot_id: agentId, test_set_id: options.testSet };
36
50
  if (options.rubric)
37
51
  input.rubric_id = options.rubric;
38
52
  if (options.concurrency)
39
53
  input.concurrency = parseInt(options.concurrency, 10);
54
+ // Voice simulations share call rooms; the dashboard runs them one at a time.
55
+ if ((testSet.items ?? []).some((i) => i.type === 'voice_simulation'))
56
+ input.concurrency = 1;
40
57
  if (options.evalModel)
41
58
  input.eval_model = options.evalModel;
42
- if (options.keep !== undefined)
43
- input.keep = options.keep;
44
59
  if (options.limit)
45
60
  input.limit = parseInt(options.limit, 10);
46
61
  if (options.componentScope)
@@ -49,33 +64,67 @@ export function registerEvalCommands(program) {
49
64
  input.component_ids = options.componentIds.split(',');
50
65
  if (options.conversationIds)
51
66
  input.conversation_ids = options.conversationIds.split(',');
52
- if (options.mode)
67
+ if (options.webhookUrl)
68
+ input.webhook_url = options.webhookUrl;
69
+ if (Boolean(options.dateFrom) !== Boolean(options.dateTo)) {
70
+ throw new CliError('--date-from and --date-to go together.', EXIT.NEEDS_INPUT);
71
+ }
72
+ if (options.dateFrom) {
73
+ input.date_from = options.dateFrom;
74
+ input.date_to = options.dateTo;
75
+ if (options.mode && options.mode !== 'transcript')
76
+ throw new CliError('A date range only works with --mode transcript.', EXIT.NEEDS_INPUT);
77
+ // The service ignores dates outside transcript mode and would run the test set instead.
78
+ input.mode = 'transcript';
79
+ }
80
+ else if (options.mode) {
53
81
  input.mode = options.mode;
82
+ }
83
+ const items = testSet.items?.length ?? 0;
84
+ const count = input.mode === 'transcript' ? 'real conversations' : `${input.limit ? Math.min(input.limit, items) : items} item(s)`;
85
+ await confirmOrExit(`Run an evaluation of agent ${agentId} on ${count} with concurrency ${input.concurrency ?? 1}? It uses paid model calls and cannot be cancelled.`, Boolean(options.yes));
86
+ const gate = {
87
+ minPassRate: options.minPassRate !== undefined ? Number(options.minPassRate) : undefined,
88
+ maxP95Ms: options.maxP95Ms !== undefined ? Number(options.maxP95Ms) : undefined,
89
+ errorPolicy: options.errorPolicy === 'warn' ? 'warn' : 'block',
90
+ failOnError: Boolean(options.failOnError),
91
+ };
92
+ const gating = gate.minPassRate !== undefined || gate.maxP95Ms !== undefined || gate.failOnError;
93
+ const spinner = ora('Triggering evaluation...').start();
54
94
  const result = await sdk.triggerEvaluation(input);
55
95
  spinner.stop();
56
- printSuccess(`Evaluation started: ${result.run_id}`);
57
- console.log(` Status: ${result.status}`);
58
- if (result.total)
59
- console.log(` Total items: ${result.total}`);
60
- if (options.wait) {
61
- const pollSpinner = ora('Waiting for evaluation to complete...').start();
62
- try {
63
- const run = await sdk.pollRunUntilComplete(result.run_id);
64
- pollSpinner.stop();
65
- printSuccess(`Evaluation completed: ${run.status}`);
66
- if (run.passed !== undefined)
67
- console.log(` Passed: ${run.passed}`);
68
- if (run.failed !== undefined)
69
- console.log(` Failed: ${run.failed}`);
70
- if (run.criteria_passed !== undefined && run.criteria_total !== undefined) {
71
- console.log(` Criteria: ${run.criteria_passed}/${run.criteria_total}`);
72
- }
73
- }
74
- catch (err) {
75
- pollSpinner.fail('Evaluation did not complete in time');
76
- throw err;
77
- }
96
+ if (!options.json) {
97
+ printSuccess(`Evaluation started: ${result.run_id}`);
98
+ if (result.total)
99
+ console.log(` Total items: ${result.total}`);
78
100
  }
101
+ if (!options.wait && !gating)
102
+ return;
103
+ const pollSpinner = options.json ? null : ora('Waiting for evaluation to complete...').start();
104
+ const run = await sdk.pollRunUntilComplete(result.run_id, 10_000, Number(options.timeout) * 1000).catch((err) => {
105
+ pollSpinner?.fail('Evaluation did not complete in time');
106
+ throw err;
107
+ });
108
+ pollSpinner?.stop();
109
+ const verdict = gateVerdict(run, gate);
110
+ if (options.json) {
111
+ printJson({ run, gate: gating ? verdict : undefined });
112
+ }
113
+ else {
114
+ printSuccess(`Evaluation ${run.status}: ${run.passed ?? 0}/${run.total ?? 0} passed (${(verdict.passRate * 100).toFixed(1)}%)`);
115
+ if (run.errors)
116
+ console.log(chalk.yellow(` Infrastructure errors: ${run.errors}`));
117
+ if (run.latency?.p95_ms !== undefined)
118
+ console.log(` p95 latency: ${run.latency.p95_ms} ms`);
119
+ if (run.criteria_passed !== undefined && run.criteria_total !== undefined)
120
+ console.log(` Criteria: ${run.criteria_passed}/${run.criteria_total}`);
121
+ }
122
+ if (gating && !verdict.pass) {
123
+ console.error(chalk.red(`Gate failed: ${verdict.reasons.join('; ')}`));
124
+ process.exit(1);
125
+ }
126
+ if (gating && !options.json)
127
+ printSuccess('Gate passed.');
79
128
  }
80
129
  catch (err) {
81
130
  printError(err);
@@ -84,8 +133,8 @@ export function registerEvalCommands(program) {
84
133
  });
85
134
  eval_
86
135
  .command('list')
87
- .description('List evaluation runs')
88
- .option('--agent-id <id>', 'Filter by agent ID')
136
+ .description('List evaluation runs of an agent')
137
+ .requiredOption('--agent-id <id>', 'Agent ID (runs of other orgs are never listed)')
89
138
  .option('--json', 'Output as JSON')
90
139
  .option('--limit <n>', 'Limit number of results')
91
140
  .option('--page <n>', 'Page number (use with --limit)')
@@ -94,6 +143,7 @@ export function registerEvalCommands(program) {
94
143
  const client = new IndemnClient();
95
144
  const sdk = new EvalSDK(client);
96
145
  const spinner = ora('Fetching evaluation runs...').start();
146
+ await sdk.assertAgentInOrg(options.agentId);
97
147
  let runs = await sdk.listRuns(options.agentId);
98
148
  spinner.stop();
99
149
  if (options.page && options.limit) {
@@ -137,7 +187,7 @@ export function registerEvalCommands(program) {
137
187
  const client = new IndemnClient();
138
188
  const sdk = new EvalSDK(client);
139
189
  const spinner = ora('Fetching run status...').start();
140
- const run = await sdk.getRunStatus(runId);
190
+ const run = await sdk.getRunInOrg(runId);
141
191
  spinner.stop();
142
192
  if (options.json) {
143
193
  printJson(run);
@@ -181,6 +231,7 @@ export function registerEvalCommands(program) {
181
231
  const client = new IndemnClient();
182
232
  const sdk = new EvalSDK(client);
183
233
  const spinner = ora('Fetching results...').start();
234
+ await sdk.getRunInOrg(runId);
184
235
  const results = await sdk.getRunResults(runId);
185
236
  spinner.stop();
186
237
  if (options.json) {
@@ -209,6 +260,7 @@ export function registerEvalCommands(program) {
209
260
  const client = new IndemnClient();
210
261
  const sdk = new EvalSDK(client);
211
262
  const spinner = ora('Fetching bot context...').start();
263
+ await sdk.assertAgentInOrg(agentId);
212
264
  const context = await sdk.getBotContext(agentId);
213
265
  spinner.stop();
214
266
  if (options.json) {
@@ -248,6 +300,7 @@ export function registerEvalCommands(program) {
248
300
  try {
249
301
  const client = new IndemnClient();
250
302
  const spinner = ora('Fetching evaluation data...').start();
303
+ await new EvalSDK(client).getRunInOrg(runId);
251
304
  const result = await exportEvalMarkdown(runId, options.output, client);
252
305
  spinner.stop();
253
306
  printSuccess(`Exported to ${result.file_path}`);
@@ -260,13 +313,33 @@ export function registerEvalCommands(program) {
260
313
  eval_
261
314
  .command('report')
262
315
  .argument('<run-id>', 'Evaluation run ID')
263
- .description('Generate branded evaluation report PDF')
316
+ .description('Branded evaluation report PDF (made here, or by the evaluations service with --server)')
264
317
  .option('--output <path>', 'Output file path')
318
+ .option('--server', 'Ask the evaluations service to render it (completed runs only)')
319
+ .option('--force', 'With --server: render again instead of reusing the last report')
265
320
  .action(async (runId, options) => {
266
321
  try {
267
322
  const client = new IndemnClient();
268
- const spinner = ora('Fetching evaluation data...').start();
269
- spinner.text = 'Generating PDF...';
323
+ const sdk = new EvalSDK(client);
324
+ await sdk.getRunInOrg(runId);
325
+ if (options.server) {
326
+ const spinner = ora('Rendering on the evaluations service...').start();
327
+ const { download_url, report_id } = await sdk.exportReport(runId, Boolean(options.force));
328
+ spinner.stop();
329
+ if (options.output) {
330
+ const res = await fetch(download_url);
331
+ if (!res.ok)
332
+ throw new Error(`Download failed (${res.status}).`);
333
+ fs.writeFileSync(options.output, Buffer.from(await res.arrayBuffer()));
334
+ printSuccess(`Report saved to ${options.output}`);
335
+ }
336
+ else {
337
+ console.log(download_url);
338
+ console.log(chalk.dim(`Link valid for 15 minutes. New link later: indemn eval report-link ${report_id}`));
339
+ }
340
+ return;
341
+ }
342
+ const spinner = ora('Generating PDF...').start();
270
343
  const result = await exportEvalReport(runId, options.output, client);
271
344
  spinner.stop();
272
345
  printSuccess(`Report saved to ${result.file_path}`);
@@ -276,12 +349,198 @@ export function registerEvalCommands(program) {
276
349
  process.exit(1);
277
350
  }
278
351
  });
352
+ eval_
353
+ .command('report-link')
354
+ .argument('<report-id>', 'Report id printed by `eval report --server`')
355
+ .description('A fresh download link for a server-rendered report')
356
+ .action(async (reportId) => {
357
+ try {
358
+ console.log((await new EvalSDK(new IndemnClient()).downloadReport(reportId)).download_url);
359
+ }
360
+ catch (err) {
361
+ printError(err);
362
+ process.exit(1);
363
+ }
364
+ });
365
+ eval_
366
+ .command('trends')
367
+ .requiredOption('--agent <id>', 'Agent ID')
368
+ .option('--test-set <id>', 'Only runs of this test set')
369
+ .option('--limit <n>', 'Runs to include', '50')
370
+ .option('--json', 'Output as JSON')
371
+ .description('Pass rate and latency over past runs, with a drift warning when the pass rate dropped')
372
+ .action(async (options) => {
373
+ try {
374
+ const sdk = new EvalSDK(new IndemnClient());
375
+ await sdk.assertAgentInOrg(options.agent);
376
+ const res = await sdk.trends(options.agent, options.testSet, Number(options.limit));
377
+ if (options.json) {
378
+ printJson(res);
379
+ return;
380
+ }
381
+ console.log(formatTable(['Run', 'When', 'Pass rate', 'Total', 'p95 ms'], res.series.map((r) => [
382
+ String(r.run_id), String(r.created_at ?? ''), `${(Number(r.pass_rate ?? 0) * 100).toFixed(1)}%`, String(r.total ?? ''), String(r.latency_p95_ms ?? ''),
383
+ ])));
384
+ if (res.drift)
385
+ console.log(chalk.yellow(`Drift: pass rate fell from ${res.drift.from} to ${res.drift.to} in run ${res.drift.run_id}.`));
386
+ }
387
+ catch (err) {
388
+ printError(err);
389
+ process.exit(1);
390
+ }
391
+ });
392
+ eval_
393
+ .command('compare')
394
+ .argument('<run-a>', 'First run')
395
+ .argument('<run-b>', 'Second run')
396
+ .description('Pass rate and p95 latency difference between two runs')
397
+ .action(async (a, b) => {
398
+ try {
399
+ const sdk = new EvalSDK(new IndemnClient());
400
+ await sdk.getRunInOrg(a);
401
+ await sdk.getRunInOrg(b);
402
+ printJson(await sdk.compareRuns(a, b));
403
+ }
404
+ catch (err) {
405
+ printError(err);
406
+ process.exit(1);
407
+ }
408
+ });
409
+ eval_
410
+ .command('metrics')
411
+ .description('Voice quality metrics a test item may require (names for expected.voice.required_metrics)')
412
+ .action(async () => {
413
+ try {
414
+ printJson(await new EvalSDK(new IndemnClient()).qualityMetrics());
415
+ }
416
+ catch (err) {
417
+ printError(err);
418
+ process.exit(1);
419
+ }
420
+ });
421
+ eval_
422
+ .command('transcript')
423
+ .requiredOption('--agent <id>', 'Agent ID (needs a stored evaluator config)')
424
+ .requiredOption('--baseline <file>', 'JSON list of { sender: "user"|"bot", text }')
425
+ .requiredOption('--replay <file>', 'JSON list of { sender: "user"|"bot", text }')
426
+ .option('--yes', 'Do not ask for confirmation')
427
+ .description('Grade a replayed conversation against a baseline one')
428
+ .action(async (options) => {
429
+ try {
430
+ const sdk = new EvalSDK(new IndemnClient());
431
+ await sdk.assertAgentInOrg(options.agent);
432
+ await confirmOrExit('Grade the transcript? It uses paid model calls.', Boolean(options.yes));
433
+ printJson(await sdk.evaluateTranscript({
434
+ agent_id: options.agent,
435
+ baseline_transcript: JSON.parse(fs.readFileSync(options.baseline, 'utf8')),
436
+ replay_transcript: JSON.parse(fs.readFileSync(options.replay, 'utf8')),
437
+ }));
438
+ }
439
+ catch (err) {
440
+ printError(err);
441
+ process.exit(1);
442
+ }
443
+ });
444
+ const prod = eval_.command('prod-calls').description('Ingested production voice calls, and turning them into regression tests');
445
+ prod.command('list')
446
+ .requiredOption('--agent <id>', 'Agent ID')
447
+ .option('--search <text>', 'Only calls whose transcript contains this')
448
+ .option('--limit <n>', 'At most this many', '50')
449
+ .action(async (options) => {
450
+ try {
451
+ const sdk = new EvalSDK(new IndemnClient());
452
+ await sdk.assertAgentInOrg(options.agent);
453
+ printJson(await sdk.prodCalls(options.agent, { search: options.search, limit: Number(options.limit) }));
454
+ }
455
+ catch (err) {
456
+ printError(err);
457
+ process.exit(1);
458
+ }
459
+ });
460
+ prod.command('promote')
461
+ .argument('<session-id>', 'Call session id')
462
+ .option('--yes', 'Do not ask for confirmation')
463
+ .description('Add the call as a test item to the agent\'s "Regressions (review)" test set')
464
+ .action(async (sessionId, options) => {
465
+ try {
466
+ const sdk = new EvalSDK(new IndemnClient());
467
+ const call = await sdk.prodCall(sessionId);
468
+ if (call.bot_id)
469
+ await sdk.assertAgentInOrg(String(call.bot_id));
470
+ await confirmOrExit(`Add call ${sessionId} to the "Regressions (review)" test set?`, Boolean(options.yes));
471
+ printJson(await sdk.promoteProdCall(sessionId));
472
+ }
473
+ catch (err) {
474
+ printError(err);
475
+ process.exit(1);
476
+ }
477
+ });
478
+ const datasets = eval_.command('datasets').description('Low-level datasets the evaluation trigger creates');
479
+ datasets.command('list').requiredOption('--agent <id>', 'Agent ID').action(async (options) => {
480
+ try {
481
+ const sdk = new EvalSDK(new IndemnClient());
482
+ await sdk.assertAgentInOrg(options.agent);
483
+ printJson(await sdk.listDatasets(options.agent));
484
+ }
485
+ catch (err) {
486
+ printError(err);
487
+ process.exit(1);
488
+ }
489
+ });
490
+ datasets.command('cases').argument('<dataset-id>').description('Test cases of a dataset').action(async (id) => {
491
+ try {
492
+ const sdk = new EvalSDK(new IndemnClient());
493
+ const ds = await sdk.getDataset(id);
494
+ if (ds.agent_id)
495
+ await sdk.assertAgentInOrg(String(ds.agent_id));
496
+ printJson(await sdk.listTestCases(id));
497
+ }
498
+ catch (err) {
499
+ printError(err);
500
+ process.exit(1);
501
+ }
502
+ });
503
+ const evaluators = eval_.command('evaluators').description('Evaluator types and the stored evaluator configs of an agent');
504
+ evaluators.command('types').action(async () => {
505
+ try {
506
+ printJson(await new EvalSDK(new IndemnClient()).evaluatorTypes());
507
+ }
508
+ catch (err) {
509
+ printError(err);
510
+ process.exit(1);
511
+ }
512
+ });
513
+ evaluators.command('list').requiredOption('--agent <id>', 'Agent ID').action(async (options) => {
514
+ try {
515
+ const sdk = new EvalSDK(new IndemnClient());
516
+ await sdk.assertAgentInOrg(options.agent);
517
+ printJson(await sdk.listEvaluatorConfigs(options.agent));
518
+ }
519
+ catch (err) {
520
+ printError(err);
521
+ process.exit(1);
522
+ }
523
+ });
524
+ evaluators.command('delete').argument('<config-id>').option('--yes', 'Do not ask for confirmation').action(async (id, options) => {
525
+ try {
526
+ const sdk = new EvalSDK(new IndemnClient());
527
+ const cfg = await sdk.getEvaluatorConfig(id);
528
+ if (cfg.agent_id)
529
+ await sdk.assertAgentInOrg(String(cfg.agent_id));
530
+ await confirmOrExit(`Delete evaluator config ${id}?`, Boolean(options.yes));
531
+ printJson(await sdk.deleteEvaluatorConfig(id));
532
+ }
533
+ catch (err) {
534
+ printError(err);
535
+ process.exit(1);
536
+ }
537
+ });
279
538
  // Rubric commands — registered on program, not nested under eval
280
539
  const rubric = program.command('rubric').description('Manage evaluation rubrics');
281
540
  rubric
282
541
  .command('list')
283
- .description('List rubrics')
284
- .option('--agent-id <id>', 'Filter by agent ID')
542
+ .description('List rubrics of an agent')
543
+ .requiredOption('--agent-id <id>', 'Agent ID (rubrics of other orgs are never listed)')
285
544
  .option('--json', 'Output as JSON')
286
545
  .option('--limit <n>', 'Limit number of results')
287
546
  .option('--page <n>', 'Page number (use with --limit)')
@@ -290,6 +549,7 @@ export function registerEvalCommands(program) {
290
549
  const client = new IndemnClient();
291
550
  const sdk = new RubricsSDK(client);
292
551
  const spinner = ora('Fetching rubrics...').start();
552
+ await new EvalSDK(client).assertAgentInOrg(options.agentId);
293
553
  let rubrics = await sdk.listRubrics(options.agentId);
294
554
  spinner.stop();
295
555
  if (options.page && options.limit) {
@@ -457,13 +717,10 @@ export function registerEvalCommands(program) {
457
717
  .command('delete')
458
718
  .argument('<id>', 'Rubric ID')
459
719
  .description('Delete a rubric')
460
- .action(async (id) => {
720
+ .option('--yes', 'Do not ask for confirmation')
721
+ .action(async (id, options) => {
461
722
  try {
462
- const confirmed = await confirm(`Delete rubric ${id}? This cannot be undone.`);
463
- if (!confirmed) {
464
- console.log('Cancelled.');
465
- return;
466
- }
723
+ await confirmOrExit(`Delete rubric ${id}? This cannot be undone.`, Boolean(options.yes));
467
724
  const client = new IndemnClient();
468
725
  const sdk = new RubricsSDK(client);
469
726
  const spinner = ora('Deleting rubric...').start();
@@ -512,8 +769,8 @@ export function registerEvalCommands(program) {
512
769
  const testset = program.command('testset').description('Manage evaluation test sets');
513
770
  testset
514
771
  .command('list')
515
- .description('List test sets')
516
- .option('--agent-id <id>', 'Filter by agent ID')
772
+ .description('List test sets of an agent')
773
+ .requiredOption('--agent-id <id>', 'Agent ID (test sets of other orgs are never listed)')
517
774
  .option('--json', 'Output as JSON')
518
775
  .option('--limit <n>', 'Limit number of results')
519
776
  .option('--page <n>', 'Page number (use with --limit)')
@@ -522,18 +779,8 @@ export function registerEvalCommands(program) {
522
779
  const client = new IndemnClient();
523
780
  const sdk = new TestSetsSDK(client);
524
781
  const spinner = ora('Fetching test sets...').start();
525
- let sets;
526
- try {
527
- sets = await sdk.listTestSets(options.agentId);
528
- }
529
- catch (err) {
530
- spinner.stop();
531
- if (!options.agentId && err?.message?.includes('500')) {
532
- printError('The server returned an error listing all test sets. Try filtering by agent: indemn testset list --agent-id <id>');
533
- process.exit(1);
534
- }
535
- throw err;
536
- }
782
+ await new EvalSDK(client).assertAgentInOrg(options.agentId);
783
+ let sets = await sdk.listTestSets(options.agentId);
537
784
  spinner.stop();
538
785
  if (options.page && options.limit) {
539
786
  const page = parseInt(options.page, 10);
@@ -700,13 +947,10 @@ export function registerEvalCommands(program) {
700
947
  .command('delete')
701
948
  .argument('<id>', 'Test set ID')
702
949
  .description('Delete a test set')
703
- .action(async (id) => {
950
+ .option('--yes', 'Do not ask for confirmation')
951
+ .action(async (id, options) => {
704
952
  try {
705
- const confirmed = await confirm(`Delete test set ${id}? This cannot be undone.`);
706
- if (!confirmed) {
707
- console.log('Cancelled.');
708
- return;
709
- }
953
+ await confirmOrExit(`Delete test set ${id}? This cannot be undone.`, Boolean(options.yes));
710
954
  const client = new IndemnClient();
711
955
  const sdk = new TestSetsSDK(client);
712
956
  const spinner = ora('Deleting test set...').start();