@indemn/cli 1.0.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 (126) hide show
  1. package/.claude-plugin/plugin.json +13 -0
  2. package/.mcp.json +9 -0
  3. package/README.md +225 -0
  4. package/dist/cli/agents.d.ts +3 -0
  5. package/dist/cli/agents.d.ts.map +1 -0
  6. package/dist/cli/agents.js +187 -0
  7. package/dist/cli/agents.js.map +1 -0
  8. package/dist/cli/auth.d.ts +3 -0
  9. package/dist/cli/auth.d.ts.map +1 -0
  10. package/dist/cli/auth.js +161 -0
  11. package/dist/cli/auth.js.map +1 -0
  12. package/dist/cli/chat.d.ts +3 -0
  13. package/dist/cli/chat.d.ts.map +1 -0
  14. package/dist/cli/chat.js +142 -0
  15. package/dist/cli/chat.js.map +1 -0
  16. package/dist/cli/config.d.ts +3 -0
  17. package/dist/cli/config.d.ts.map +1 -0
  18. package/dist/cli/config.js +119 -0
  19. package/dist/cli/config.js.map +1 -0
  20. package/dist/cli/eval.d.ts +3 -0
  21. package/dist/cli/eval.d.ts.map +1 -0
  22. package/dist/cli/eval.js +717 -0
  23. package/dist/cli/eval.js.map +1 -0
  24. package/dist/cli/functions.d.ts +3 -0
  25. package/dist/cli/functions.d.ts.map +1 -0
  26. package/dist/cli/functions.js +346 -0
  27. package/dist/cli/functions.js.map +1 -0
  28. package/dist/cli/index.d.ts +3 -0
  29. package/dist/cli/index.d.ts.map +1 -0
  30. package/dist/cli/index.js +64 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/kb.d.ts +3 -0
  33. package/dist/cli/kb.d.ts.map +1 -0
  34. package/dist/cli/kb.js +377 -0
  35. package/dist/cli/kb.js.map +1 -0
  36. package/dist/cli/utils/output.d.ts +6 -0
  37. package/dist/cli/utils/output.d.ts.map +1 -0
  38. package/dist/cli/utils/output.js +36 -0
  39. package/dist/cli/utils/output.js.map +1 -0
  40. package/dist/mcp/server.d.ts +2 -0
  41. package/dist/mcp/server.d.ts.map +1 -0
  42. package/dist/mcp/server.js +15 -0
  43. package/dist/mcp/server.js.map +1 -0
  44. package/dist/mcp/tools.d.ts +4 -0
  45. package/dist/mcp/tools.d.ts.map +1 -0
  46. package/dist/mcp/tools.js +814 -0
  47. package/dist/mcp/tools.js.map +1 -0
  48. package/dist/sdk/agents.d.ts +13 -0
  49. package/dist/sdk/agents.d.ts.map +1 -0
  50. package/dist/sdk/agents.js +27 -0
  51. package/dist/sdk/agents.js.map +1 -0
  52. package/dist/sdk/auth.d.ts +18 -0
  53. package/dist/sdk/auth.d.ts.map +1 -0
  54. package/dist/sdk/auth.js +98 -0
  55. package/dist/sdk/auth.js.map +1 -0
  56. package/dist/sdk/chat.d.ts +17 -0
  57. package/dist/sdk/chat.d.ts.map +1 -0
  58. package/dist/sdk/chat.js +113 -0
  59. package/dist/sdk/chat.js.map +1 -0
  60. package/dist/sdk/client.d.ts +36 -0
  61. package/dist/sdk/client.d.ts.map +1 -0
  62. package/dist/sdk/client.js +219 -0
  63. package/dist/sdk/client.js.map +1 -0
  64. package/dist/sdk/config.d.ts +11 -0
  65. package/dist/sdk/config.d.ts.map +1 -0
  66. package/dist/sdk/config.js +20 -0
  67. package/dist/sdk/config.js.map +1 -0
  68. package/dist/sdk/eval.d.ts +13 -0
  69. package/dist/sdk/eval.d.ts.map +1 -0
  70. package/dist/sdk/eval.js +36 -0
  71. package/dist/sdk/eval.js.map +1 -0
  72. package/dist/sdk/functions.d.ts +30 -0
  73. package/dist/sdk/functions.d.ts.map +1 -0
  74. package/dist/sdk/functions.js +59 -0
  75. package/dist/sdk/functions.js.map +1 -0
  76. package/dist/sdk/index.d.ts +12 -0
  77. package/dist/sdk/index.d.ts.map +1 -0
  78. package/dist/sdk/index.js +12 -0
  79. package/dist/sdk/index.js.map +1 -0
  80. package/dist/sdk/kb.d.ts +30 -0
  81. package/dist/sdk/kb.d.ts.map +1 -0
  82. package/dist/sdk/kb.js +102 -0
  83. package/dist/sdk/kb.js.map +1 -0
  84. package/dist/sdk/rubrics.d.ts +14 -0
  85. package/dist/sdk/rubrics.d.ts.map +1 -0
  86. package/dist/sdk/rubrics.js +29 -0
  87. package/dist/sdk/rubrics.js.map +1 -0
  88. package/dist/sdk/test-sets.d.ts +14 -0
  89. package/dist/sdk/test-sets.d.ts.map +1 -0
  90. package/dist/sdk/test-sets.js +29 -0
  91. package/dist/sdk/test-sets.js.map +1 -0
  92. package/dist/sdk/types.d.ts +305 -0
  93. package/dist/sdk/types.d.ts.map +1 -0
  94. package/dist/sdk/types.js +3 -0
  95. package/dist/sdk/types.js.map +1 -0
  96. package/package.json +40 -0
  97. package/skills/agent-setup/SKILL.md +131 -0
  98. package/skills/eval-orchestration/SKILL.md +257 -0
  99. package/skills/rubric-creation/SKILL.md +163 -0
  100. package/skills/test-set-creation/SKILL.md +250 -0
  101. package/src/cli/agents.ts +206 -0
  102. package/src/cli/auth.ts +170 -0
  103. package/src/cli/chat.ts +161 -0
  104. package/src/cli/config.ts +131 -0
  105. package/src/cli/eval.ts +750 -0
  106. package/src/cli/functions.ts +330 -0
  107. package/src/cli/index.ts +68 -0
  108. package/src/cli/kb.ts +412 -0
  109. package/src/cli/utils/output.ts +42 -0
  110. package/src/mcp/server.ts +17 -0
  111. package/src/mcp/tools.ts +971 -0
  112. package/src/sdk/agents.ts +47 -0
  113. package/src/sdk/auth.ts +132 -0
  114. package/src/sdk/chat.ts +133 -0
  115. package/src/sdk/client.ts +238 -0
  116. package/src/sdk/config.ts +28 -0
  117. package/src/sdk/eval.ts +57 -0
  118. package/src/sdk/functions.ts +103 -0
  119. package/src/sdk/index.ts +11 -0
  120. package/src/sdk/kb.ts +183 -0
  121. package/src/sdk/rubrics.ts +35 -0
  122. package/src/sdk/test-sets.ts +35 -0
  123. package/src/sdk/types.ts +347 -0
  124. package/test/fixtures/test-rubric.json +66 -0
  125. package/test/fixtures/test-testset.json +55 -0
  126. package/tsconfig.json +19 -0
