coolhand-cli 0.11.0 → 0.12.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 (91) hide show
  1. package/README.md +8 -0
  2. package/dist/api/client-file-client.d.ts +19 -0
  3. package/dist/api/client-file-client.d.ts.map +1 -0
  4. package/dist/api/client-file-client.js +47 -0
  5. package/dist/api/client-file-client.js.map +1 -0
  6. package/dist/api/help-center-client.d.ts +26 -0
  7. package/dist/api/help-center-client.d.ts.map +1 -0
  8. package/dist/api/help-center-client.js +89 -0
  9. package/dist/api/help-center-client.js.map +1 -0
  10. package/dist/api/last-sync.d.ts.map +1 -1
  11. package/dist/api/last-sync.js +4 -1
  12. package/dist/api/last-sync.js.map +1 -1
  13. package/dist/api/workload-client.d.ts +16 -0
  14. package/dist/api/workload-client.d.ts.map +1 -0
  15. package/dist/api/workload-client.js +28 -0
  16. package/dist/api/workload-client.js.map +1 -0
  17. package/dist/auth/callback-server.d.ts.map +1 -1
  18. package/dist/auth/callback-server.js +13 -1
  19. package/dist/auth/callback-server.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +429 -4
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands/download-client-file.d.ts +9 -0
  24. package/dist/commands/download-client-file.d.ts.map +1 -0
  25. package/dist/commands/download-client-file.js +71 -0
  26. package/dist/commands/download-client-file.js.map +1 -0
  27. package/dist/commands/fetch-log.d.ts.map +1 -1
  28. package/dist/commands/fetch-log.js +9 -0
  29. package/dist/commands/fetch-log.js.map +1 -1
  30. package/dist/commands/get-help-article.d.ts +3 -0
  31. package/dist/commands/get-help-article.d.ts.map +1 -0
  32. package/dist/commands/get-help-article.js +49 -0
  33. package/dist/commands/get-help-article.js.map +1 -0
  34. package/dist/commands/get-template.d.ts.map +1 -1
  35. package/dist/commands/get-template.js +18 -1
  36. package/dist/commands/get-template.js.map +1 -1
  37. package/dist/commands/link-feedback.d.ts +5 -0
  38. package/dist/commands/link-feedback.d.ts.map +1 -0
  39. package/dist/commands/link-feedback.js +89 -0
  40. package/dist/commands/link-feedback.js.map +1 -0
  41. package/dist/commands/list-client-files.d.ts +3 -0
  42. package/dist/commands/list-client-files.d.ts.map +1 -0
  43. package/dist/commands/list-client-files.js +55 -0
  44. package/dist/commands/list-client-files.js.map +1 -0
  45. package/dist/commands/list-help-categories.d.ts +3 -0
  46. package/dist/commands/list-help-categories.d.ts.map +1 -0
  47. package/dist/commands/list-help-categories.js +47 -0
  48. package/dist/commands/list-help-categories.js.map +1 -0
  49. package/dist/commands/search-help-articles.d.ts +3 -0
  50. package/dist/commands/search-help-articles.d.ts.map +1 -0
  51. package/dist/commands/search-help-articles.js +73 -0
  52. package/dist/commands/search-help-articles.js.map +1 -0
  53. package/dist/commands/search-logs.d.ts.map +1 -1
  54. package/dist/commands/search-logs.js +26 -0
  55. package/dist/commands/search-logs.js.map +1 -1
  56. package/dist/commands/search-templates.d.ts.map +1 -1
  57. package/dist/commands/search-templates.js +12 -0
  58. package/dist/commands/search-templates.js.map +1 -1
  59. package/dist/commands/search-workloads.d.ts +3 -0
  60. package/dist/commands/search-workloads.d.ts.map +1 -0
  61. package/dist/commands/search-workloads.js +79 -0
  62. package/dist/commands/search-workloads.js.map +1 -0
  63. package/dist/commands/update-client-file.d.ts +3 -0
  64. package/dist/commands/update-client-file.d.ts.map +1 -0
  65. package/dist/commands/update-client-file.js +39 -0
  66. package/dist/commands/update-client-file.js.map +1 -0
  67. package/dist/errors.d.ts +1 -1
  68. package/dist/errors.d.ts.map +1 -1
  69. package/dist/errors.js.map +1 -1
  70. package/dist/format-cost.d.ts +9 -0
  71. package/dist/format-cost.d.ts.map +1 -0
  72. package/dist/format-cost.js +22 -0
  73. package/dist/format-cost.js.map +1 -0
  74. package/dist/logger.d.ts.map +1 -1
  75. package/dist/logger.js +12 -1
  76. package/dist/logger.js.map +1 -1
  77. package/dist/proxy/sender.d.ts.map +1 -1
  78. package/dist/proxy/sender.js +1 -0
  79. package/dist/proxy/sender.js.map +1 -1
  80. package/dist/sessions/redact-secrets.d.ts.map +1 -1
  81. package/dist/sessions/redact-secrets.js +11 -3
  82. package/dist/sessions/redact-secrets.js.map +1 -1
  83. package/dist/skills/skill-scanner.d.ts.map +1 -1
  84. package/dist/skills/skill-scanner.js +3 -1
  85. package/dist/skills/skill-scanner.js.map +1 -1
  86. package/dist/types.d.ts +86 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js.map +1 -1
  89. package/dist/version.d.ts +2 -2
  90. package/dist/version.js +1 -1
  91. package/package.json +6 -4
