@moda-ai/cli 1.1.1 → 1.2.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 (59) hide show
  1. package/README.md +36 -0
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/agent.js +477 -0
  4. package/dist/analyst-tui.d.ts +81 -0
  5. package/dist/analyst-tui.js +314 -0
  6. package/dist/api-client.d.ts +4 -0
  7. package/dist/api-client.js +20 -3
  8. package/dist/auth.d.ts +22 -3
  9. package/dist/auth.js +230 -66
  10. package/dist/cli.d.ts +20 -3
  11. package/dist/cli.js +768 -93
  12. package/dist/command-context.d.ts +34 -0
  13. package/dist/command-context.js +38 -0
  14. package/dist/command-registry.d.ts +22 -0
  15. package/dist/command-registry.js +15 -0
  16. package/dist/config.d.ts +40 -1
  17. package/dist/config.js +70 -3
  18. package/dist/doctor.d.ts +79 -0
  19. package/dist/doctor.js +1102 -0
  20. package/dist/errors.d.ts +5 -0
  21. package/dist/errors.js +9 -0
  22. package/dist/harness-report.d.ts +138 -0
  23. package/dist/harness-report.js +1464 -0
  24. package/dist/harness.d.ts +233 -0
  25. package/dist/harness.js +3264 -0
  26. package/dist/init/index.d.ts +30 -1
  27. package/dist/init/index.js +383 -23
  28. package/dist/init/rules.d.ts +5 -1
  29. package/dist/init/rules.js +22 -16
  30. package/dist/init/sdk-guide.d.ts +3 -0
  31. package/dist/init/sdk-guide.js +397 -0
  32. package/dist/init/tenant.js +12 -3
  33. package/dist/keys.d.ts +47 -0
  34. package/dist/keys.js +136 -0
  35. package/dist/output.d.ts +118 -0
  36. package/dist/output.js +228 -0
  37. package/dist/production-intelligence.d.ts +134 -0
  38. package/dist/production-intelligence.js +1113 -0
  39. package/dist/profile-commands.d.ts +10 -0
  40. package/dist/profile-commands.js +252 -0
  41. package/dist/profiles.d.ts +90 -0
  42. package/dist/profiles.js +432 -0
  43. package/dist/prompts.d.ts +6 -0
  44. package/dist/prompts.js +368 -0
  45. package/dist/schemas.d.ts +4 -4
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +468 -0
  48. package/dist/sync.d.ts +5 -0
  49. package/dist/sync.js +27 -0
  50. package/dist/telemetry.d.ts +18 -0
  51. package/dist/telemetry.js +63 -0
  52. package/dist/tui.d.ts +40 -0
  53. package/dist/tui.js +387 -0
  54. package/dist/workspace.d.ts +12 -0
  55. package/dist/workspace.js +1294 -0
  56. package/hooks/moda-snapshot.sh +41 -0
  57. package/package.json +10 -7
  58. package/skills/moda-cli/SKILL.md +48 -4
  59. package/skills/integrate-moda/SKILL.md +0 -63
package/dist/cli.js CHANGED
@@ -1,43 +1,82 @@
1
1
  #!/usr/bin/env node
2
2
  import { z } from 'zod';
3
- import { validateConfig, VERSION } from './config.js';
4
- import { callDataAPI, ApiError } from './api-client.js';
3
+ import { validateConfig, VERSION, resolveApiKey } from './config.js';
4
+ import { callDataAPI, ApiError, getApiRequestCount, resetApiRequestCount, } from './api-client.js';
5
5
  import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
6
6
  import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
7
+ import { runPromptsCommand } from './prompts.js';
8
+ import { runHooksInstall, runSparkCommand, runWorkspaceCommand } from './workspace.js';
9
+ import { runSkillsCommand } from './skills.js';
10
+ import { runHarnessCommand } from './harness.js';
11
+ import { runSyncCommand } from './sync.js';
12
+ import { runDoctorCommand, runStatusCommand } from './doctor.js';
13
+ import { CliInputError } from './errors.js';
14
+ import { isTelemetryDisabled, sendCliUsageTelemetry, stripTelemetryFlag, } from './telemetry.js';
15
+ import { createCommandContext } from './command-context.js';
16
+ import { createCommandRegistry } from './command-registry.js';
17
+ import { createAgentErrorEnvelope, errorToAgentError, outputModeFromFlags, } from './output.js';
18
+ import { runAuthCommand, runConfigCommand, runKeysCommand, runProfilesCommand } from './profile-commands.js';
19
+ import { runAgentCommand } from './agent.js';
20
+ import { runProductionCommand } from './production-intelligence.js';
21
+ export { CliInputError } from './errors.js';
7
22
  export function parseArgs(argv) {
8
23
  const args = argv.slice(2);
9
- const command = args[0] || '';
24
+ let command = '';
10
25
  const flags = {};
26
+ const positionals = [];
11
27
  let positional;
12
- let i = 1;
28
+ let i = 0;
13
29
  while (i < args.length) {
14
30
  const arg = args[i];
15
31
  if (arg.startsWith('--')) {
16
- const eqIndex = arg.indexOf('=');
17
- if (eqIndex !== -1) {
18
- // --key=value
19
- const key = arg.slice(2, eqIndex);
20
- flags[key] = arg.slice(eqIndex + 1);
21
- }
22
- else {
23
- // --key value
24
- const key = arg.slice(2);
25
- const next = args[i + 1];
26
- if (next !== undefined && !next.startsWith('--')) {
27
- flags[key] = next;
28
- i++;
29
- }
30
- else {
31
- flags[key] = 'true';
32
- }
33
- }
32
+ i = parseFlag(args, i, flags, command === '');
33
+ }
34
+ else if (command === '') {
35
+ command = arg;
34
36
  }
35
37
  else if (positional === undefined) {
36
38
  positional = arg;
39
+ positionals.push(arg);
40
+ }
41
+ else {
42
+ positionals.push(arg);
37
43
  }
38
44
  i++;
39
45
  }
40
- return { command, positional, flags };
46
+ return { command, positional, positionals, flags };
47
+ }
48
+ const GLOBAL_BOOLEAN_FLAGS = new Set([
49
+ 'agent',
50
+ 'json',
51
+ 'help',
52
+ 'version',
53
+ 'quiet',
54
+ 'verbose',
55
+ 'yes',
56
+ 'dry-run',
57
+ 'stream',
58
+ 'tui',
59
+ 'no-tui',
60
+ ]);
61
+ function parseFlag(args, index, flags, beforeCommand) {
62
+ const arg = args[index];
63
+ const eqIndex = arg.indexOf('=');
64
+ if (eqIndex !== -1) {
65
+ flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
66
+ return index;
67
+ }
68
+ const key = arg.slice(2);
69
+ const next = args[index + 1];
70
+ if (beforeCommand && GLOBAL_BOOLEAN_FLAGS.has(key)) {
71
+ flags[key] = 'true';
72
+ return index;
73
+ }
74
+ if (next !== undefined && !next.startsWith('--')) {
75
+ flags[key] = next;
76
+ return index + 1;
77
+ }
78
+ flags[key] = 'true';
79
+ return index;
41
80
  }