@@ -0,0 +1,750 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { IndemnClient } from '../sdk/client.js';
6
+ import { EvalSDK } from '../sdk/eval.js';
7
+ import { RubricsSDK } from '../sdk/rubrics.js';
8
+ import { TestSetsSDK } from '../sdk/test-sets.js';
9
+ import { formatTable, printJson, printError, printSuccess, confirm } from './utils/output.js';
10
+
11
+ export function registerEvalCommands(program: Command): void {
12
+ const eval_ = program.command('eval').description('Run and manage evaluations');
13
+
14
+ eval_
15
+ .command('run')
16
+ .argument('<agent-id>', 'Agent ID')
17
+ .description('Run an evaluation')
18
+ .requiredOption('--test-set <id>', 'Test set ID')
19
+ .option('--rubric <id>', 'Rubric ID')
20
+ .option('--concurrency <n>', 'Concurrency level')
21
+ .option('--wait', 'Wait for evaluation to complete')
22
+ .option('--eval-model <model>', 'Model for evaluation judging')
23
+ .option('--keep', 'Keep evaluation data after completion')
24
+ .option('--limit <n>', 'Limit number of items to evaluate')
25
+ .option('--component-scope <scope>', 'Component scope filter')
26
+ .option('--component-ids <ids>', 'Comma-separated component IDs')
27
+ .option('--conversation-ids <ids>', 'Comma-separated conversation IDs')
28
+ .option('--mode <mode>', 'Evaluation mode (e.g. "transcript")')
29
+ .action(async (agentId: string, options) => {
30
+ try {
31
+ const client = new IndemnClient();
32
+ const sdk = new EvalSDK(client);
33
+ const spinner = ora('Triggering evaluation...').start();
34
+
35
+ const input: Record<string, unknown> = {
36
+ bot_id: agentId,
37
+ test_set_id: options.testSet,
38
+ };
39
+ if (options.rubric) input.rubric_id = options.rubric;
40
+ if (options.concurrency) input.concurrency = parseInt(options.concurrency, 10);
41
+ if (options.evalModel) input.eval_model = options.evalModel;
42
+ if (options.keep !== undefined) input.keep = options.keep;
43
+ if (options.limit) input.limit = parseInt(options.limit, 10);
44
+ if (options.componentScope) input.component_scope = options.componentScope;
45
+ if (options.componentIds) input.component_ids = options.componentIds.split(',');
46
+ if (options.conversationIds) input.conversation_ids = options.conversationIds.split(',');
47
+ if (options.mode) input.mode = options.mode;
48
+
49
+ const result = await sdk.triggerEvaluation(input as any);
50
+ spinner.stop();
51
+
52
+ printSuccess(`Evaluation started: ${result.run_id}`);
53
+ console.log(` Status: ${result.status}`);
54
+ if (result.total) console.log(` Total items: ${result.total}`);
55
+
56
+ if (options.wait) {
57
+ const pollSpinner = ora('Waiting for evaluation to complete...').start();
58
+
59
+ try {
60
+ const run = await sdk.pollRunUntilComplete(result.run_id);
61
+ pollSpinner.stop();
62
+
63
+ printSuccess(`Evaluation completed: ${run.status}`);
64
+ if (run.passed !== undefined) console.log(` Passed: ${run.passed}`);
65
+ if (run.failed !== undefined) console.log(` Failed: ${run.failed}`);
66
+ if (run.criteria_passed !== undefined && run.criteria_total !== undefined) {
67
+ console.log(` Criteria: ${run.criteria_passed}/${run.criteria_total}`);
68
+ }
69
+ } catch (err) {
70
+ pollSpinner.fail('Evaluation did not complete in time');
71
+ throw err;
72
+ }
73
+ }
74
+ } catch (err) {
75
+ printError(err);
76
+ process.exit(1);
77
+ }
78
+ });
79
+
80
+ eval_
81
+ .command('list')
82
+ .description('List evaluation runs')
83
+ .option('--agent-id <id>', 'Filter by agent ID')
84
+ .option('--json', 'Output as JSON')
85
+ .option('--limit <n>', 'Limit number of results')
86
+ .option('--page <n>', 'Page number (use with --limit)')
87
+ .action(async (options) => {
88
+ try {
89
+ const client = new IndemnClient();
90
+ const sdk = new EvalSDK(client);
91
+ const spinner = ora('Fetching evaluation runs...').start();
92
+
93
+ let runs = await sdk.listRuns(options.agentId);
94
+ spinner.stop();
95
+
96
+ if (options.page && options.limit) {
97
+ const page = parseInt(options.page, 10);
98
+ const limit = parseInt(options.limit, 10);
99
+ runs = runs.slice((page - 1) * limit, page * limit);
100
+ } else if (options.limit) {
101
+ runs = runs.slice(0, parseInt(options.limit, 10));
102
+ }
103
+
104
+ if (options.json) {
105
+ printJson(runs);
106
+ return;
107
+ }
108
+
109
+ if (runs.length === 0) {
110
+ console.log('No evaluation runs found.');
111
+ return;
112
+ }
113
+
114
+ const rows = runs.map((r) => [
115
+ r.run_id || '',
116
+ r.status || '',
117
+ r.agent_id || '',
118
+ r.passed !== undefined ? String(r.passed) : '',
119
+ r.failed !== undefined ? String(r.failed) : '',
120
+ r.started_at ? new Date(r.started_at).toLocaleDateString() : '',
121
+ ]);
122
+
123
+ console.log(formatTable(['Run ID', 'Status', 'Agent', 'Passed', 'Failed', 'Started'], rows));
124
+ } catch (err) {
125
+ printError(err);
126
+ process.exit(1);
127
+ }
128
+ });
129
+
130
+ eval_
131
+ .command('status')
132
+ .argument('<run-id>', 'Evaluation run ID')
133
+ .description('Get evaluation run status')
134
+ .option('--json', 'Output as JSON')
135
+ .action(async (runId: string, options) => {
136
+ try {
137
+ const client = new IndemnClient();
138
+ const sdk = new EvalSDK(client);
139
+ const spinner = ora('Fetching run status...').start();
140
+
141
+ const run = await sdk.getRunStatus(runId);
142
+ spinner.stop();
143
+
144
+ if (options.json) {
145
+ printJson(run);
146
+ return;
147
+ }
148
+
149
+ const statusColor = run.status === 'completed' ? chalk.green : run.status === 'failed' ? chalk.red : chalk.yellow;
150
+
151
+ console.log(`Run ID: ${run.run_id}`);
152
+ console.log(`Status: ${statusColor(run.status)}`);
153
+ if (run.agent_id) console.log(`Agent: ${run.agent_id}`);
154
+ if (run.total !== undefined) console.log(`Total: ${run.total}`);
155
+ if (run.completed !== undefined) console.log(`Done: ${run.completed}`);
156
+ if (run.passed !== undefined) console.log(`Passed: ${run.passed}`);
157
+ if (run.failed !== undefined) console.log(`Failed: ${run.failed}`);
158
+ if (run.criteria_passed !== undefined && run.criteria_total !== undefined) {
159
+ console.log(`Criteria: ${run.criteria_passed}/${run.criteria_total}`);
160
+ }
161
+ if (run.started_at) console.log(`Started: ${run.started_at}`);
162
+ if (run.completed_at) console.log(`Finished: ${run.completed_at}`);
163
+ if (run.error) console.log(`Error: ${chalk.red(run.error)}`);
164
+ } catch (err) {
165
+ printError(err);
166
+ process.exit(1);
167
+ }
168
+ });
169
+
170
+ eval_
171
+ .command('results')
172
+ .argument('<run-id>', 'Evaluation run ID')
173
+ .description('Get evaluation results')
174
+ .option('--json', 'Output as JSON')
175
+ .action(async (runId: string, options) => {
176
+ try {
177
+ const client = new IndemnClient();
178
+ const sdk = new EvalSDK(client);
179
+ const spinner = ora('Fetching results...').start();
180
+
181
+ const results = await sdk.getRunResults(runId);
182
+ spinner.stop();
183
+
184
+ if (options.json) {
185
+ printJson(results);
186
+ return;
187
+ }
188
+
189
+ if (results.length === 0) {
190
+ console.log('No results available.');
191
+ return;
192
+ }
193
+
194
+ // Print results as formatted JSON since structure varies
195
+ printJson(results);
196
+ } catch (err) {
197
+ printError(err);
198
+ process.exit(1);
199
+ }
200
+ });
201
+
202
+ eval_
203
+ .command('bot-context')
204
+ .argument('<agent-id>', 'Agent ID')
205
+ .description('Get bot context used for evaluations')
206
+ .option('--json', 'Output as JSON')
207
+ .action(async (agentId: string, options) => {
208
+ try {
209
+ const client = new IndemnClient();
210
+ const sdk = new EvalSDK(client);
211
+ const spinner = ora('Fetching bot context...').start();
212
+
213
+ const context = await sdk.getBotContext(agentId);
214
+ spinner.stop();
215
+
216
+ if (options.json) {
217
+ printJson(context);
218
+ return;
219
+ }
220
+
221
+ if (context.system_prompt) {
222
+ console.log(chalk.bold('System Prompt:'));
223
+ console.log(context.system_prompt);
224
+ console.log();
225
+ }
226
+ if (context.tools && Array.isArray(context.tools)) {
227
+ console.log(chalk.bold(`Tools (${context.tools.length}):`));
228
+ for (const tool of context.tools) {
229
+ console.log(` - ${tool.name || tool._id}: ${tool.description || ''}`);
230
+ }
231
+ console.log();
232
+ }
233
+ if (context.knowledge_bases && Array.isArray(context.knowledge_bases)) {
234
+ console.log(chalk.bold(`Knowledge Bases (${context.knowledge_bases.length}):`));
235
+ for (const kb of context.knowledge_bases) {
236
+ console.log(` - ${kb.name || kb._id}: ${kb.type || ''}`);
237
+ }
238
+ }
239
+ } catch (err) {
240
+ printError(err);
241
+ process.exit(1);
242
+ }
243
+ });
244
+
245
+ // Rubric commands — registered on program, not nested under eval
246
+ const rubric = program.command('rubric').description('Manage evaluation rubrics');
247
+
248
+ rubric
249
+ .command('list')
250
+ .description('List rubrics')
251
+ .option('--agent-id <id>', 'Filter by agent ID')
252
+ .option('--json', 'Output as JSON')
253
+ .option('--limit <n>', 'Limit number of results')
254
+ .option('--page <n>', 'Page number (use with --limit)')
255
+ .action(async (options) => {
256
+ try {
257
+ const client = new IndemnClient();
258
+ const sdk = new RubricsSDK(client);
259
+ const spinner = ora('Fetching rubrics...').start();
260
+
261
+ let rubrics = await sdk.listRubrics(options.agentId);
262
+ spinner.stop();
263
+
264
+ if (options.page && options.limit) {
265
+ const page = parseInt(options.page, 10);
266
+ const limit = parseInt(options.limit, 10);
267
+ rubrics = rubrics.slice((page - 1) * limit, page * limit);
268
+ } else if (options.limit) {
269
+ rubrics = rubrics.slice(0, parseInt(options.limit, 10));
270
+ }
271
+
272
+ if (options.json) {
273
+ printJson(rubrics);
274
+ return;
275
+ }
276
+
277
+ if (rubrics.length === 0) {
278
+ console.log('No rubrics found.');
279
+ return;
280
+ }
281
+
282
+ const rows = rubrics.map((r) => [
283
+ r._id || r.rubric_id || '',
284
+ r.name,
285
+ String(r.version || 1),
286
+ String(r.rules?.length || 0),
287
+ r.updatedAt ? new Date(r.updatedAt).toLocaleDateString() : '',
288
+ ]);
289
+
290
+ console.log(formatTable(['ID', 'Name', 'Version', 'Rules', 'Updated'], rows));
291
+ } catch (err) {
292
+ printError(err);
293
+ process.exit(1);
294
+ }
295
+ });
296
+
297
+ rubric
298
+ .command('get')
299
+ .argument('<id>', 'Rubric ID')
300
+ .description('Get rubric details')
301
+ .option('--json', 'Output as JSON')
302
+ .option('--revision <n>', 'Get a specific version/revision')
303
+ .action(async (id: string, options) => {
304
+ try {
305
+ const client = new IndemnClient();
306
+ const sdk = new RubricsSDK(client);
307
+ const spinner = ora('Fetching rubric...').start();
308
+
309
+ const r = options.revision
310
+ ? await sdk.getVersion(id, parseInt(options.revision, 10))
311
+ : await sdk.getRubric(id);
312
+ spinner.stop();
313
+
314
+ if (options.json) {
315
+ printJson(r);
316
+ return;
317
+ }
318
+
319
+ console.log(`ID: ${r._id || r.rubric_id}`);
320
+ console.log(`Name: ${r.name}`);
321
+ console.log(`Description: ${r.description || '(none)'}`);
322
+ console.log(`Version: ${r.version || 1}`);
323
+ console.log(`Rules: ${r.rules?.length || 0}`);
324
+ if (r.rules?.length) {
325
+ console.log();
326
+ for (const rule of r.rules) {
327
+ console.log(` [${rule.severity}] ${rule.name}: ${rule.description}`);
328
+ }
329
+ }
330
+ } catch (err) {
331
+ printError(err);
332
+ process.exit(1);
333
+ }
334
+ });
335
+
336
+ rubric
337
+ .command('create')
338
+ .description('Create a rubric')
339
+ .option('--file <path>', 'Path to JSON file with rubric definition')
340
+ .option('--name <name>', 'Rubric name')
341
+ .option('--agent-id <id>', 'Agent ID')
342
+ .action(async (options) => {
343
+ try {
344
+ let input: Record<string, unknown>;
345
+
346
+ if (options.file) {
347
+ const filePath = options.file as string;
348
+ if (!fs.existsSync(filePath)) {
349
+ printError(`File not found: ${filePath}`);
350
+ process.exit(1);
351
+ }
352
+ const content = fs.readFileSync(filePath, 'utf-8');
353
+ try {
354
+ input = JSON.parse(content);
355
+ } catch {
356
+ printError('File must contain valid JSON.');
357
+ process.exit(1);
358
+ }
359
+ // Merge CLI options into file content (CLI flags override file values)
360
+ if (options.name) input.name = options.name;
361
+ if (options.agentId) input.agent_id = options.agentId;
362
+ } else if (options.name) {
363
+ input = { name: options.name, rules: [] };
364
+ if (options.agentId) input.agent_id = options.agentId;
365
+ } else {
366
+ printError('Provide --file with rubric JSON or --name for an empty rubric.');
367
+ process.exit(1);
368
+ }
369
+
370
+ const client = new IndemnClient();
371
+ const sdk = new RubricsSDK(client);
372
+ const spinner = ora('Creating rubric...').start();
373
+
374
+ const rubricResult = await sdk.createRubric(input as any);
375
+ spinner.stop();
376
+
377
+ printSuccess(`Rubric created: ${rubricResult._id || rubricResult.rubric_id}`);
378
+ console.log(` Name: ${rubricResult.name}`);
379
+ console.log(` Rules: ${rubricResult.rules?.length || 0}`);
380
+ } catch (err) {
381
+ printError(err);
382
+ process.exit(1);
383
+ }
384
+ });
385
+
386
+ rubric
387
+ .command('update')
388
+ .argument('<id>', 'Rubric ID')
389
+ .description('Update a rubric')
390
+ .option('--file <path>', 'Path to JSON file with rubric updates')
391
+ .option('--name <name>', 'New rubric name')
392
+ .action(async (id: string, options) => {
393
+ try {
394
+ let input: Record<string, unknown>;
395
+
396
+ if (options.file) {
397
+ const filePath = options.file as string;
398
+ if (!fs.existsSync(filePath)) {
399
+ printError(`File not found: ${filePath}`);
400
+ process.exit(1);
401
+ }
402
+ const content = fs.readFileSync(filePath, 'utf-8');
403
+ try {
404
+ input = JSON.parse(content);
405
+ } catch {
406
+ printError('File must contain valid JSON.');
407
+ process.exit(1);
408
+ }
409
+ } else if (options.name) {
410
+ input = { name: options.name };
411
+ } else {
412
+ printError('Provide --file with rubric JSON or --name.');
413
+ process.exit(1);
414
+ }
415
+
416
+ const client = new IndemnClient();
417
+ const sdk = new RubricsSDK(client);
418
+ const spinner = ora('Updating rubric...').start();
419
+
420
+ await sdk.updateRubric(id, input as any);
421
+ spinner.stop();
422
+
423
+ printSuccess(`Rubric ${id} updated.`);
424
+ } catch (err) {
425
+ printError(err);
426
+ process.exit(1);
427
+ }
428
+ });
429
+
430
+ rubric
431
+ .command('delete')
432
+ .argument('<id>', 'Rubric ID')
433
+ .description('Delete a rubric')
434
+ .action(async (id: string) => {
435
+ try {
436
+ const confirmed = await confirm(`Delete rubric ${id}? This cannot be undone.`);
437
+ if (!confirmed) {
438
+ console.log('Cancelled.');
439
+ return;
440
+ }
441
+
442
+ const client = new IndemnClient();
443
+ const sdk = new RubricsSDK(client);
444
+ const spinner = ora('Deleting rubric...').start();
445
+
446
+ await sdk.deleteRubric(id);
447
+ spinner.stop();
448
+
449
+ printSuccess(`Rubric ${id} deleted.`);
450
+ } catch (err) {
451
+ printError(err);
452
+ process.exit(1);
453
+ }
454
+ });
455
+
456
+ rubric
457
+ .command('versions')
458
+ .argument('<id>', 'Rubric ID')
459
+ .description('List rubric versions')
460
+ .option('--json', 'Output as JSON')
461
+ .action(async (id: string, options) => {
462
+ try {
463
+ const client = new IndemnClient();
464
+ const sdk = new RubricsSDK(client);
465
+ const spinner = ora('Fetching versions...').start();
466
+
467
+ const versions = await sdk.listVersions(id);
468
+ spinner.stop();
469
+
470
+ if (options.json) {
471
+ printJson(versions);
472
+ return;
473
+ }
474
+
475
+ if (versions.length === 0) {
476
+ console.log('No versions found.');
477
+ return;
478
+ }
479
+
480
+ const rows = versions.map((v) => [
481
+ String(v.version || ''),
482
+ String(v.rules?.length || 0),
483
+ v.updatedAt ? new Date(v.updatedAt).toLocaleDateString() : '',
484
+ ]);
485
+
486
+ console.log(formatTable(['Version', 'Rules', 'Updated'], rows));
487
+ } catch (err) {
488
+ printError(err);
489
+ process.exit(1);
490
+ }
491
+ });
492
+
493
+ // Test set commands — registered on program, not nested under eval
494
+ const testset = program.command('testset').description('Manage evaluation test sets');
495
+
496
+ testset
497
+ .command('list')
498
+ .description('List test sets')
499
+ .option('--agent-id <id>', 'Filter by agent ID')
500
+ .option('--json', 'Output as JSON')
501
+ .option('--limit <n>', 'Limit number of results')
502
+ .option('--page <n>', 'Page number (use with --limit)')
503
+ .action(async (options) => {
504
+ try {
505
+ const client = new IndemnClient();
506
+ const sdk = new TestSetsSDK(client);
507
+ const spinner = ora('Fetching test sets...').start();
508
+
509
+ let sets: any[];
510
+ try {
511
+ sets = await sdk.listTestSets(options.agentId);
512
+ } catch (err: any) {
513
+ spinner.stop();
514
+ if (!options.agentId && err?.message?.includes('500')) {
515
+ printError('The server returned an error listing all test sets. Try filtering by agent: indemn testset list --agent-id <id>');
516
+ process.exit(1);
517
+ }
518
+ throw err;
519
+ }
520
+ spinner.stop();
521
+
522
+ if (options.page && options.limit) {
523
+ const page = parseInt(options.page, 10);
524
+ const limit = parseInt(options.limit, 10);
525
+ sets = sets.slice((page - 1) * limit, page * limit);
526
+ } else if (options.limit) {
527
+ sets = sets.slice(0, parseInt(options.limit, 10));
528
+ }
529
+
530
+ if (options.json) {
531
+ printJson(sets);
532
+ return;
533
+ }
534
+
535
+ if (sets.length === 0) {
536
+ console.log('No test sets found.');
537
+ return;
538
+ }
539
+
540
+ const rows = sets.map((s) => [
541
+ s._id || s.test_set_id || '',
542
+ s.name,
543
+ String(s.version || 1),
544
+ String(s.items?.length || 0),
545
+ s.updatedAt ? new Date(s.updatedAt).toLocaleDateString() : '',
546
+ ]);
547
+
548
+ console.log(formatTable(['ID', 'Name', 'Version', 'Items', 'Updated'], rows));
549
+ } catch (err) {
550
+ printError(err);
551
+ process.exit(1);
552
+ }
553
+ });
554
+
555
+ testset
556
+ .command('get')
557
+ .argument('<id>', 'Test set ID')
558
+ .description('Get test set details')
559
+ .option('--json', 'Output as JSON')
560
+ .option('--revision <n>', 'Get a specific version/revision')
561
+ .action(async (id: string, options) => {
562
+ try {
563
+ const client = new IndemnClient();
564
+ const sdk = new TestSetsSDK(client);
565
+ const spinner = ora('Fetching test set...').start();
566
+
567
+ const ts = options.revision
568
+ ? await sdk.getVersion(id, parseInt(options.revision, 10))
569
+ : await sdk.getTestSet(id);
570
+ spinner.stop();
571
+
572
+ if (options.json) {
573
+ printJson(ts);
574
+ return;
575
+ }
576
+
577
+ console.log(`ID: ${ts._id || ts.test_set_id}`);
578
+ console.log(`Name: ${ts.name}`);
579
+ console.log(`Description: ${ts.description || '(none)'}`);
580
+ console.log(`Version: ${ts.version || 1}`);
581
+ console.log(`Items: ${ts.items?.length || 0}`);
582
+ if (ts.items?.length) {
583
+ console.log();
584
+ for (const item of ts.items) {
585
+ console.log(` [${item.type}] ${item.name}: ${item.inputs.message || item.inputs.initial_message || ''}`);
586
+ }
587
+ }
588
+ } catch (err) {
589
+ printError(err);
590
+ process.exit(1);
591
+ }
592
+ });
593
+
594
+ testset
595
+ .command('create')
596
+ .description('Create a test set')
597
+ .option('--file <path>', 'Path to JSON file with test set definition')
598
+ .option('--name <name>', 'Test set name')
599
+ .option('--agent-id <id>', 'Agent ID')
600
+ .action(async (options) => {
601
+ try {
602
+ let input: Record<string, unknown>;
603
+
604
+ if (options.file) {
605
+ const filePath = options.file as string;
606
+ if (!fs.existsSync(filePath)) {
607
+ printError(`File not found: ${filePath}`);
608
+ process.exit(1);
609
+ }
610
+ const content = fs.readFileSync(filePath, 'utf-8');
611
+ try {
612
+ input = JSON.parse(content);
613
+ } catch {
614
+ printError('File must contain valid JSON.');
615
+ process.exit(1);
616
+ }
617
+ // Merge CLI options into file content (CLI flags override file values)
618
+ if (options.name) input.name = options.name;
619
+ if (options.agentId) input.agent_id = options.agentId;
620
+ } else if (options.name) {
621
+ input = { name: options.name, items: [] };
622
+ if (options.agentId) input.agent_id = options.agentId;
623
+ } else {
624
+ printError('Provide --file with test set JSON or --name for an empty test set.');
625
+ process.exit(1);
626
+ }
627
+
628
+ const client = new IndemnClient();
629
+ const sdk = new TestSetsSDK(client);
630
+ const spinner = ora('Creating test set...').start();
631
+
632
+ const testSetResult = await sdk.createTestSet(input as any);
633
+ spinner.stop();
634
+
635
+ printSuccess(`Test set created: ${testSetResult._id || testSetResult.test_set_id}`);
636
+ console.log(` Name: ${testSetResult.name}`);
637
+ console.log(` Items: ${testSetResult.items?.length || 0}`);
638
+ } catch (err) {
639
+ printError(err);
640
+ process.exit(1);
641
+ }
642
+ });
643
+
644
+ testset
645
+ .command('update')
646
+ .argument('<id>', 'Test set ID')
647
+ .description('Update a test set')
648
+ .option('--file <path>', 'Path to JSON file with test set updates')
649
+ .option('--name <name>', 'New test set name')
650
+ .action(async (id: string, options) => {
651
+ try {
652
+ let input: Record<string, unknown>;
653
+
654
+ if (options.file) {
655
+ const filePath = options.file as string;
656
+ if (!fs.existsSync(filePath)) {
657
+ printError(`File not found: ${filePath}`);
658
+ process.exit(1);
659
+ }
660
+ const content = fs.readFileSync(filePath, 'utf-8');
661
+ try {
662
+ input = JSON.parse(content);
663
+ } catch {
664
+ printError('File must contain valid JSON.');
665
+ process.exit(1);
666
+ }
667
+ } else if (options.name) {
668
+ input = { name: options.name };
669
+ } else {
670
+ printError('Provide --file with test set JSON or --name.');
671
+ process.exit(1);
672
+ }
673
+
674
+ const client = new IndemnClient();
675
+ const sdk = new TestSetsSDK(client);
676
+ const spinner = ora('Updating test set...').start();
677
+
678
+ await sdk.updateTestSet(id, input as any);
679
+ spinner.stop();
680
+
681
+ printSuccess(`Test set ${id} updated.`);
682
+ } catch (err) {
683
+ printError(err);
684
+ process.exit(1);
685
+ }
686
+ });
687
+
688
+ testset
689
+ .command('delete')
690
+ .argument('<id>', 'Test set ID')
691
+ .description('Delete a test set')
692
+ .action(async (id: string) => {
693
+ try {
694
+ const confirmed = await confirm(`Delete test set ${id}? This cannot be undone.`);
695
+ if (!confirmed) {
696
+ console.log('Cancelled.');
697
+ return;
698
+ }
699
+
700
+ const client = new IndemnClient();
701
+ const sdk = new TestSetsSDK(client);
702
+ const spinner = ora('Deleting test set...').start();
703
+
704
+ await sdk.deleteTestSet(id);
705
+ spinner.stop();
706
+
707
+ printSuccess(`Test set ${id} deleted.`);
708
+ } catch (err) {
709
+ printError(err);
710
+ process.exit(1);
711
+ }
712
+ });
713
+
714
+ testset
715
+ .command('versions')
716
+ .argument('<id>', 'Test set ID')
717
+ .description('List test set versions')
718
+ .option('--json', 'Output as JSON')
719
+ .action(async (id: string, options) => {
720
+ try {
721
+ const client = new IndemnClient();
722
+ const sdk = new TestSetsSDK(client);
723
+ const spinner = ora('Fetching versions...').start();
724
+
725
+ const versions = await sdk.listVersions(id);
726
+ spinner.stop();
727
+
728
+ if (options.json) {
729
+ printJson(versions);
730
+ return;
731
+ }
732
+
733
+ if (versions.length === 0) {
734
+ console.log('No versions found.');
735
+ return;
736
+ }
737
+
738
+ const rows = versions.map((v) => [
739
+ String(v.version || ''),
740
+ String(v.items?.length || 0),
741
+ v.updatedAt ? new Date(v.updatedAt).toLocaleDateString() : '',
742
+ ]);
743
+
744
+ console.log(formatTable(['Version', 'Items', 'Updated'], rows));
745
+ } catch (err) {
746
+ printError(err);
747
+ process.exit(1);
748
+ }
749
+ });
750
+ }