package/dist/cli.js CHANGED
@@ -16,6 +16,9 @@ import { run as runMonitor } from './commands/monitor.js';
16
16
  import { run as runAnalyzeClaudeSessions } from './commands/analyze-claude-sessions.js';
17
17
  import { run as runMapClaudeProjects } from './commands/map-claude-projects.js';
18
18
  import { run as runUploadClientFile } from './commands/upload-client-file.js';
19
+ import { run as runListClientFiles } from './commands/list-client-files.js';
20
+ import { run as runDownloadClientFile } from './commands/download-client-file.js';
21
+ import { run as runUpdateClientFile } from './commands/update-client-file.js';
19
22
  import { run as runSyncSkills } from './commands/sync-skills.js';
20
23
  import { run as runListWorkloads } from './commands/list-workloads.js';
21
24
  import { run as runGetWorkload } from './commands/get-workload.js';
@@ -23,15 +26,20 @@ import { run as runUpdateWorkload } from './commands/update-workload.js';
23
26
  import { run as runFetchLog } from './commands/fetch-log.js';
24
27
  import { run as runSearchLogs } from './commands/search-logs.js';
25
28
  import { run as runSearchTemplates } from './commands/search-templates.js';
29
+ import { run as runSearchWorkloads } from './commands/search-workloads.js';
26
30
  import { run as runGetTemplate } from './commands/get-template.js';
27
31
  import { run as runFlushPending, spawnBackgroundFlush } from './commands/flush-pending.js';
28
32
  import { run as runSearchFeedback } from './commands/search-feedback.js';
29
33
  import { run as runGetFeedback } from './commands/get-feedback.js';
34
+ import { run as runSearchHelpArticles } from './commands/search-help-articles.js';
35
+ import { run as runGetHelpArticle } from './commands/get-help-article.js';
36
+ import { run as runListHelpCategories } from './commands/list-help-categories.js';
37
+ import { run as runLinkFeedback } from './commands/link-feedback.js';
30
38
  import { run as runSearchReferencedFiles } from './commands/search-referenced-files.js';
31
39
  import { run as runListReferencedFileSessions } from './commands/list-referenced-file-sessions.js';
32
40
  import { countPending, flushFailed } from './pending-store.js';
33
41
  import { confirm } from './prompt.js';
34
- const BOOLEAN_FLAGS = new Set(['all', 'help', 'h', 'json', 'version', 'v', 'dry-run', 'include-archived', 'include-system', 'include-templates', 'full', 'matched', 'unmatched', 'include-thinking', 'unmatched-only', 'include-prompts', 'include-deprecated', 'force']);
42
+ const BOOLEAN_FLAGS = new Set(['all', 'help', 'h', 'json', 'version', 'v', 'dry-run', 'include-archived', 'include-system', 'include-templates', 'full', 'matched', 'unmatched', 'include-thinking', 'unmatched-only', 'include-prompts', 'include-total', 'include-metrics', 'no-metrics', 'include-deprecated', 'force']);
35
43
  /** Flags whose repeated occurrences accumulate into an array instead of overwriting. */
36
44
  const REPEATABLE_FLAGS = new Set(['project', 'exclude-project', 'skill', 'exclude-skill', 'source']);