42
81
  // Convert kebab-case flags to snake_case for Zod schema compatibility
43
82
  export function flagsToArgs(flags, positionalKey, positionalValue) {
@@ -59,7 +98,39 @@ function printHelp() {
59
98
 
60
99
  Commands:
61
100
  init Set up Moda in your project
62
- overview Dashboard overview with KPIs
101
+ status Show concise local Moda setup status
102
+ doctor Diagnose local Moda setup and data flow
103
+ auth login Authenticate with Moda
104
+ auth logout Clear cached Moda auth
105
+ auth whoami Show the authenticated Moda user
106
+ profiles list List local Moda profiles
107
+ profiles use <name> Switch active Moda profile
108
+ profiles create <name> Create or update a local Moda profile
109
+ profiles doctor [name] Validate a local Moda profile
110
+ keys list List local key descriptors
111
+ keys create Create a local key descriptor
112
+ config show Show resolved Moda config
113
+ harness analyze Produce a cited structured harness report
114
+ harness validate-report Validate .moda/harness-report.json citations
115
+ harness approve Approve a validated harness report for sync
116
+ harness scan Discover local runtime agents and write .moda/harness.json
117
+ harness sync Sync the local harness graph to Moda Cloud
118
+ harness agents List detected runtime agents and families
119
+ harness explain Explain the discovered harness graph
120
+ agent context Emit local Moda context for coding agents
121
+ agent investigate Diagnose local Moda readiness for agents
122
+ agent next-action Emit prioritized next commands for agents
123
+ agent evidence <ref> Expand one evidence reference
124
+ investigate Rank production issues with evidence
125
+ failures Show production failures worth fixing first
126
+ why-failed <run_id> Explain a failed or partial production run
127
+ ask "question" Ask Moda a production/harness question
128
+ sync Sync the local harness graph to Moda Cloud
129
+ sync harness Sync the local harness graph to Moda Cloud
130
+ sync prompts Sync prompt versions to Moda Cloud
131
+ doctor --json Emit structured diagnostic checks
132
+ status --json Emit concise setup state
133
+ overview Harness health briefing with production signals
63
134
  clusters Browse topic cluster hierarchy
64
135
  cluster-conversations <node_id> List conversations in a cluster
65
136
  conversations Search and filter conversations
@@ -75,6 +146,38 @@ Commands:
75
146
  loops Currently-active loop detections
76
147
  loop <detection_id> Single loop detail
77
148
 
149
+ Prompt management:
150
+ prompts init Create .moda/prompts.yml
151
+ prompts status Show local prompt changes
152
+ prompts diff Show local prompt changes
153
+ prompts sync Upload changed prompt versions
154
+ prompts sync --watch Upload again when prompt files change
155
+ prompts promote <key> Move a dev/staging/prod label
156
+
157
+ Shadow workspace:
158
+ spark status Show Shadow Spark workspace state
159
+ spark explain Explain what Shadow Spark captures and posts
160
+ spark snapshot Manual / hook-invoked Shadow Spark snapshot
161
+ spark snapshot --dry-run Show file inventory without copy/commit/post
162
+ spark log Show Shadow Spark history
163
+ spark bundle Bundle a shadow commit range for replay
164
+ spark remove --yes Delete this repo's local shadow workspace
165
+ workspace init Create shadow workspace for current repo
166
+ workspace snapshot Manual / hook-invoked sync + shadow commit
167
+ workspace status Show shadow workspace state
168
+ workspace explain Explain shadow workspace behavior
169
+ workspace log Show Moda shadow history (not user git log)
170
+ workspace bundle Bundle a shadow commit range for replay
171
+ hooks install Install Claude Code shadow-workspace hooks
172
+ hooks install --agent=codex Install Codex hooks
173
+ hooks install --agent=cursor Install Cursor hooks
174
+ hooks install --agent=all Install Claude Code, Codex, and Cursor hooks
175
+
176
+ Skills:
177
+ skills gen Kick off tenant-wide skill generation
178
+ skills pull Pull tenant-generated skills into .claude/skills (and .cursor/rules)
179
+ skills pull --status=proposed Include not-yet-promoted skills (default: approved)
180
+
78
181
  Options:
79
182
  --help Show this help message
80
183
  --version Show version number
@@ -84,6 +187,14 @@ Common flags:
84
187
  --limit=N Maximum results to return
85
188
  --offset=N Number of results to skip
86
189
  --time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
190
+ --profile=NAME Use a named local Moda profile
191
+ --json Emit machine-readable JSON
192
+ --agent Emit schema-versioned agent JSON
193
+ --stream With --agent, emit NDJSON agent events
194
+ --dry-run Show the planned mutation when supported
195
+ --yes Accept explicit automation defaults
196
+ --tui Force the human analyst dashboard when supported
197
+ --no-tui Show raw analyst stream instead of dashboard
87
198
 
88
199
  Runs flags:
89
200
  --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
@@ -92,9 +203,47 @@ Runs flags:
92
203
 
93
204
  Init flags:
94
205
  --yes Accept all defaults (non-interactive)
206
+ --analyst=auto|claude|codex|cursor|local-scan
207
+ Choose harness analyst adapter
208
+ --tui Show full-screen analyst dashboard in human TTYs
209
+ --no-tui Show raw external analyst output
210
+ --coding-agent-traces Install Claude/Codex/Cursor trace hooks
211
+ --no-coding-agent-traces Skip Claude/Codex/Cursor trace hook installation
212
+ --sdk-guide Write .moda/SDK_INTEGRATION.md
213
+ --no-sdk-guide Skip SDK integration guide creation
214
+ --ci-cd Create GitHub Actions Moda validation workflow
215
+ --no-ci-cd Skip CI/CD workflow creation
95
216
 
96
217
  Examples:
97
218
  moda init
219
+ moda status
220
+ moda doctor --json
221
+ moda profiles list
222
+ moda profiles use staging
223
+ moda config show --profile=prod
224
+ moda harness analyze --summary
225
+ moda harness analyze --analyst=claude --tui
226
+ moda harness analyze --analyst=claude --no-tui
227
+ moda harness analyze --analyst=claude --summary
228
+ moda harness analyze --analyst=local-scan --summary
229
+ moda harness validate-report
230
+ moda harness approve --yes
231
+ moda harness sync --from-report .moda/harness-report.json
232
+ moda --agent doctor
233
+ moda agent context
234
+ moda agent next-action
235
+ moda agent evidence doctor:harness_report_approved_synced
236
+ moda investigate
237
+ moda failures
238
+ moda why-failed 12345678-1234-1234-1234-1234567890ab
239
+ moda ask "what should I fix first?"
240
+ moda sync --dry-run
241
+ moda sync harness --profile=prod
242
+ moda harness scan --json
243
+ moda harness explain --agent=agent_support
244
+ moda init --coding-agent-traces
245
+ moda init --no-coding-agent-traces
246
+ moda init --ci-cd
98
247
  moda overview --days-back=30
99
248
  moda clusters --time-range=7d
100
249
  moda conversations --search="error" --limit=5
@@ -105,7 +254,16 @@ Examples:
105
254
  moda runs --class-id=cron:helpscout-triage-hourly --limit=20
106
255
  moda run 12345678-1234-1234-1234-1234567890ab
107
256
  moda loops --limit=10
108
- moda loop 12345678-1234-1234-1234-1234567890ab`);
257
+ moda loop 12345678-1234-1234-1234-1234567890ab
258
+ moda prompts status
259
+ moda prompts sync
260
+ moda prompts promote support.triage --label=prod --version=pver_abc123
261
+ moda spark explain
262
+ moda spark snapshot --dry-run --json
263
+ moda workspace snapshot --json
264
+ moda hooks install
265
+ moda hooks install --agent=all
266
+ moda skills pull`);
109
267
  }
