@indemn/cli 1.6.0 → 1.7.1

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 +62 -79
  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 +21 -4
  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 +89 -109
  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/eval.ts CHANGED
@@ -3,7 +3,10 @@ import * as fs from 'fs';
3
3
  import ora from 'ora';
4
4
  import chalk from 'chalk';
5
5
  import { IndemnClient } from '../sdk/client.js';
6
- import { EvalSDK } from '../sdk/eval.js';
6
+ import { EvalSDK, gateVerdict, type GateOptions } from '../sdk/eval.js';
7
+ import type { EvaluationTriggerInput } from '../sdk/types.js';
8
+ import { CliError, EXIT } from '../core/runtime.js';
9
+ import { confirmOrExit } from './link.js';
7
10
  import { RubricsSDK } from '../sdk/rubrics.js';
8
11
  import { TestSetsSDK } from '../sdk/test-sets.js';
9
12
  import { exportEvalMarkdown, exportEvalReport } from '../report/export.js';
@@ -15,63 +18,105 @@ export function registerEvalCommands(program: Command): void {
15
18
  eval_
16
19
  .command('run')
17
20
  .argument('<agent-id>', 'Agent ID')
18
- .description('Run an evaluation')
19
- .requiredOption('--test-set <id>', 'Test set ID')
21
+ .description('Run an evaluation (costs model calls; asks first). With a threshold it becomes a CI gate')
22
+ .requiredOption('--test-set <id>', 'Test set ID (must belong to this agent)')
20
23
  .option('--rubric <id>', 'Rubric ID')
21
24
  .option('--concurrency <n>', 'Concurrency level')
22
25
  .option('--wait', 'Wait for evaluation to complete')
26
+ .option('--timeout <seconds>', 'With --wait: give up after this long', '1800')
23
27
  .option('--eval-model <model>', 'Model for evaluation judging')
24
- .option('--keep', 'Keep evaluation data after completion')
25
28
  .option('--limit <n>', 'Limit number of items to evaluate')
26
29
  .option('--component-scope <scope>', 'Component scope filter')
27
30
  .option('--component-ids <ids>', 'Comma-separated component IDs')
28
- .option('--conversation-ids <ids>', 'Comma-separated conversation IDs')
29
- .option('--mode <mode>', 'Evaluation mode (e.g. "transcript")')
31
+ .option('--conversation-ids <ids>', 'Comma-separated conversation IDs (transcript mode)')
32
+ .option('--date-from <date>', 'Grade real conversations from this date (transcript mode)')
33
+ .option('--date-to <date>', 'Up to this date, exclusive (needs --date-from)')
34
+ .option('--mode <mode>', 'Evaluation mode: "transcript" grades real conversations')
35
+ .option('--webhook-url <url>', 'Called when the run finishes')
36
+ .option('--min-pass-rate <rate>', 'CI gate: exit 1 when passed/total is below this (0..1). Implies --wait')
37
+ .option('--max-p95-ms <ms>', 'CI gate: exit 1 when p95 latency is above this. Implies --wait')
38
+ .option('--error-policy <policy>', 'CI gate: block (a failed run fails the gate) or warn', 'block')
39
+ .option('--fail-on-error', 'CI gate: also exit 1 when any case had an infrastructure error. Implies --wait')
40
+ .option('--allow-other-agent', 'Run a test set written for another agent')
41
+ .option('--yes', 'Do not ask for confirmation')
42
+ .option('--json', 'Print the final run as JSON')
30
43
  .action(async (agentId: string, options) => {
31
44
  try {
32
45
  const client = new IndemnClient();
33
46
  const sdk = new EvalSDK(client);
34
- const spinner = ora('Triggering evaluation...').start();
47
+ await sdk.assertAgentInOrg(agentId);
35
48
 
36
- const input: Record<string, unknown> = {
37
- bot_id: agentId,
38
- test_set_id: options.testSet,
39
- };
49
+ const testSet = await new TestSetsSDK(client).getTestSet(options.testSet);
50
+ const owner = (testSet as { agent_id?: string }).agent_id;
51
+ if (owner && owner !== agentId && !options.allowOtherAgent) {
52
+ 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);
53
+ }
54
+
55
+ const input: EvaluationTriggerInput = { bot_id: agentId, test_set_id: options.testSet };
40
56
  if (options.rubric) input.rubric_id = options.rubric;
41
57
  if (options.concurrency) input.concurrency = parseInt(options.concurrency, 10);
58
+ // Voice simulations share call rooms; the dashboard runs them one at a time.
59
+ if (((testSet as { items?: Array<{ type?: string }> }).items ?? []).some((i) => i.type === 'voice_simulation')) input.concurrency = 1;
42
60
  if (options.evalModel) input.eval_model = options.evalModel;
43
- if (options.keep !== undefined) input.keep = options.keep;
44
61
  if (options.limit) input.limit = parseInt(options.limit, 10);
45
62
  if (options.componentScope) input.component_scope = options.componentScope;
46
63
  if (options.componentIds) input.component_ids = options.componentIds.split(',');
47
64
  if (options.conversationIds) input.conversation_ids = options.conversationIds.split(',');
48
- if (options.mode) input.mode = options.mode;
65
+ if (options.webhookUrl) input.webhook_url = options.webhookUrl;
66
+ if (Boolean(options.dateFrom) !== Boolean(options.dateTo)) {
67
+ throw new CliError('--date-from and --date-to go together.', EXIT.NEEDS_INPUT);
68
+ }
69
+ if (options.dateFrom) {
70
+ input.date_from = options.dateFrom;
71
+ input.date_to = options.dateTo;
72
+ if (options.mode && options.mode !== 'transcript') throw new CliError('A date range only works with --mode transcript.', EXIT.NEEDS_INPUT);
73
+ // The service ignores dates outside transcript mode and would run the test set instead.
74
+ input.mode = 'transcript';
75
+ } else if (options.mode) {
76
+ input.mode = options.mode;
77
+ }
78
+
79
+ const items = (testSet as { items?: unknown[] }).items?.length ?? 0;
80
+ const count = input.mode === 'transcript' ? 'real conversations' : `${input.limit ? Math.min(input.limit, items) : items} item(s)`;
81
+ 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));
82
+
83
+ const gate: GateOptions = {
84
+ minPassRate: options.minPassRate !== undefined ? Number(options.minPassRate) : undefined,
85
+ maxP95Ms: options.maxP95Ms !== undefined ? Number(options.maxP95Ms) : undefined,
86
+ errorPolicy: options.errorPolicy === 'warn' ? 'warn' : 'block',
87
+ failOnError: Boolean(options.failOnError),
88
+ };
89
+ const gating = gate.minPassRate !== undefined || gate.maxP95Ms !== undefined || gate.failOnError;
49
90
 
50
- const result = await sdk.triggerEvaluation(input as any);
91
+ const spinner = ora('Triggering evaluation...').start();
92
+ const result = await sdk.triggerEvaluation(input);
51
93
  spinner.stop();
94
+ if (!options.json) {
95
+ printSuccess(`Evaluation started: ${result.run_id}`);
96
+ if (result.total) console.log(` Total items: ${result.total}`);
97
+ }
98
+ if (!options.wait && !gating) return;
52
99
 
53
- printSuccess(`Evaluation started: ${result.run_id}`);
54
- console.log(` Status: ${result.status}`);
55
- if (result.total) console.log(` Total items: ${result.total}`);
56
-
57
- if (options.wait) {
58
- const pollSpinner = ora('Waiting for evaluation to complete...').start();
59
-
60
- try {
61
- const run = await sdk.pollRunUntilComplete(result.run_id);
62
- pollSpinner.stop();
63
-
64
- printSuccess(`Evaluation completed: ${run.status}`);
65
- if (run.passed !== undefined) console.log(` Passed: ${run.passed}`);
66
- if (run.failed !== undefined) console.log(` Failed: ${run.failed}`);
67
- if (run.criteria_passed !== undefined && run.criteria_total !== undefined) {
68
- console.log(` Criteria: ${run.criteria_passed}/${run.criteria_total}`);
69
- }
70
- } catch (err) {
71
- pollSpinner.fail('Evaluation did not complete in time');
72
- throw err;
73
- }
100
+ const pollSpinner = options.json ? null : ora('Waiting for evaluation to complete...').start();
101
+ const run = await sdk.pollRunUntilComplete(result.run_id, 10_000, Number(options.timeout) * 1000).catch((err) => {
102
+ pollSpinner?.fail('Evaluation did not complete in time');
103
+ throw err;
104
+ });
105
+ pollSpinner?.stop();
106
+ const verdict = gateVerdict(run, gate);
107
+ if (options.json) {
108
+ printJson({ run, gate: gating ? verdict : undefined });
109
+ } else {
110
+ printSuccess(`Evaluation ${run.status}: ${run.passed ?? 0}/${run.total ?? 0} passed (${(verdict.passRate * 100).toFixed(1)}%)`);
111
+ if (run.errors) console.log(chalk.yellow(` Infrastructure errors: ${run.errors}`));
112
+ if (run.latency?.p95_ms !== undefined) console.log(` p95 latency: ${run.latency.p95_ms} ms`);
113
+ if (run.criteria_passed !== undefined && run.criteria_total !== undefined) console.log(` Criteria: ${run.criteria_passed}/${run.criteria_total}`);
114
+ }
115
+ if (gating && !verdict.pass) {
116
+ console.error(chalk.red(`Gate failed: ${verdict.reasons.join('; ')}`));
117
+ process.exit(1);
74
118
  }
119
+ if (gating && !options.json) printSuccess('Gate passed.');
75
120
  } catch (err) {
76
121
  printError(err);
77
122
  process.exit(1);
@@ -80,8 +125,8 @@ export function registerEvalCommands(program: Command): void {
80
125
 
81
126
  eval_
82
127
  .command('list')
83
- .description('List evaluation runs')
84
- .option('--agent-id <id>', 'Filter by agent ID')
128
+ .description('List evaluation runs of an agent')
129
+ .requiredOption('--agent-id <id>', 'Agent ID (runs of other orgs are never listed)')
85
130
  .option('--json', 'Output as JSON')
86
131
  .option('--limit <n>', 'Limit number of results')
87
132
  .option('--page <n>', 'Page number (use with --limit)')
@@ -91,6 +136,7 @@ export function registerEvalCommands(program: Command): void {
91
136
  const sdk = new EvalSDK(client);
92
137
  const spinner = ora('Fetching evaluation runs...').start();
93
138
 
139
+ await sdk.assertAgentInOrg(options.agentId);
94
140
  let runs = await sdk.listRuns(options.agentId);
95
141
  spinner.stop();
96
142
 
@@ -139,7 +185,7 @@ export function registerEvalCommands(program: Command): void {
139
185
  const sdk = new EvalSDK(client);
140
186
  const spinner = ora('Fetching run status...').start();
141
187
 
142
- const run = await sdk.getRunStatus(runId);
188
+ const run = await sdk.getRunInOrg(runId);
143
189
  spinner.stop();
144
190
 
145
191
  if (options.json) {
@@ -179,6 +225,7 @@ export function registerEvalCommands(program: Command): void {
179
225
  const sdk = new EvalSDK(client);
180
226
  const spinner = ora('Fetching results...').start();
181
227
 
228
+ await sdk.getRunInOrg(runId);
182
229
  const results = await sdk.getRunResults(runId);
183
230
  spinner.stop();
184
231
 
@@ -211,6 +258,7 @@ export function registerEvalCommands(program: Command): void {
211
258
  const sdk = new EvalSDK(client);
212
259
  const spinner = ora('Fetching bot context...').start();
213
260
 
261
+ await sdk.assertAgentInOrg(agentId);
214
262
  const context = await sdk.getBotContext(agentId);
215
263
  spinner.stop();
216
264
 
@@ -253,6 +301,7 @@ export function registerEvalCommands(program: Command): void {
253
301
  const client = new IndemnClient();
254
302
  const spinner = ora('Fetching evaluation data...').start();
255
303
 
304
+ await new EvalSDK(client).getRunInOrg(runId);
256
305
  const result = await exportEvalMarkdown(runId, options.output, client);
257
306
 
258
307
  spinner.stop();
@@ -266,16 +315,32 @@ export function registerEvalCommands(program: Command): void {
266
315
  eval_
267
316
  .command('report')
268
317
  .argument('<run-id>', 'Evaluation run ID')
269
- .description('Generate branded evaluation report PDF')
318
+ .description('Branded evaluation report PDF (made here, or by the evaluations service with --server)')
270
319
  .option('--output <path>', 'Output file path')
320
+ .option('--server', 'Ask the evaluations service to render it (completed runs only)')
321
+ .option('--force', 'With --server: render again instead of reusing the last report')
271
322
  .action(async (runId: string, options) => {
272
323
  try {
273
324
  const client = new IndemnClient();
274
- const spinner = ora('Fetching evaluation data...').start();
275
-
276
- spinner.text = 'Generating PDF...';
325
+ const sdk = new EvalSDK(client);
326
+ await sdk.getRunInOrg(runId);
327
+ if (options.server) {
328
+ const spinner = ora('Rendering on the evaluations service...').start();
329
+ const { download_url, report_id } = await sdk.exportReport(runId, Boolean(options.force));
330
+ spinner.stop();
331
+ if (options.output) {
332
+ const res = await fetch(download_url);
333
+ if (!res.ok) throw new Error(`Download failed (${res.status}).`);
334
+ fs.writeFileSync(options.output, Buffer.from(await res.arrayBuffer()));
335
+ printSuccess(`Report saved to ${options.output}`);
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();
277
343
  const result = await exportEvalReport(runId, options.output, client);
278
-
279
344
  spinner.stop();
280
345
  printSuccess(`Report saved to ${result.file_path}`);
281
346
  } catch (err) {
@@ -284,13 +349,146 @@ export function registerEvalCommands(program: Command): void {
284
349
  }
285
350
  });
286
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: string) => {
357
+ try { console.log((await new EvalSDK(new IndemnClient()).downloadReport(reportId)).download_url); } catch (err) { printError(err); process.exit(1); }
358
+ });
359
+
360
+ eval_
361
+ .command('trends')
362
+ .requiredOption('--agent <id>', 'Agent ID')
363
+ .option('--test-set <id>', 'Only runs of this test set')
364
+ .option('--limit <n>', 'Runs to include', '50')
365
+ .option('--json', 'Output as JSON')
366
+ .description('Pass rate and latency over past runs, with a drift warning when the pass rate dropped')
367
+ .action(async (options) => {
368
+ try {
369
+ const sdk = new EvalSDK(new IndemnClient());
370
+ await sdk.assertAgentInOrg(options.agent);
371
+ const res = await sdk.trends(options.agent, options.testSet, Number(options.limit)) as { series: Array<Record<string, unknown>>; drift: Record<string, unknown> | null };
372
+ if (options.json) { printJson(res); return; }
373
+ console.log(formatTable(['Run', 'When', 'Pass rate', 'Total', 'p95 ms'], res.series.map((r) => [
374
+ String(r.run_id), String(r.created_at ?? ''), `${(Number(r.pass_rate ?? 0) * 100).toFixed(1)}%`, String(r.total ?? ''), String(r.latency_p95_ms ?? ''),
375
+ ])));
376
+ if (res.drift) console.log(chalk.yellow(`Drift: pass rate fell from ${res.drift.from} to ${res.drift.to} in run ${res.drift.run_id}.`));
377
+ } catch (err) { printError(err); process.exit(1); }
378
+ });
379
+
380
+ eval_
381
+ .command('compare')
382
+ .argument('<run-a>', 'First run')
383
+ .argument('<run-b>', 'Second run')
384
+ .description('Pass rate and p95 latency difference between two runs')
385
+ .action(async (a: string, b: string) => {
386
+ try {
387
+ const sdk = new EvalSDK(new IndemnClient());
388
+ await sdk.getRunInOrg(a);
389
+ await sdk.getRunInOrg(b);
390
+ printJson(await sdk.compareRuns(a, b));
391
+ } catch (err) { printError(err); process.exit(1); }
392
+ });
393
+
394
+ eval_
395
+ .command('metrics')
396
+ .description('Voice quality metrics a test item may require (names for expected.voice.required_metrics)')
397
+ .action(async () => {
398
+ try { printJson(await new EvalSDK(new IndemnClient()).qualityMetrics()); } catch (err) { printError(err); process.exit(1); }
399
+ });
400
+
401
+ eval_
402
+ .command('transcript')
403
+ .requiredOption('--agent <id>', 'Agent ID (needs a stored evaluator config)')
404
+ .requiredOption('--baseline <file>', 'JSON list of { sender: "user"|"bot", text }')
405
+ .requiredOption('--replay <file>', 'JSON list of { sender: "user"|"bot", text }')
406
+ .option('--yes', 'Do not ask for confirmation')
407
+ .description('Grade a replayed conversation against a baseline one')
408
+ .action(async (options) => {
409
+ try {
410
+ const sdk = new EvalSDK(new IndemnClient());
411
+ await sdk.assertAgentInOrg(options.agent);
412
+ await confirmOrExit('Grade the transcript? It uses paid model calls.', Boolean(options.yes));
413
+ printJson(await sdk.evaluateTranscript({
414
+ agent_id: options.agent,
415
+ baseline_transcript: JSON.parse(fs.readFileSync(options.baseline, 'utf8')),
416
+ replay_transcript: JSON.parse(fs.readFileSync(options.replay, 'utf8')),
417
+ }));
418
+ } catch (err) { printError(err); process.exit(1); }
419
+ });
420
+
421
+ const prod = eval_.command('prod-calls').description('Ingested production voice calls, and turning them into regression tests');
422
+ prod.command('list')
423
+ .requiredOption('--agent <id>', 'Agent ID')
424
+ .option('--search <text>', 'Only calls whose transcript contains this')
425
+ .option('--limit <n>', 'At most this many', '50')
426
+ .action(async (options) => {
427
+ try {
428
+ const sdk = new EvalSDK(new IndemnClient());
429
+ await sdk.assertAgentInOrg(options.agent);
430
+ printJson(await sdk.prodCalls(options.agent, { search: options.search, limit: Number(options.limit) }));
431
+ } catch (err) { printError(err); process.exit(1); }
432
+ });
433
+ prod.command('promote')
434
+ .argument('<session-id>', 'Call session id')
435
+ .option('--yes', 'Do not ask for confirmation')
436
+ .description('Add the call as a test item to the agent\'s "Regressions (review)" test set')
437
+ .action(async (sessionId: string, options) => {
438
+ try {
439
+ const sdk = new EvalSDK(new IndemnClient());
440
+ const call = await sdk.prodCall(sessionId);
441
+ if (call.bot_id) await sdk.assertAgentInOrg(String(call.bot_id));
442
+ await confirmOrExit(`Add call ${sessionId} to the "Regressions (review)" test set?`, Boolean(options.yes));
443
+ printJson(await sdk.promoteProdCall(sessionId));
444
+ } catch (err) { printError(err); process.exit(1); }
445
+ });
446
+
447
+ const datasets = eval_.command('datasets').description('Low-level datasets the evaluation trigger creates');
448
+ datasets.command('list').requiredOption('--agent <id>', 'Agent ID').action(async (options) => {
449
+ try {
450
+ const sdk = new EvalSDK(new IndemnClient());
451
+ await sdk.assertAgentInOrg(options.agent);
452
+ printJson(await sdk.listDatasets(options.agent));
453
+ } catch (err) { printError(err); process.exit(1); }
454
+ });
455
+ datasets.command('cases').argument('<dataset-id>').description('Test cases of a dataset').action(async (id: string) => {
456
+ try {
457
+ const sdk = new EvalSDK(new IndemnClient());
458
+ const ds = await sdk.getDataset(id);
459
+ if (ds.agent_id) await sdk.assertAgentInOrg(String(ds.agent_id));
460
+ printJson(await sdk.listTestCases(id));
461
+ } catch (err) { printError(err); process.exit(1); }
462
+ });
463
+
464
+ const evaluators = eval_.command('evaluators').description('Evaluator types and the stored evaluator configs of an agent');
465
+ evaluators.command('types').action(async () => {
466
+ try { printJson(await new EvalSDK(new IndemnClient()).evaluatorTypes()); } catch (err) { printError(err); process.exit(1); }
467
+ });
468
+ evaluators.command('list').requiredOption('--agent <id>', 'Agent ID').action(async (options) => {
469
+ try {
470
+ const sdk = new EvalSDK(new IndemnClient());
471
+ await sdk.assertAgentInOrg(options.agent);
472
+ printJson(await sdk.listEvaluatorConfigs(options.agent));
473
+ } catch (err) { printError(err); process.exit(1); }
474
+ });
475
+ evaluators.command('delete').argument('<config-id>').option('--yes', 'Do not ask for confirmation').action(async (id: string, options) => {
476
+ try {
477
+ const sdk = new EvalSDK(new IndemnClient());
478
+ const cfg = await sdk.getEvaluatorConfig(id);
479
+ if (cfg.agent_id) await sdk.assertAgentInOrg(String(cfg.agent_id));
480
+ await confirmOrExit(`Delete evaluator config ${id}?`, Boolean(options.yes));
481
+ printJson(await sdk.deleteEvaluatorConfig(id));
482
+ } catch (err) { printError(err); process.exit(1); }
483
+ });
484
+
287
485
  // Rubric commands — registered on program, not nested under eval
288
486
  const rubric = program.command('rubric').description('Manage evaluation rubrics');
289
487
 
290
488
  rubric
291
489
  .command('list')
292
- .description('List rubrics')
293
- .option('--agent-id <id>', 'Filter by agent ID')
490
+ .description('List rubrics of an agent')
491
+ .requiredOption('--agent-id <id>', 'Agent ID (rubrics of other orgs are never listed)')
294
492
  .option('--json', 'Output as JSON')
295
493
  .option('--limit <n>', 'Limit number of results')
296
494
  .option('--page <n>', 'Page number (use with --limit)')
@@ -300,6 +498,7 @@ export function registerEvalCommands(program: Command): void {
300
498
  const sdk = new RubricsSDK(client);
301
499
  const spinner = ora('Fetching rubrics...').start();
302
500
 
501
+ await new EvalSDK(client).assertAgentInOrg(options.agentId);
303
502
  let rubrics = await sdk.listRubrics(options.agentId);
304
503
  spinner.stop();
305
504
 
@@ -473,13 +672,10 @@ export function registerEvalCommands(program: Command): void {
473
672
  .command('delete')
474
673
  .argument('<id>', 'Rubric ID')
475
674
  .description('Delete a rubric')
476
- .action(async (id: string) => {
675
+ .option('--yes', 'Do not ask for confirmation')
676
+ .action(async (id: string, options) => {
477
677
  try {
478
- const confirmed = await confirm(`Delete rubric ${id}? This cannot be undone.`);
479
- if (!confirmed) {
480
- console.log('Cancelled.');
481
- return;
482
- }
678
+ await confirmOrExit(`Delete rubric ${id}? This cannot be undone.`, Boolean(options.yes));
483
679
 
484
680
  const client = new IndemnClient();
485
681
  const sdk = new RubricsSDK(client);
@@ -537,8 +733,8 @@ export function registerEvalCommands(program: Command): void {
537
733
 
538
734
  testset
539
735
  .command('list')
540
- .description('List test sets')
541
- .option('--agent-id <id>', 'Filter by agent ID')
736
+ .description('List test sets of an agent')
737
+ .requiredOption('--agent-id <id>', 'Agent ID (test sets of other orgs are never listed)')
542
738
  .option('--json', 'Output as JSON')
543
739
  .option('--limit <n>', 'Limit number of results')
544
740
  .option('--page <n>', 'Page number (use with --limit)')
@@ -548,17 +744,8 @@ export function registerEvalCommands(program: Command): void {
548
744
  const sdk = new TestSetsSDK(client);
549
745
  const spinner = ora('Fetching test sets...').start();
550
746
 
551
- let sets: any[];
552
- try {
553
- sets = await sdk.listTestSets(options.agentId);
554
- } catch (err: any) {
555
- spinner.stop();
556
- if (!options.agentId && err?.message?.includes('500')) {
557
- printError('The server returned an error listing all test sets. Try filtering by agent: indemn testset list --agent-id <id>');
558
- process.exit(1);
559
- }
560
- throw err;
561
- }
747
+ await new EvalSDK(client).assertAgentInOrg(options.agentId);
748
+ let sets: any[] = await sdk.listTestSets(options.agentId);
562
749
  spinner.stop();
563
750
 
564
751
  if (options.page && options.limit) {
@@ -731,13 +918,10 @@ export function registerEvalCommands(program: Command): void {
731
918
  .command('delete')
732
919
  .argument('<id>', 'Test set ID')
733
920
  .description('Delete a test set')
734
- .action(async (id: string) => {
921
+ .option('--yes', 'Do not ask for confirmation')
922
+ .action(async (id: string, options) => {
735
923
  try {
736
- const confirmed = await confirm(`Delete test set ${id}? This cannot be undone.`);
737
- if (!confirmed) {
738
- console.log('Cancelled.');
739
- return;
740
- }
924
+ await confirmOrExit(`Delete test set ${id}? This cannot be undone.`, Boolean(options.yes));
741
925
 
742
926
  const client = new IndemnClient();
743
927
  const sdk = new TestSetsSDK(client);