37
45
  const COMMANDS = [
@@ -172,6 +180,17 @@ const COMMANDS = [
172
180
  { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
173
181
  ],
174
182
  },
183
+ {
184
+ name: 'link-feedback',
185
+ oneLiner: 'Link feedback records to an optimization in bulk (requires a private key)',
186
+ usage: 'coolhand link-feedback <optimization-id> [feedback-id...] [--file PATH|--file=-] [--note TEXT] [options]',
187
+ options: [
188
+ { flag: '--file PATH', description: 'Read feedback ids from a file (whitespace/comma separated); use --file=- for stdin' },
189
+ { flag: '--note TEXT', description: 'Note attached to every link created' },
190
+ { flag: '--client-id ID', description: 'Use a specific stored client' },
191
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
192
+ ],
193
+ },
175
194
  {
176
195
  name: 'get-feedback',
177
196
  oneLiner: 'Get a single feedback record by ID (requires a private key)',
@@ -222,6 +241,40 @@ const COMMANDS = [
222
241
  { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
223
242
  ],
224
243
  },
244
+ {
245
+ name: 'list-client-files',
246
+ oneLiner: "List the client's uploaded files, newest first (requires a client admin's private key)",
247
+ usage: 'coolhand list-client-files [options]',
248
+ options: [
249
+ { flag: '--status VALUE', description: 'Filter by status: draft, published, or archived' },
250
+ { flag: '--file-type TYPE', description: 'Filter by type: slide_deck, report, or document' },
251
+ { flag: '--page N', description: 'Page number (default: 1)' },
252
+ { flag: '--per-page N', description: 'Results per page (default: 25, max: 100)' },
253
+ { flag: '--client-id ID', description: 'Use a specific stored client' },
254
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
255
+ ],
256
+ },
257
+ {
258
+ name: 'download-client-file',
259
+ oneLiner: "Download a client file's contents to disk (requires a client admin's private key)",
260
+ usage: 'coolhand download-client-file <file-id> [options]',
261
+ options: [
262
+ { flag: '--output PATH', description: 'Where to write the file; a directory keeps the server-provided filename (default: that filename in the current directory)' },
263
+ { flag: '--force', description: 'Overwrite the destination if it already exists' },
264
+ { flag: '--client-id ID', description: 'Use a specific stored client' },
265
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
266
+ ],
267
+ },
268
+ {
269
+ name: 'update-client-file',
270
+ oneLiner: "Change a client file's status (requires a client admin's private key)",
271
+ usage: 'coolhand update-client-file <file-id> --status <draft|published|archived> [options]',
272
+ options: [
273
+ { flag: '--status VALUE', description: 'New status: draft, published, or archived (required)' },
274
+ { flag: '--client-id ID', description: 'Use a specific stored client' },
275
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
276
+ ],
277
+ },
225
278
  {
226
279
  name: 'sync-skills',
227
280
  oneLiner: 'Upload locally-built Claude skills (SKILL.md files) to Coolhand as client files',
@@ -318,8 +371,15 @@ const COMMANDS = [
318
371
  { flag: '--source-api VALUE', description: "Filter by source API (e.g. 'openai', 'anthropic', 'vertex')" },
319
372
  { flag: '--source-api-result VALUE', description: 'Filter by result status: success, failed, operational, unmatched' },
320
373
  { flag: '--unmatched-only', description: 'Only return logs with no assigned template' },
321
- { flag: '--days-back N', description: 'Limit to logs created in the last N days (unrestricted if omitted)' },
374
+ { flag: '--source-application VALUE', description: 'Filter by exact source_application' },
375
+ { flag: '--project-path PATH', description: 'Filter by exact metadata.project_path' },
376
+ { flag: '--days-back N', description: 'Limit to logs created in the last N days (unrestricted if omitted; ignored with --since/--until)' },
377
+ { flag: '--since TS', description: 'Only logs created at or after this ISO8601 timestamp (replaces --days-back)' },
378
+ { flag: '--until TS', description: 'Only logs created before this ISO8601 timestamp (exclusive; must be after --since)' },
379
+ { flag: '--min-cost USD', description: 'Only logs whose cost is at least this many USD (unpriced logs are excluded; 0 is valid)' },
380
+ { flag: '--order VALUE', description: "Order by cost instead of --sort: 'cost_desc' (highest cost first)" },
322
381
  { flag: '--include-prompts', description: 'Include system_prompt and user_prompt in results (may be large)' },
382
+ { flag: '--include-total', description: 'Ask the backend to compute the exact total_count/total_pages instead of the cheaper lower-bound estimate (costs a COUNT(*) server-side; off by default)' },
323
383
  { flag: '--sort VALUE', description: "Sort expression, e.g. 'created_at desc' (default: newest first)" },
324
384
  { flag: '--page N', description: 'Page number (default: 1)' },
325
385
  { flag: '--per-page N', description: 'Results per page (default: 25, max: 100)' },
@@ -337,16 +397,44 @@ const COMMANDS = [
337
397
  { flag: '--status VALUE', description: 'Filter by status: draft, published, or failure' },
338
398
  { flag: '--include-deprecated', description: 'Include deprecated templates (hidden by default)' },
339
399
  { flag: '--include-system', description: "Include the 'Unmatched' and 'Ignored API Calls' system buckets (hidden by default) — inspect 'Unmatched' when logs are misrouting" },
400
+ { flag: '--include-metrics', description: 'Add a cost and performance metrics object to each template' },
401
+ { flag: '--days-back N', description: 'Metrics window: rolling N days ending now (server default 28, max 365; ignored with --since)' },
402
+ { flag: '--since TS', description: 'Metrics window start, ISO8601 (inclusive; overrides --days-back)' },
403
+ { flag: '--until TS', description: 'Metrics window end, ISO8601 (exclusive; defaults to now)' },
340
404
  { flag: '--page N', description: 'Page number (default: 1)' },
341
405
  { flag: '--per-page N', description: 'Results per page (default: 25, max: 100)' },
342
406
  { flag: '--client-id ID', description: 'Use a specific stored client' },
343
407
  { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
344
408
  ],
345
- }, {
409
+ },
410
+ {
411
+ name: 'search-workloads',
412
+ oneLiner: 'List and search workloads, optionally with cost and performance metrics (requires a private key)',
413
+ usage: 'coolhand search-workloads [options]',
414
+ options: [
415
+ { flag: '--search TEXT', description: 'Case-insensitive substring to match in the workload name' },
416
+ { flag: '--include-archived', description: 'Include archived workloads (hidden by default)' },
417
+ { flag: '--include-system', description: "Include system workloads such as 'Unmatched' (hidden by default)" },
418
+ { flag: '--include-templates', description: "Add each workload's active templates and their routing patterns" },
419
+ { flag: '--include-metrics', description: 'Add a cost and performance metrics object to each workload' },
420
+ { flag: '--days-back N', description: 'Metrics window: rolling N days ending now (server default 28, max 365; ignored with --since)' },
421
+ { flag: '--since TS', description: 'Metrics window start, ISO8601 (inclusive; overrides --days-back)' },
422
+ { flag: '--until TS', description: 'Metrics window end, ISO8601 (exclusive; defaults to now)' },
423
+ { flag: '--page N', description: 'Page number (default: 1)' },
424
+ { flag: '--per-page N', description: 'Results per page (default: 25, max: 100)' },
425
+ { flag: '--client-id ID', description: 'Use a specific stored client' },
426
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
427
+ ],
428
+ },
429
+ {
346
430
  name: 'get-template',
347
- oneLiner: 'Get a single LLM request template by ID, including its prompt patterns (requires a private key)',
431
+ oneLiner: 'Get a single LLM request template by ID, including its prompt patterns and metrics (requires a private key)',
348
432
  usage: 'coolhand get-template <template-id> [options]',
349
433
  options: [
434
+ { flag: '--no-metrics', description: 'Omit the metrics object (included by default)' },
435
+ { flag: '--days-back N', description: 'Metrics window: rolling N days ending now (server default 28, max 365; ignored with --since)' },
436
+ { flag: '--since TS', description: 'Metrics window start, ISO8601 (inclusive; overrides --days-back)' },
437
+ { flag: '--until TS', description: 'Metrics window end, ISO8601 (exclusive; defaults to now)' },
350
438
  { flag: '--client-id ID', description: 'Use a specific stored client' },
351
439
  { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
352
440
  ],
@@ -375,6 +463,40 @@ const COMMANDS = [
375
463
  { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
376
464
  ],
377
465
  },
466
+ {
467
+ name: 'search-help-articles',
468
+ oneLiner: 'Search or list Coolhand help center articles (no login needed)',
469
+ usage: 'coolhand search-help-articles [query] [options]',
470
+ options: [
471
+ { flag: '--query TEXT', description: 'Relevance search: quoted phrases, -exclusions and "or" work (or pass it as positional text). Omit to list every article' },
472
+ { flag: '--category SLUG', description: 'Only articles in one section (see list-help-categories)' },
473
+ { flag: '--page N', description: 'Page number (default: 1)' },
474
+ { flag: '--per-page N', description: 'Results per page (default: 25, max: 100)' },
475
+ { flag: '--base-url URL', description: 'Read from another Coolhand server; the stored key is only sent to the server you logged in to' },
476
+ { flag: '--client-id ID', description: 'Use a specific stored client (optional — reads work anonymously)' },
477
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
478
+ ],
479
+ },
480
+ {
481
+ name: 'get-help-article',
482
+ oneLiner: 'Read one help center article by slug, with its full markdown body (no login needed)',
483
+ usage: 'coolhand get-help-article <slug> [options]',
484
+ options: [
485
+ { flag: '--base-url URL', description: 'Read from another Coolhand server; the stored key is only sent to the server you logged in to' },
486
+ { flag: '--client-id ID', description: 'Use a specific stored client (optional — reads work anonymously)' },
487
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
488
+ ],
489
+ },
490
+ {
491
+ name: 'list-help-categories',
492
+ oneLiner: 'List help center sections and their allowed tags (no login needed)',
493
+ usage: 'coolhand list-help-categories [options]',
494
+ options: [
495
+ { flag: '--base-url URL', description: 'Read from another Coolhand server; the stored key is only sent to the server you logged in to' },
496
+ { flag: '--client-id ID', description: 'Use a specific stored client (optional — reads work anonymously)' },
497
+ { flag: '--json', description: 'Emit JSON output instead of human-readable text' },
498
+ ],
499
+ },
378
500
  {
379
501
  name: 'help',
380
502
  oneLiner: 'Show this message or per-command help',
@@ -934,6 +1056,145 @@ function fetchLogOptions(parsed) {
934
1056
  }
935
1057
  return opts;
936
1058
  }
1059
+ const CLIENT_FILE_STATUSES = new Set(['draft', 'published', 'archived']);
1060
+ function parseClientFileStatus(raw) {
1061
+ if (!CLIENT_FILE_STATUSES.has(raw)) {
1062
+ throw new CliError('INVALID_ARGS', '--status must be one of: draft, published, archived');
1063
+ }
1064
+ return raw;
1065
+ }
1066
+ function listClientFilesOptions(parsed) {
1067
+ const opts = {};
1068
+ if (typeof parsed.flags['status'] === 'string') {
1069
+ opts.status = parseClientFileStatus(parsed.flags['status']);
1070
+ }
1071
+ if (typeof parsed.flags['file-type'] === 'string') {
1072
+ if (!CLIENT_FILE_TYPES.has(parsed.flags['file-type'])) {
1073
+ throw new CliError('INVALID_ARGS', '--file-type must be one of: slide_deck, report, document');
1074
+ }
1075
+ opts.fileType = parsed.flags['file-type'];
1076
+ }
1077
+ if (typeof parsed.flags['page'] === 'string') {
1078
+ const raw = parsed.flags['page'];
1079
+ const n = parseInt(raw, 10);
1080
+ if (!/^\d+$/.test(raw) || isNaN(n) || n < 1) {
1081
+ throw new CliError('INVALID_ARGS', '--page must be a positive integer');
1082
+ }
1083
+ opts.page = n;
1084
+ }
1085
+ if (typeof parsed.flags['per-page'] === 'string') {
1086
+ const raw = parsed.flags['per-page'];
1087
+ const n = parseInt(raw, 10);
1088
+ if (!/^\d+$/.test(raw) || isNaN(n) || n < 1 || n > 100) {
1089
+ throw new CliError('INVALID_ARGS', '--per-page must be an integer between 1 and 100');
1090
+ }
1091
+ opts.perPage = n;
1092
+ }
1093
+ if (typeof parsed.flags['client-id'] === 'string') {
1094
+ opts.clientId = parsed.flags['client-id'];
1095
+ }
1096
+ if (parsed.flags.json === true) {
1097
+ opts.json = true;
1098
+ }
1099
+ return opts;
1100
+ }
1101
+ function downloadClientFileOptions(parsed) {
1102
+ const id = parsed.positional[0];
1103
+ if (!id) {
1104
+ throw new CliError('INVALID_ARGS', 'download-client-file requires a <file-id> argument');
1105
+ }
1106
+ const opts = { id };
1107
+ if ('output' in parsed.flags) {
1108
+ const v = parsed.flags['output'];
1109
+ if (typeof v !== 'string' || v.length === 0) {
1110
+ throw new CliError('INVALID_ARGS', '--output requires a path argument');
1111
+ }
1112
+ opts.output = v;
1113
+ }
1114
+ if (parsed.flags['force'] === true) {
1115
+ opts.force = true;
1116
+ }
1117
+ if (typeof parsed.flags['client-id'] === 'string') {
1118
+ opts.clientId = parsed.flags['client-id'];
1119
+ }
1120
+ if (parsed.flags.json === true) {
1121
+ opts.json = true;
1122
+ }
1123
+ return opts;
1124
+ }
1125
+ function updateClientFileOptions(parsed) {
1126
+ const id = parsed.positional[0];
1127
+ if (!id) {
1128
+ throw new CliError('INVALID_ARGS', 'update-client-file requires a <file-id> argument');
1129
+ }
1130
+ const rawStatus = parsed.flags['status'];
1131
+ if (typeof rawStatus !== 'string') {
1132
+ throw new CliError('INVALID_ARGS', 'update-client-file requires --status (draft, published, or archived)');
1133
+ }
1134
+ const opts = { id, status: parseClientFileStatus(rawStatus) };
1135
+ if (typeof parsed.flags['client-id'] === 'string') {
1136
+ opts.clientId = parsed.flags['client-id'];
1137
+ }
1138
+ if (parsed.flags.json === true) {
1139
+ opts.json = true;
1140
+ }
1141
+ return opts;
1142
+ }
1143
+ /** Strict positive integer (`/^\d+$/`, `>= 1`), as `--days-back` has always been parsed. */
1144
+ function positiveIntFlag(parsed, name) {
1145
+ const raw = parsed.flags[name];
1146
+ if (raw === undefined) {
1147
+ return undefined;
1148
+ }
1149
+ const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
1150
+ if (typeof raw !== 'string' || !/^\d+$/.test(raw) || isNaN(n) || n < 1) {
1151
+ throw new CliError('INVALID_ARGS', `--${name} must be a positive integer`);
1152
+ }
1153
+ return n;
1154
+ }
1155
+ /** `--per-page`: strict integer between 1 and 100. */
1156
+ function perPageFlag(parsed) {
1157
+ const raw = parsed.flags['per-page'];
1158
+ if (raw === undefined) {
1159
+ return undefined;
1160
+ }
1161
+ const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
1162
+ if (typeof raw !== 'string' || !/^\d+$/.test(raw) || isNaN(n) || n < 1 || n > 100) {
1163
+ throw new CliError('INVALID_ARGS', '--per-page must be an integer between 1 and 100');
1164
+ }
1165
+ return n;
1166
+ }
1167
+ /**
1168
+ * ISO8601 `--since`/`--until`: passed straight through, never parsed into a `Date` locally — the
1169
+ * server validates it and a 422 surfaces as a `CliError` with its message.
1170
+ */
1171
+ function timestampFlag(parsed, name) {
1172
+ const raw = parsed.flags[name];
1173
+ if (raw === undefined) {
1174
+ return undefined;
1175
+ }
1176
+ if (typeof raw !== 'string' || raw.length === 0) {
1177
+ throw new CliError('INVALID_ARGS', `--${name} requires an ISO8601 timestamp`);
1178
+ }
1179
+ return raw;
1180
+ }
1181
+ /** The metrics window flags shared by search-templates, get-template and search-workloads. */
1182
+ function metricsWindow(parsed) {
1183
+ const window = {};
1184
+ const daysBack = positiveIntFlag(parsed, 'days-back');
1185
+ if (daysBack !== undefined) {
1186
+ window.daysBack = daysBack;
1187
+ }
1188
+ const since = timestampFlag(parsed, 'since');
1189
+ if (since !== undefined) {
1190
+ window.since = since;
1191
+ }
1192
+ const until = timestampFlag(parsed, 'until');
1193
+ if (until !== undefined) {
1194
+ window.until = until;
1195
+ }
1196
+ return window;
1197
+ }
937
1198
  const TEMPLATE_STATUSES = new Set(['draft', 'published', 'failure']);
938
1199
  function searchTemplatesOptions(parsed) {
939
1200
  const opts = {};
@@ -956,6 +1217,10 @@ function searchTemplatesOptions(parsed) {
956
1217
  if (parsed.flags['include-system'] === true) {
957
1218
  opts.includeSystem = true;
958
1219
  }
1220
+ if (parsed.flags['include-metrics'] === true) {
1221
+ opts.includeMetrics = true;
1222
+ }
1223
+ Object.assign(opts, metricsWindow(parsed));
959
1224
  if (typeof parsed.flags['page'] === 'string') {
960
1225
  const raw = parsed.flags['page'];
961
1226
  const n = parseInt(raw, 10);
@@ -980,12 +1245,63 @@ function searchTemplatesOptions(parsed) {
980
1245
  }
981
1246
  return opts;
982
1247
  }
1248
+ function searchWorkloadsOptions(parsed) {
1249
+ const opts = {};
1250
+ if (typeof parsed.flags['search'] === 'string') {
1251
+ opts.search = parsed.flags['search'];
1252
+ }
1253
+ if (parsed.flags['include-archived'] === true) {
1254
+ opts.includeArchived = true;
1255
+ }
1256
+ if (parsed.flags['include-system'] === true) {
1257
+ opts.includeSystem = true;
1258
+ }
1259
+ if (parsed.flags['include-templates'] === true) {
1260
+ opts.includeTemplates = true;
1261
+ }
1262
+ if (parsed.flags['include-metrics'] === true) {
1263
+ opts.includeMetrics = true;
1264
+ }
1265
+ Object.assign(opts, metricsWindow(parsed));
1266
+ const page = positiveIntFlag(parsed, 'page');
1267
+ if (page !== undefined) {
1268
+ opts.page = page;
1269
+ }
1270
+ const perPage = perPageFlag(parsed);
1271
+ if (perPage !== undefined) {
1272
+ opts.perPage = perPage;
1273
+ }
1274
+ if (typeof parsed.flags['client-id'] === 'string') {
1275
+ opts.clientId = parsed.flags['client-id'];
1276
+ }
1277
+ if (parsed.flags.json === true) {
1278
+ opts.json = true;
1279
+ }
1280
+ return opts;
1281
+ }
983
1282
  function getTemplateOptions(parsed) {
984
1283
  const id = parsed.positional[0];
985
1284
  if (!id) {
986
1285
  throw new CliError('INVALID_ARGS', 'get-template requires a <template-id> argument');
987
1286
  }
988
1287
  const opts = { id };
1288
+ if (parsed.flags['no-metrics'] === true) {
1289
+ opts.noMetrics = true;
1290
+ }
1291
+ Object.assign(opts, metricsWindow(parsed));
1292
+ if (typeof parsed.flags['client-id'] === 'string') {
1293
+ opts.clientId = parsed.flags['client-id'];
1294
+ }
1295
+ if (parsed.flags.json === true) {
1296
+ opts.json = true;
1297
+ }
1298
+ return opts;
1299
+ }
1300
+ function helpCenterReadOptions(parsed) {
1301
+ const opts = {};
1302
+ if (typeof parsed.flags['base-url'] === 'string') {
1303
+ opts.baseUrl = parsed.flags['base-url'];
1304
+ }
989
1305
  if (typeof parsed.flags['client-id'] === 'string') {
990
1306
  opts.clientId = parsed.flags['client-id'];
991
1307
  }
@@ -994,6 +1310,47 @@ function getTemplateOptions(parsed) {
994
1310
  }
995
1311
  return opts;
996
1312
  }
1313
+ function searchHelpArticlesOptions(parsed) {
1314
+ const opts = helpCenterReadOptions(parsed);
1315
+ if (parsed.flags['query'] === true) {
1316
+ throw new CliError('INVALID_ARGS', '--query requires a value (attach a leading-dash query as --query=-text)');
1317
+ }
1318
+ const positionalQuery = parsed.positional.join(' ');
1319
+ if (typeof parsed.flags['query'] === 'string' && positionalQuery.trim()) {
1320
+ throw new CliError('INVALID_ARGS', 'Pass the search text either as --query or as positional text, not both');
1321
+ }
1322
+ const query = typeof parsed.flags['query'] === 'string' ? parsed.flags['query'] : positionalQuery;
1323
+ if (query.trim()) {
1324
+ opts.query = query;
1325
+ }
1326
+ if (typeof parsed.flags['category'] === 'string') {
1327
+ opts.category = parsed.flags['category'];
1328
+ }
1329
+ if (typeof parsed.flags['page'] === 'string') {
1330
+ const raw = parsed.flags['page'];
1331
+ const n = parseInt(raw, 10);
1332
+ if (!/^\d+$/.test(raw) || isNaN(n) || n < 1) {
1333
+ throw new CliError('INVALID_ARGS', '--page must be a positive integer');
1334
+ }
1335
+ opts.page = n;
1336
+ }
1337
+ if (typeof parsed.flags['per-page'] === 'string') {
1338
+ const raw = parsed.flags['per-page'];
1339
+ const n = parseInt(raw, 10);
1340
+ if (!/^\d+$/.test(raw) || isNaN(n) || n < 1 || n > 100) {
1341
+ throw new CliError('INVALID_ARGS', '--per-page must be an integer between 1 and 100');
1342
+ }
1343
+ opts.perPage = n;
1344
+ }
1345
+ return opts;
1346
+ }
1347
+ function getHelpArticleOptions(parsed) {
1348
+ const slug = parsed.positional[0];
1349
+ if (!slug) {
1350
+ throw new CliError('INVALID_ARGS', 'get-help-article requires a <slug> argument');
1351
+ }
1352
+ return { ...helpCenterReadOptions(parsed), slug };
1353
+ }
997
1354
  function searchLogsOptions(parsed) {
998
1355
  const opts = {};
999
1356
  if (typeof parsed.flags['template-id'] === 'string') {
@@ -1028,9 +1385,38 @@ function searchLogsOptions(parsed) {
1028
1385
  }
1029
1386
  opts.daysBack = n;
1030
1387
  }
1388
+ if (typeof parsed.flags['source-application'] === 'string') {
1389
+ opts.sourceApplication = parsed.flags['source-application'];
1390
+ }
1391
+ if (typeof parsed.flags['project-path'] === 'string') {
1392
+ opts.projectPath = parsed.flags['project-path'];
1393
+ }
1394
+ const since = timestampFlag(parsed, 'since');
1395
+ if (since !== undefined) {
1396
+ opts.since = since;
1397
+ }
1398
+ const until = timestampFlag(parsed, 'until');
1399
+ if (until !== undefined) {
1400
+ opts.until = until;
1401
+ }
1402
+ if (parsed.flags['min-cost'] !== undefined) {
1403
+ const raw = parsed.flags['min-cost'];
1404
+ // `0` is a valid floor and must reach the server, so compare against undefined, not falsiness.
1405
+ if (typeof raw !== 'string' || !/^\d+(\.\d+)?$/.test(raw)) {
1406
+ throw new CliError('INVALID_ARGS', '--min-cost must be a non-negative decimal number (USD)');
1407
+ }
1408
+ opts.minCost = parseFloat(raw);
1409
+ }
1410
+ if (typeof parsed.flags['order'] === 'string') {
1411
+ // Not allowlisted here: the server validates it and a 422 surfaces as a CliError.
1412
+ opts.order = parsed.flags['order'];
1413
+ }
1031
1414
  if (parsed.flags['include-prompts'] === true) {
1032
1415
  opts.includePrompts = true;
1033
1416
  }
1417
+ if (parsed.flags['include-total'] === true) {
1418
+ opts.includeTotal = true;
1419
+ }
1034
1420
  if (typeof parsed.flags['sort'] === 'string') {
1035
1421
  opts.sort = parsed.flags['sort'];
1036
1422
  }
@@ -1229,6 +1615,29 @@ function searchFeedbackOptions(parsed) {
1229
1615
  }
1230
1616
  return opts;
1231
1617
  }
1618
+ function linkFeedbackOptions(parsed) {
1619
+ const optimizationId = parsed.positional[0];
1620
+ if (!optimizationId) {
1621
+ throw new CliError('INVALID_ARGS', 'link-feedback requires an <optimization-id> argument');
1622
+ }
1623
+ const opts = { optimizationId, feedbackIds: parsed.positional.slice(1) };
1624
+ if (typeof parsed.flags.file === 'string') {
1625
+ opts.file = parsed.flags.file;
1626
+ }
1627
+ else if (parsed.flags.file === true) {
1628
+ throw new CliError('INVALID_ARGS', 'link-feedback --file requires a path; use --file=- to read ids from stdin');
1629
+ }
1630
+ if (typeof parsed.flags.note === 'string') {
1631
+ opts.note = parsed.flags.note;
1632
+ }
1633
+ if (typeof parsed.flags['client-id'] === 'string') {
1634
+ opts.clientId = parsed.flags['client-id'];
1635
+ }
1636
+ if (parsed.flags.json === true) {
1637
+ opts.json = true;
1638
+ }
1639
+ return opts;
1640
+ }
1232
1641
  function getFeedbackOptions(parsed) {
1233
1642
  const id = parsed.positional[0];
1234
1643
  if (!id) {
@@ -1400,6 +1809,8 @@ export async function run(argv) {
1400
1809
  return await runWildcard(wildcardOptions(parsed));
1401
1810
  case 'search-feedback':
1402
1811
  return await runSearchFeedback(searchFeedbackOptions(parsed));
1812
+ case 'link-feedback':
1813
+ return await runLinkFeedback(linkFeedbackOptions(parsed));
1403
1814
  case 'get-feedback':
1404
1815
  return await runGetFeedback(getFeedbackOptions(parsed));
1405
1816
  case 'analyze-claude-sessions':
@@ -1408,6 +1819,12 @@ export async function run(argv) {
1408
1819
  return await runMapClaudeProjects(mapClaudeProjectsOptions(parsed));
1409
1820
  case 'upload-client-file':
1410
1821
  return await runUploadClientFile(uploadClientFileOptions(parsed));
1822
+ case 'list-client-files':
1823
+ return await runListClientFiles(listClientFilesOptions(parsed));
1824
+ case 'download-client-file':
1825
+ return await runDownloadClientFile(downloadClientFileOptions(parsed));
1826
+ case 'update-client-file':
1827
+ return await runUpdateClientFile(updateClientFileOptions(parsed));
1411
1828
  case 'sync-skills':
1412
1829
  return await runSyncSkills(syncSkillsOptions(parsed));
1413
1830
  case 'list-workloads':
@@ -1422,8 +1839,16 @@ export async function run(argv) {
1422
1839
  return await runSearchLogs(searchLogsOptions(parsed));
1423
1840
  case 'search-templates':
1424
1841
  return await runSearchTemplates(searchTemplatesOptions(parsed));
1842
+ case 'search-workloads':
1843
+ return await runSearchWorkloads(searchWorkloadsOptions(parsed));
1425
1844
  case 'get-template':
1426
1845
  return await runGetTemplate(getTemplateOptions(parsed));
1846
+ case 'search-help-articles':
1847
+ return await runSearchHelpArticles(searchHelpArticlesOptions(parsed));
1848
+ case 'get-help-article':
1849
+ return await runGetHelpArticle(getHelpArticleOptions(parsed));
1850
+ case 'list-help-categories':
1851
+ return await runListHelpCategories(helpCenterReadOptions(parsed));
1427
1852
  case 'search-referenced-files':
1428
1853
  return await runSearchReferencedFiles(searchReferencedFilesOptions(parsed));
1429
1854
  case 'list-referenced-file-sessions':