110
268
  // --- Command Handlers ---
111
269
  async function attachWindows(rows, windowSize) {
@@ -141,25 +299,37 @@ async function attachWindows(rows, windowSize) {
141
299
  }
142
300
  }));
143
301
  }
144
- async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize) {
302
+ async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize, output) {
145
303
  if (!includeWindow) {
146
- console.log(JSON.stringify(enriched, null, 2));
304
+ output.writeData(enriched);
147
305
  return;
148
306
  }
149
307
  const list = Array.isArray(enriched[listKey])
150
308
  ? enriched[listKey]
151
309
  : [];
152
310
  const withWindows = await attachWindows(list, windowSize);
153
- console.log(JSON.stringify({ ...enriched, [listKey]: withWindows }, null, 2));
311
+ output.writeData({ ...enriched, [listKey]: withWindows });
154
312
  }
155
- export async function runCommand(command, positional, flags) {
313
+ export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
156
314
  switch (command) {
315
+ case 'prompts': {
316
+ await runPromptsCommand(positionals, flags, {
317
+ profile: context.profile,
318
+ env: context.env,
319
+ });
320
+ break;
321
+ }
157
322
  case 'overview': {
158
323
  const args = flagsToArgs(flags);
159
- const params = OverviewSchema.parse(args);
160
- const queryString = params.days_back ? `?days_back=${params.days_back}` : '';
161
- const data = await callDataAPI(`/overview${queryString}`);
162
- console.log(JSON.stringify(data, null, 2));
324
+ OverviewSchema.parse(args);
325
+ await runProductionCommand(context);
326
+ break;
327
+ }
328
+ case 'investigate':
329
+ case 'failures':
330
+ case 'why-failed':
331
+ case 'ask': {
332
+ await runProductionCommand(context);
163
333
  break;
164
334
  }
165
335
  case 'clusters': {
@@ -172,14 +342,12 @@ export async function runCommand(command, positional, flags) {
172
342
  query.set('time_range', params.time_range);
173
343
  const queryString = query.toString() ? `?${query.toString()}` : '';
174
344
  const data = await callDataAPI(`/clusters${queryString}`);
175
- console.log(JSON.stringify(data, null, 2));
345
+ context.output.writeData(data);
176
346
  break;
177
347
  }
178
348
  case 'cluster-conversations': {
179
349
  if (!positional) {
180
- console.error('Error: <node_id> is required');
181
- console.error('Usage: moda cluster-conversations <node_id> [--limit=N] [--offset=N]');
182
- process.exit(1);
350
+ throw new CliInputError('<node_id> is required', 'Usage: moda cluster-conversations <node_id> [--limit=N] [--offset=N]');
183
351
  }
184
352
  const args = flagsToArgs(flags, 'node_id', positional);
185
353
  const params = ClusterConversationsSchema.parse(args);
@@ -190,7 +358,7 @@ export async function runCommand(command, positional, flags) {
190
358
  query.set('offset', params.offset.toString());
191
359
  const queryString = query.toString() ? `?${query.toString()}` : '';
192
360
  const data = await callDataAPI(`/clusters/${params.node_id}/conversations${queryString}`);
193
- console.log(JSON.stringify(data, null, 2));
361
+ context.output.writeData(data);
194
362
  break;
195
363
  }
196
364
  case 'conversations': {
@@ -213,14 +381,12 @@ export async function runCommand(command, positional, flags) {
213
381
  query.set('offset', params.offset.toString());
214
382
  const queryString = query.toString() ? `?${query.toString()}` : '';
215
383
  const data = await callDataAPI(`/conversations${queryString}`);
216
- console.log(JSON.stringify(data, null, 2));
384
+ context.output.writeData(data);
217
385
  break;
218
386
  }
219
387
  case 'context': {
220
388
  if (!positional) {
221
- console.error('Error: <conversation_id> is required');
222
- console.error('Usage: moda context <conversation_id> [--msg-index=N] [--window=N]');
223
- process.exit(1);
389
+ throw new CliInputError('<conversation_id> is required', 'Usage: moda context <conversation_id> [--msg-index=N] [--window=N]');
224
390
  }
225
391
  const args = flagsToArgs(flags, 'conversation_id', positional);
226
392
  const params = ContextSchema.parse(args);
@@ -231,7 +397,7 @@ export async function runCommand(command, positional, flags) {
231
397
  query.set('window', params.window.toString());
232
398
  const queryString = query.toString() ? `?${query.toString()}` : '';
233
399
  const data = await callDataAPI(`/conversations/${params.conversation_id}/context${queryString}`);
234
- console.log(JSON.stringify(data, null, 2));
400
+ context.output.writeData(data);
235
401
  break;
236
402
  }
237
403
  case 'frustrations': {
@@ -246,7 +412,7 @@ export async function runCommand(command, positional, flags) {
246
412
  query.set('offset', params.offset.toString());
247
413
  const queryString = query.toString() ? `?${query.toString()}` : '';
248
414
  const data = await callDataAPI(`/frustrations${queryString}`);
249
- await printWithOptionalWindows(withFrustrationAnchors(data), 'frustrations', params.include_window === true, params.window ?? 1);
415
+ await printWithOptionalWindows(withFrustrationAnchors(data), 'frustrations', params.include_window === true, params.window ?? 1, context.output);
250
416
  break;
251
417
  }
252
418
  case 'tool-failures': {
@@ -254,14 +420,12 @@ export async function runCommand(command, positional, flags) {
254
420
  const params = ToolFailuresSchema.parse(args);
255
421
  const queryString = params.days_back ? `?days_back=${params.days_back}` : '';
256
422
  const data = await callDataAPI(`/tool-failures${queryString}`);
257
- console.log(JSON.stringify(data, null, 2));
423
+ context.output.writeData(data);
258
424
  break;
259
425
  }
260
426
  case 'tool-failure-detail': {
261
427
  if (!positional) {
262
- console.error('Error: <tool_name> is required');
263
- console.error('Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N] [--offset=N]');
264
- process.exit(1);
428
+ throw new CliInputError('<tool_name> is required', 'Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N] [--offset=N]');
265
429
  }
266
430
  const args = flagsToArgs(flags, 'tool_name', positional);
267
431
  const params = ToolFailureDetailSchema.parse(args);
@@ -276,7 +440,7 @@ export async function runCommand(command, positional, flags) {
276
440
  query.set('offset', params.offset.toString());
277
441
  const queryString = query.toString() ? `?${query.toString()}` : '';
278
442
  const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
279
- await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1);
443
+ await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1, context.output);
280
444
  break;
281
445
  }
282
446
  // ─── Workflows + Runs ──────────────────────────────────────
@@ -290,7 +454,7 @@ export async function runCommand(command, positional, flags) {
290
454
  query.set('user_id', params.user_id);
291
455
  const queryString = query.toString() ? `?${query.toString()}` : '';
292
456
  const data = await callDataAPI(`/workflows${queryString}`);
293
- console.log(JSON.stringify(data, null, 2));
457
+ context.output.writeData(data);
294
458
  break;
295
459
  }
296
460
  case 'runs': {
@@ -311,19 +475,17 @@ export async function runCommand(command, positional, flags) {
311
475
  query.set('offset', params.offset.toString());
312
476
  const queryString = query.toString() ? `?${query.toString()}` : '';
313
477
  const data = await callDataAPI(`/runs${queryString}`);
314
- console.log(JSON.stringify(data, null, 2));
478
+ context.output.writeData(data);
315
479
  break;
316
480
  }
317
481
  case 'run': {
318
482
  if (!positional) {
319
- console.error('Error: <run_id> is required');
320
- console.error('Usage: moda run <run_id>');
321
- process.exit(1);
483
+ throw new CliInputError('<run_id> is required', 'Usage: moda run <run_id>');
322
484
  }
323
485
  const args = flagsToArgs(flags, 'run_id', positional);
324
486
  const params = RunDetailSchema.parse(args);
325
487
  const data = await callDataAPI(`/runs/${params.run_id}`);
326
- console.log(JSON.stringify(data, null, 2));
488
+ context.output.writeData(data);
327
489
  break;
328
490
  }
329
491
  case 'loops': {
@@ -334,47 +496,575 @@ export async function runCommand(command, positional, flags) {
334
496
  query.set('limit', params.limit.toString());
335
497
  const queryString = query.toString() ? `?${query.toString()}` : '';
336
498
  const data = await callDataAPI(`/loops/active${queryString}`);
337
- console.log(JSON.stringify(data, null, 2));
499
+ context.output.writeData(data);
338
500
  break;
339
501
  }
340
502
  case 'loop': {
341
503
  if (!positional) {
342
- console.error('Error: <detection_id> is required');
343
- console.error('Usage: moda loop <detection_id>');
344
- process.exit(1);
504
+ throw new CliInputError('<detection_id> is required', 'Usage: moda loop <detection_id>');
345
505
  }
346
506
  const args = flagsToArgs(flags, 'detection_id', positional);
347
507
  const params = LoopDetailSchema.parse(args);
348
508
  const data = await callDataAPI(`/loops/${params.detection_id}`);
349
- console.log(JSON.stringify(data, null, 2));
509
+ context.output.writeData(data);
350
510
  break;
351
511
  }
352
512
  default:
353
- console.error(`Error: Unknown command '${command}'`);
354
- console.error('Run "moda --help" for usage information.');
355
- process.exit(1);
513
+ throw new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
356
514
  }
357
515
  }
516
+ function legacyCommand(metadata) {
517
+ return {
518
+ ...metadata,
519
+ handler: async (context) => {
520
+ await runCommand(context.command, context.positional, context.flags, context.positionals, context);
521
+ return { exitCode: 0 };
522
+ },
523
+ };
524
+ }
525
+ function initAgentWriteOptions(result) {
526
+ const changedFiles = initChangedFiles(result.setupPlan);
527
+ const manualActions = result.setupPlan.filter((action) => action.status === 'manual' || action.status === 'skipped');
528
+ return {
529
+ summary: {
530
+ text: `Moda init completed for ${result.tenantName}. ${changedFiles.length} artifact(s) were created or verified.`,
531
+ confidence: 'high',
532
+ },
533
+ findings: result.phases.map((phase) => ({
534
+ id: `init:${phase.id}`,
535
+ title: phase.title,
536
+ status: phase.status,
537
+ description: phase.details,
538
+ })),
539
+ recommendedActions: manualActions.map((action) => ({
540
+ id: `init:action:${action.category}`,
541
+ title: action.title,
542
+ rationale: action.reason,
543
+ command: action.command,
544
+ mutability: action.command?.includes(' sync') ? 'sync' : 'write',
545
+ requires_approval: true,
546
+ })),
547
+ nextCommands: [
548
+ {
549
+ command: 'moda status',
550
+ purpose: 'Inspect the local Moda setup state.',
551
+ mutability: 'read',
552
+ requires_approval: false,
553
+ },
554
+ {
555
+ command: 'moda doctor',
556
+ purpose: 'Diagnose local setup and data-flow readiness.',
557
+ mutability: 'read',
558
+ requires_approval: false,
559
+ },
560
+ {
561
+ command: 'moda harness sync --summary',
562
+ purpose: 'Sync the approved harness graph to Moda Cloud after review.',
563
+ mutability: 'sync',
564
+ requires_approval: true,
565
+ },
566
+ ],
567
+ artifacts: changedFiles.map((file) => ({
568
+ kind: initArtifactKind(file),
569
+ path: file,
570
+ description: 'Created or verified during Moda init.',
571
+ })),
572
+ warnings: manualActions
573
+ .filter((action) => action.reason)
574
+ .map((action) => `${action.title}: ${action.reason}`),
575
+ };
576
+ }
577
+ function initChangedFiles(setupPlan) {
578
+ return [...new Set(setupPlan
579
+ .filter((action) => action.status === 'applied')
580
+ .flatMap((action) => action.files))].filter((file) => !file.includes('.env'));
581
+ }
582
+ function initArtifactKind(file) {
583
+ if (file.includes('harness'))
584
+ return 'harness_artifact';
585
+ if (file.includes('prompt'))
586
+ return 'prompt_manifest';
587
+ if (file.includes('workflow'))
588
+ return 'ci_workflow';
589
+ if (file.startsWith('~/'))
590
+ return 'agent_hook_config';
591
+ return 'init_artifact';
592
+ }
593
+ const dataApiDefaults = {
594
+ auth: 'api-key',
595
+ outputModes: ['human', 'json', 'agent'],
596
+ mutability: 'read',
597
+ validateConfigBeforeRun: true,
598
+ resetApiRequestCountBeforeRun: true,
599
+ telemetry: 'result-and-error',
600
+ };
601
+ export const commandRegistry = createCommandRegistry([
602
+ {
603
+ name: 'init',
604
+ description: 'Set up Moda in your project',
605
+ examples: ['moda init', 'moda init --coding-agent-traces', 'moda init --ci-cd'],
606
+ auth: 'none',
607
+ outputModes: ['human', 'json', 'agent'],
608
+ mutability: 'write',
609
+ validateConfigBeforeRun: false,
610
+ resetApiRequestCountBeforeRun: true,
611
+ telemetry: 'result',
612
+ handler: async (context) => {
613
+ const { runInit } = await import('./init/index.js');
614
+ if (context.outputMode === 'agent-stream') {
615
+ context.output.writeEvent({
616
+ event: 'started',
617
+ phase: 'init',
618
+ message: 'Starting Moda init.',
619
+ elapsed_ms: Date.now() - context.startedAt,
620
+ });
621
+ }
622
+ const result = await runInit(flagsToArgs(context.flags), {
623
+ onEvent: (event) => {
624
+ context.output.writeEvent({
625
+ event: 'progress',
626
+ phase: event.phase,
627
+ message: event.message,
628
+ elapsed_ms: Date.now() - context.startedAt,
629
+ });
630
+ },
631
+ });
632
+ if (context.outputMode !== 'human') {
633
+ context.output.writeData(result, initAgentWriteOptions(result));
634
+ }
635
+ return { exitCode: 0, apiKey: result.apiKey };
636
+ },
637
+ },
638
+ {
639
+ name: 'auth',
640
+ description: 'Authenticate and inspect browser auth sessions',
641
+ examples: ['moda auth login', 'moda auth whoami', 'moda auth logout'],
642
+ auth: 'none',
643
+ outputModes: ['human', 'json', 'agent'],
644
+ mutability: 'write',
645
+ validateConfigBeforeRun: false,
646
+ resetApiRequestCountBeforeRun: false,
647
+ telemetry: 'none',
648
+ handler: runAuthCommand,
649
+ },
650
+ {
651
+ name: 'status',
652
+ description: 'Show concise local Moda setup status',
653
+ examples: ['moda status', 'moda status --json'],
654
+ auth: 'none',
655
+ outputModes: ['human', 'json', 'agent'],
656
+ mutability: 'read',
657
+ validateConfigBeforeRun: false,
658
+ resetApiRequestCountBeforeRun: false,
659
+ telemetry: 'none',
660
+ handler: runStatusCommand,
661
+ },
662
+ {
663
+ name: 'doctor',
664
+ description: 'Diagnose local Moda setup and data flow',
665
+ examples: ['moda doctor', 'moda doctor --json', 'moda doctor --offline'],
666
+ auth: 'none',
667
+ outputModes: ['human', 'json', 'agent'],
668
+ mutability: 'read',
669
+ validateConfigBeforeRun: false,
670
+ resetApiRequestCountBeforeRun: false,
671
+ telemetry: 'none',
672
+ handler: runDoctorCommand,
673
+ },
674
+ {
675
+ name: 'profiles',
676
+ description: 'Manage local Moda profiles',
677
+ examples: ['moda profiles list', 'moda profiles use staging'],
678
+ auth: 'none',
679
+ outputModes: ['human', 'json', 'agent'],
680
+ mutability: 'write',
681
+ validateConfigBeforeRun: false,
682
+ resetApiRequestCountBeforeRun: false,
683
+ telemetry: 'none',
684
+ handler: runProfilesCommand,
685
+ },
686
+ {
687
+ name: 'config',
688
+ description: 'Show resolved local Moda configuration',
689
+ examples: ['moda config show', 'moda config show --profile=prod'],
690
+ auth: 'none',
691
+ outputModes: ['human', 'json', 'agent'],
692
+ mutability: 'read',
693
+ validateConfigBeforeRun: false,
694
+ resetApiRequestCountBeforeRun: false,
695
+ telemetry: 'none',
696
+ handler: runConfigCommand,
697
+ },
698
+ {
699
+ name: 'keys',
700
+ description: 'Manage local key descriptors and cloud key lifecycle requirements',
701
+ examples: ['moda keys list', 'moda keys create --name=local-dev'],
702
+ auth: 'none',
703
+ outputModes: ['human', 'json', 'agent'],
704
+ mutability: 'write',
705
+ validateConfigBeforeRun: false,
706
+ resetApiRequestCountBeforeRun: false,
707
+ telemetry: 'none',
708
+ handler: runKeysCommand,
709
+ },
710
+ {
711
+ name: 'harness',
712
+ description: 'Analyze, validate, approve, sync, and explain the local multi-agent harness graph',
713
+ examples: [
714
+ 'moda harness analyze --summary',
715
+ 'moda harness approve --yes',
716
+ 'moda harness sync --from-report .moda/harness-report.json',
717
+ 'moda harness explain --agent=agent_support',
718
+ ],
719
+ auth: 'none',
720
+ outputModes: ['human', 'json', 'agent'],
721
+ mutability: 'write',
722
+ validateConfigBeforeRun: false,
723
+ resetApiRequestCountBeforeRun: false,
724
+ telemetry: 'none',
725
+ handler: runHarnessCommand,
726
+ },
727
+ {
728
+ name: 'agent',
729
+ description: 'Agent-native Moda context, investigations, next actions, and evidence',
730
+ examples: [
731
+ 'moda agent context',
732
+ 'moda agent investigate',
733
+ 'moda agent next-action',
734
+ 'moda agent evidence doctor:harness_report_approved_synced',
735
+ ],
736
+ auth: 'none',
737
+ outputModes: ['agent', 'agent-stream', 'json'],
738
+ mutability: 'read',
739
+ validateConfigBeforeRun: false,
740
+ resetApiRequestCountBeforeRun: false,
741
+ telemetry: 'none',
742
+ handler: runAgentCommand,
743
+ },
744
+ {
745
+ name: 'sync',
746
+ description: 'Sync local Moda artifacts to Moda Cloud',
747
+ examples: [
748
+ 'moda sync --dry-run',
749
+ 'moda sync harness --from-report .moda/harness-report.json',
750
+ 'moda sync prompts',
751
+ ],
752
+ auth: 'none',
753
+ outputModes: ['human', 'json', 'agent'],
754
+ mutability: 'write',
755
+ validateConfigBeforeRun: false,
756
+ resetApiRequestCountBeforeRun: false,
757
+ telemetry: 'none',
758
+ handler: runSyncCommand,
759
+ },
760
+ legacyCommand({
761
+ name: 'overview',
762
+ description: 'Harness health briefing from production data',
763
+ examples: ['moda overview --days-back=30', 'moda --agent overview'],
764
+ ...dataApiDefaults,
765
+ }),
766
+ legacyCommand({
767
+ name: 'investigate',
768
+ description: 'Rank production issues with evidence and next commands',
769
+ examples: ['moda investigate', 'moda investigate --tool lookupCustomer'],
770
+ ...dataApiDefaults,
771
+ }),
772
+ legacyCommand({
773
+ name: 'failures',
774
+ description: 'Show production failures worth fixing first',
775
+ examples: ['moda failures', 'moda failures --tool lookupCustomer'],
776
+ ...dataApiDefaults,
777
+ }),
778
+ legacyCommand({
779
+ name: 'why-failed',
780
+ description: 'Explain a failed or partial production run',
781
+ examples: ['moda why-failed 12345678-1234-1234-1234-1234567890ab'],
782
+ ...dataApiDefaults,
783
+ }),
784
+ legacyCommand({
785
+ name: 'ask',
786
+ description: 'Ask Moda a production intelligence question',
787
+ examples: ['moda ask "what should I fix first?"', 'moda --agent ask "why did this run fail?"'],
788
+ ...dataApiDefaults,
789
+ }),
790
+ legacyCommand({
791
+ name: 'clusters',
792
+ description: 'Browse topic cluster hierarchy',
793
+ examples: ['moda clusters --time-range=7d'],
794
+ ...dataApiDefaults,
795
+ }),
796
+ legacyCommand({
797
+ name: 'cluster-conversations',
798
+ description: 'List conversations in a cluster',
799
+ examples: ['moda cluster-conversations <node_id> --limit=20'],
800
+ ...dataApiDefaults,
801
+ }),
802
+ legacyCommand({
803
+ name: 'conversations',
804
+ description: 'Search and filter conversations',
805
+ examples: ['moda conversations --search="error" --limit=5'],
806
+ ...dataApiDefaults,
807
+ }),
808
+ legacyCommand({
809
+ name: 'context',
810
+ description: 'Get windowed conversation context',
811
+ examples: ['moda context <conversation_id> --window=3'],
812
+ ...dataApiDefaults,
813
+ }),
814
+ legacyCommand({
815
+ name: 'frustrations',
816
+ description: 'Get user frustration detections',
817
+ examples: ['moda frustrations --days-back=14 --include-window'],
818
+ ...dataApiDefaults,
819
+ }),
820
+ legacyCommand({
821
+ name: 'tool-failures',
822
+ description: 'Get tool failure overview',
823
+ examples: ['moda tool-failures --days-back=14'],
824
+ ...dataApiDefaults,
825
+ }),
826
+ legacyCommand({
827
+ name: 'tool-failure-detail',
828
+ description: 'Get per-tool failure detail',
829
+ examples: ['moda tool-failure-detail search --include-window'],
830
+ ...dataApiDefaults,
831
+ }),
832
+ legacyCommand({
833
+ name: 'workflows',
834
+ description: 'Recurring workflow catalog',
835
+ examples: ['moda workflows --time-range=7d'],
836
+ ...dataApiDefaults,
837
+ }),
838
+ legacyCommand({
839
+ name: 'runs',
840
+ description: 'List workflow executions',
841
+ examples: ['moda runs --time-range=24h --outcome=partial'],
842
+ ...dataApiDefaults,
843
+ }),
844
+ legacyCommand({
845
+ name: 'run',
846
+ description: 'Get a single workflow execution',
847
+ examples: ['moda run 12345678-1234-1234-1234-1234567890ab'],
848
+ ...dataApiDefaults,
849
+ }),
850
+ legacyCommand({
851
+ name: 'loops',
852
+ description: 'List currently-active loop detections',
853
+ examples: ['moda loops --limit=10'],
854
+ ...dataApiDefaults,
855
+ }),
856
+ legacyCommand({
857
+ name: 'loop',
858
+ description: 'Get a single loop detection',
859
+ examples: ['moda loop 12345678-1234-1234-1234-1234567890ab'],
860
+ ...dataApiDefaults,
861
+ }),
862
+ legacyCommand({
863
+ name: 'prompts',
864
+ description: 'Manage prompt versions and labels',
865
+ examples: ['moda prompts status', 'moda prompts sync'],
866
+ auth: 'api-key',
867
+ outputModes: ['human', 'json', 'agent'],
868
+ mutability: 'write',
869
+ validateConfigBeforeRun: true,
870
+ resetApiRequestCountBeforeRun: true,
871
+ telemetry: 'result-and-error',
872
+ }),
873
+ {
874
+ name: 'workspace',
875
+ description: 'Manage Moda shadow workspace snapshots',
876
+ examples: ['moda workspace init', 'moda workspace snapshot --json', 'moda workspace explain'],
877
+ auth: 'none',
878
+ outputModes: ['human', 'json', 'agent'],
879
+ mutability: 'write',
880
+ validateConfigBeforeRun: false,
881
+ resetApiRequestCountBeforeRun: false,
882
+ telemetry: 'none',
883
+ handler: async (context) => ({
884
+ exitCode: await runWorkspaceCommand({
885
+ command: context.command,
886
+ positional: context.positional,
887
+ positionals: context.positionals,
888
+ flags: context.flags,
889
+ }),
890
+ }),
891
+ },
892
+ {
893
+ name: 'spark',
894
+ aliases: ['shadow'],
895
+ description: 'Manage Shadow Spark workspace snapshots',
896
+ examples: ['moda spark status', 'moda spark explain', 'moda spark snapshot --dry-run --json'],
897
+ auth: 'none',
898
+ outputModes: ['human', 'json', 'agent'],
899
+ mutability: 'write',
900
+ validateConfigBeforeRun: false,
901
+ resetApiRequestCountBeforeRun: false,
902
+ telemetry: 'none',
903
+ handler: runSparkCommand,
904
+ },
905
+ {
906
+ name: 'hooks',
907
+ description: 'Install coding-agent trace hooks',
908
+ examples: ['moda hooks install', 'moda hooks install --agent=all'],
909
+ auth: 'none',
910
+ outputModes: ['human', 'json', 'agent'],
911
+ mutability: 'write',
912
+ validateConfigBeforeRun: false,
913
+ resetApiRequestCountBeforeRun: false,
914
+ telemetry: 'none',
915
+ handler: async (context) => ({
916
+ exitCode: await runHooksInstall({
917
+ command: context.command,
918
+ positional: context.positional,
919
+ positionals: context.positionals,
920
+ flags: context.flags,
921
+ }),
922
+ }),
923
+ },
924
+ {
925
+ name: 'skills',
926
+ description: 'Generate and pull Moda skills',
927
+ examples: ['moda skills gen', 'moda skills pull'],
928
+ auth: 'none',
929
+ outputModes: ['human', 'json', 'agent'],
930
+ mutability: 'write',
931
+ validateConfigBeforeRun: false,
932
+ resetApiRequestCountBeforeRun: false,
933
+ telemetry: 'result',
934
+ handler: async (context) => ({
935
+ exitCode: await runSkillsCommand({
936
+ command: context.command,
937
+ positional: context.positional,
938
+ positionals: context.positionals,
939
+ flags: context.flags,
940
+ }),
941
+ }),
942
+ },
943
+ ]);
358
944
  // --- Main ---
359
- async function main() {
360
- const { command, positional, flags } = parseArgs(process.argv);
945
+ export function classifyCliError(error) {
946
+ if (error instanceof z.ZodError || error instanceof CliInputError) {
947
+ return 'validation_error';
948
+ }
949
+ if (error instanceof ApiError) {
950
+ return 'api_error';
951
+ }
952
+ if (error instanceof Error &&
953
+ (error.message.startsWith('Network error:') ||
954
+ error.message.startsWith('Request timed out'))) {
955
+ return 'network_error';
956
+ }
957
+ return 'unknown_error';
958
+ }
959
+ export function printCliError(error) {
960
+ if (error instanceof CliInputError) {
961
+ console.error(`Error: ${error.message}`);
962
+ if (error.usage)
963
+ console.error(error.usage);
964
+ }
965
+ else if (error instanceof z.ZodError) {
966
+ const issues = error.errors.map((e) => {
967
+ const field = e.path.length ? e.path.join('.') : 'input';
968
+ return ` ${field}: ${e.message}`;
969
+ });
970
+ console.error(`Validation error:\n${issues.join('\n')}`);
971
+ }
972
+ else if (error instanceof ApiError) {
973
+ console.error(`API error (HTTP ${error.statusCode}): ${error.responseBody || error.message}`);
974
+ }
975
+ else if (error instanceof Error) {
976
+ console.error(`Error: ${error.message}`);
977
+ }
978
+ else {
979
+ console.error('Error:', error);
980
+ }
981
+ }
982
+ function printAgentCliError(command, error) {
983
+ console.log(JSON.stringify(createAgentErrorEnvelope(command || 'unknown', errorToAgentError(error))));
984
+ }
985
+ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
986
+ const { command, flags } = parsed;
987
+ const commandFlags = stripTelemetryFlag(flags);
988
+ const telemetryDisabled = isTelemetryDisabled(flags);
361
989
  if (command === '--version' || command === '-V' || flags.version === 'true') {
362
990
  console.log(`moda-cli ${VERSION}`);
363
- process.exit(0);
991
+ return 0;
364
992
  }
365
- if (!command || command === '--help' || command === '-h' || flags.help === 'true') {
993
+ if (command === '--help' || command === '-h' || flags.help === 'true') {
366
994
  printHelp();
367
- process.exit(command ? 0 : 1);
995
+ return 0;
368
996
  }
369
- // init does NOT require MODA_API_KEY — that's what it creates
370
- if (command === 'init') {
371
- const { runInit } = await import('./init/index.js');
372
- const initFlags = flagsToArgs(flags);
373
- await runInit(initFlags);
374
- return;
997
+ if (!command) {
998
+ printHelp();
999
+ return 1;
1000
+ }
1001
+ const definition = commandRegistry.find(command);
1002
+ if (!definition) {
1003
+ const error = new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
1004
+ if (outputModeFromFlags(commandFlags) === 'agent' || outputModeFromFlags(commandFlags) === 'agent-stream') {
1005
+ printAgentCliError(command, error);
1006
+ }
1007
+ else {
1008
+ printCliError(error);
1009
+ }
1010
+ return 1;
1011
+ }
1012
+ if (definition.name === 'agent' && commandFlags.json !== 'true') {
1013
+ commandFlags.agent = 'true';
1014
+ }
1015
+ if (definition.validateConfigBeforeRun) {
1016
+ validateConfig();
1017
+ }
1018
+ if (definition.resetApiRequestCountBeforeRun) {
1019
+ resetApiRequestCount();
1020
+ }
1021
+ const context = createCommandContext(parsed, {
1022
+ commandName: definition.name,
1023
+ commandFlags,
1024
+ rawFlags: flags,
1025
+ startedAt,
1026
+ });
1027
+ try {
1028
+ const result = await definition.handler(context);
1029
+ if (!telemetryDisabled && definition.telemetry !== 'none') {
1030
+ void sendCliUsageTelemetry({
1031
+ apiKey: result.apiKey ?? resolveApiKey(),
1032
+ command: definition.name,
1033
+ flags: commandFlags,
1034
+ status: result.exitCode === 0 ? 'success' : 'error',
1035
+ exitCode: result.exitCode,
1036
+ durationMs: Date.now() - startedAt,
1037
+ apiRequestCount: getApiRequestCount(),
1038
+ });
1039
+ }
1040
+ return result.exitCode;
1041
+ }
1042
+ catch (error) {
1043
+ if (!telemetryDisabled && definition.telemetry === 'result-and-error') {
1044
+ void sendCliUsageTelemetry({
1045
+ apiKey: resolveApiKey(),
1046
+ command: definition.name,
1047
+ flags: commandFlags,
1048
+ status: 'error',
1049
+ errorType: classifyCliError(error),
1050
+ exitCode: 1,
1051
+ durationMs: Date.now() - startedAt,
1052
+ apiRequestCount: getApiRequestCount(),
1053
+ });
1054
+ }
1055
+ if (context.outputMode === 'agent' || context.outputMode === 'agent-stream') {
1056
+ printAgentCliError(definition.name, error);
1057
+ }
1058
+ else {
1059
+ printCliError(error);
1060
+ }
1061
+ return 1;
375
1062
  }
376
- validateConfig();
377
- await runCommand(command, positional, flags);
1063
+ }
1064
+ async function main() {
1065
+ // Set exitCode (don't process.exit) so any in-flight telemetry request
1066
+ // can resolve before Node drains its event loop and exits naturally.
1067
+ process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
378
1068
  }
379
1069
  // Only run main() when this file is executed directly (not when imported as a module).
380
1070
  import { fileURLToPath } from 'url';
@@ -383,23 +1073,8 @@ const isMain = process.argv[1] &&
383
1073
  fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
384
1074
  if (isMain) {
385
1075
  main().catch((error) => {
386
- if (error instanceof z.ZodError) {
387
- const issues = error.errors.map((e) => {
388
- const field = e.path.length ? e.path.join('.') : 'input';
389
- return ` ${field}: ${e.message}`;
390
- });
391
- console.error(`Validation error:\n${issues.join('\n')}`);
392
- }
393
- else if (error instanceof ApiError) {
394
- console.error(`API error (HTTP ${error.statusCode}): ${error.responseBody || error.message}`);
395
- }
396
- else if (error instanceof Error) {
397
- console.error(`Error: ${error.message}`);
398
- }
399
- else {
400
- console.error('Error:', error);
401
- }
402
- process.exit(1);
1076
+ printCliError(error);
1077
+ process.exitCode = 1;
403
1078
  });
404
1079
  }
405
1080
  //# sourceMappingURL=cli.js.map