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