@moda-ai/cli 1.9.0 → 1.10.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.
@@ -0,0 +1,3 @@
1
+ export declare const AGENT_JSON_SCHEMAS: Record<string, unknown>;
2
+ export declare function renderJsonSchemas(): string;
3
+ //# sourceMappingURL=agent-schemas.d.ts.map
@@ -0,0 +1,298 @@
1
+ // agent-schemas.ts
2
+ //
3
+ // Machine-readable JSON Schemas (JSON Schema draft-07) for the moda CLI's
4
+ // agent output contract. It is consumed by `moda --json-schemas`, which prints
5
+ // the document produced by `renderJsonSchemas()` so an external coding agent can
6
+ // introspect the exact shape of moda's agent-mode output.
7
+ //
8
+ // The canonical `moda.intelligence.v1` schema is imported from its
9
+ // single-source-of-truth module rather than re-declared here, so the published
10
+ // contract can never drift from the runtime payload.
11
+ //
12
+ // Keep these schemas in sync with:
13
+ // - AgentEnvelope / AgentEvent in ./output.ts (moda.agent.v1 / moda.agent_event.v1)
14
+ // - ProductionAskResult in ./production-intelligence.ts (production_ask.v0.1)
15
+ // - ModaIntelligenceV1 in ./intelligence-schema.ts (moda.intelligence.v1)
16
+ import { MODA_INTELLIGENCE_V1_JSON_SCHEMA } from './intelligence-schema.js';
17
+ const agentSummarySchema = {
18
+ type: 'object',
19
+ additionalProperties: false,
20
+ required: ['text', 'confidence'],
21
+ properties: {
22
+ text: { type: 'string' },
23
+ confidence: { type: 'string', enum: ['high', 'medium', 'low', 'unknown'] },
24
+ },
25
+ };
26
+ const agentFindingSchema = {
27
+ type: 'object',
28
+ additionalProperties: false,
29
+ required: ['id', 'title'],
30
+ properties: {
31
+ id: { type: 'string' },
32
+ title: { type: 'string' },
33
+ status: { type: 'string' },
34
+ severity: { type: 'string' },
35
+ description: { type: 'string' },
36
+ evidence_ref_ids: { type: 'array', items: { type: 'string' } },
37
+ },
38
+ };
39
+ const agentEvidenceRefSchema = {
40
+ type: 'object',
41
+ additionalProperties: false,
42
+ required: ['id', 'kind', 'label'],
43
+ properties: {
44
+ id: { type: 'string' },
45
+ kind: { type: 'string' },
46
+ label: { type: 'string' },
47
+ path: { type: 'string' },
48
+ line: { type: 'number' },
49
+ status: { type: 'string' },
50
+ },
51
+ };
52
+ const agentRecommendedActionSchema = {
53
+ type: 'object',
54
+ additionalProperties: false,
55
+ required: ['id', 'title', 'mutability', 'requires_approval'],
56
+ properties: {
57
+ id: { type: 'string' },
58
+ title: { type: 'string' },
59
+ rationale: { type: 'string' },
60
+ command: { type: 'string' },
61
+ mutability: { type: 'string', enum: ['read', 'write', 'sync', 'unknown'] },
62
+ requires_approval: { type: 'boolean' },
63
+ },
64
+ };
65
+ const agentNextCommandSchema = {
66
+ type: 'object',
67
+ additionalProperties: false,
68
+ required: ['command', 'purpose', 'mutability', 'requires_approval'],
69
+ properties: {
70
+ command: { type: 'string' },
71
+ purpose: { type: 'string' },
72
+ mutability: { type: 'string', enum: ['read', 'write', 'sync', 'unknown'] },
73
+ requires_approval: { type: 'boolean' },
74
+ },
75
+ };
76
+ const agentArtifactSchema = {
77
+ type: 'object',
78
+ additionalProperties: false,
79
+ required: ['kind', 'path'],
80
+ properties: {
81
+ kind: { type: 'string' },
82
+ path: { type: 'string' },
83
+ description: { type: 'string' },
84
+ },
85
+ };
86
+ const agentErrorSchema = {
87
+ type: 'object',
88
+ additionalProperties: false,
89
+ required: ['code', 'message', 'blocking'],
90
+ properties: {
91
+ code: { type: 'string' },
92
+ message: { type: 'string' },
93
+ cause: { type: 'string' },
94
+ suggested_commands: { type: 'array', items: { type: 'string' } },
95
+ blocking: { type: 'boolean' },
96
+ },
97
+ };
98
+ const agentEnvelopeSchema = {
99
+ $id: 'moda.agent.v1',
100
+ title: 'AgentEnvelope',
101
+ description: 'Top-level agent-mode output envelope emitted by the moda CLI.',
102
+ type: 'object',
103
+ additionalProperties: false,
104
+ required: [
105
+ 'schema_version',
106
+ 'command',
107
+ 'status',
108
+ 'generated_at',
109
+ 'summary',
110
+ 'findings',
111
+ 'evidence_refs',
112
+ 'recommended_actions',
113
+ 'next_commands',
114
+ 'artifacts',
115
+ 'warnings',
116
+ 'errors',
117
+ 'data',
118
+ ],
119
+ properties: {
120
+ schema_version: { type: 'string', const: 'moda.agent.v1' },
121
+ command: { type: 'string' },
122
+ status: { type: 'string', enum: ['ok', 'degraded', 'error'] },
123
+ generated_at: { type: 'string', format: 'date-time' },
124
+ profile: { type: 'string' },
125
+ summary: agentSummarySchema,
126
+ findings: { type: 'array', items: agentFindingSchema },
127
+ evidence_refs: { type: 'array', items: agentEvidenceRefSchema },
128
+ recommended_actions: { type: 'array', items: agentRecommendedActionSchema },
129
+ next_commands: { type: 'array', items: agentNextCommandSchema },
130
+ artifacts: { type: 'array', items: agentArtifactSchema },
131
+ warnings: { type: 'array', items: { type: 'string' } },
132
+ errors: { type: 'array', items: agentErrorSchema },
133
+ data: { description: 'Command-specific payload. Shape varies by command.', additionalProperties: true },
134
+ meta: { type: 'object', additionalProperties: true },
135
+ truncation: { type: 'object', additionalProperties: true },
136
+ },
137
+ };
138
+ const agentEventSchema = {
139
+ $id: 'moda.agent_event.v1',
140
+ title: 'AgentEvent',
141
+ description: 'Streaming NDJSON event emitted in agent-stream mode by the moda CLI.',
142
+ type: 'object',
143
+ additionalProperties: false,
144
+ required: ['schema_version', 'event', 'command', 'generated_at'],
145
+ properties: {
146
+ schema_version: { type: 'string', const: 'moda.agent_event.v1' },
147
+ event: {
148
+ type: 'string',
149
+ enum: ['started', 'progress', 'artifact', 'approval_required', 'completed', 'error'],
150
+ },
151
+ command: { type: 'string' },
152
+ generated_at: { type: 'string', format: 'date-time' },
153
+ status: { type: 'string', enum: ['ok', 'degraded', 'error'] },
154
+ phase: { type: 'string' },
155
+ message: { type: 'string' },
156
+ elapsed_ms: { type: 'number' },
157
+ artifact: agentArtifactSchema,
158
+ result: agentEnvelopeSchema,
159
+ error: agentErrorSchema,
160
+ },
161
+ };
162
+ const productionEvidenceRefSchema = {
163
+ type: 'object',
164
+ additionalProperties: false,
165
+ required: ['id', 'kind', 'label'],
166
+ properties: {
167
+ id: { type: 'string' },
168
+ kind: {
169
+ type: 'string',
170
+ enum: ['data_api', 'tool_failure', 'frustration', 'loop', 'run', 'harness', 'ask'],
171
+ },
172
+ label: { type: 'string' },
173
+ endpoint: { type: 'string' },
174
+ conversationId: { type: 'string' },
175
+ runId: { type: 'string' },
176
+ detectionId: { type: 'string' },
177
+ toolName: { type: 'string' },
178
+ path: { type: 'string' },
179
+ line: { type: 'number' },
180
+ value: { description: 'Arbitrary evidence payload.', additionalProperties: true },
181
+ },
182
+ };
183
+ const likelyLocationSchema = {
184
+ type: 'object',
185
+ additionalProperties: false,
186
+ required: ['kind', 'label', 'confidence', 'reason'],
187
+ properties: {
188
+ kind: { type: 'string', enum: ['tool', 'prompt', 'skill', 'entrypoint', 'harness', 'unknown'] },
189
+ path: { type: 'string' },
190
+ label: { type: 'string' },
191
+ confidence: { type: 'string', enum: ['high', 'medium', 'low', 'unknown'] },
192
+ reason: { type: 'string' },
193
+ },
194
+ };
195
+ const productionActionSchema = {
196
+ type: 'object',
197
+ additionalProperties: false,
198
+ required: ['id', 'title', 'rationale', 'command', 'mutability', 'requiresApproval'],
199
+ properties: {
200
+ id: { type: 'string' },
201
+ title: { type: 'string' },
202
+ rationale: { type: 'string' },
203
+ command: { type: 'string' },
204
+ mutability: { type: 'string', enum: ['read', 'write', 'sync', 'unknown'] },
205
+ requiresApproval: { type: 'boolean' },
206
+ },
207
+ };
208
+ const productionFindingSchema = {
209
+ type: 'object',
210
+ additionalProperties: false,
211
+ required: [
212
+ 'id',
213
+ 'kind',
214
+ 'title',
215
+ 'severity',
216
+ 'confidence',
217
+ 'impactScore',
218
+ 'rankReason',
219
+ 'summary',
220
+ 'evidenceRefIds',
221
+ 'likelyLocations',
222
+ 'recommendedActions',
223
+ ],
224
+ properties: {
225
+ id: { type: 'string' },
226
+ kind: {
227
+ type: 'string',
228
+ enum: [
229
+ 'tool_failure',
230
+ 'loop',
231
+ 'frustration',
232
+ 'partial_run',
233
+ 'prompt_regression',
234
+ 'skill_gap',
235
+ 'insufficient_data',
236
+ ],
237
+ },
238
+ title: { type: 'string' },
239
+ severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'] },
240
+ confidence: { type: 'string', enum: ['high', 'medium', 'low', 'unknown'] },
241
+ impactScore: { type: 'number' },
242
+ rankReason: { type: 'string' },
243
+ summary: { type: 'string' },
244
+ evidenceRefIds: { type: 'array', items: { type: 'string' } },
245
+ likelyLocations: { type: 'array', items: likelyLocationSchema },
246
+ recommendedActions: { type: 'array', items: productionActionSchema },
247
+ },
248
+ };
249
+ const productionAskResultSchema = {
250
+ $id: 'production_ask.v0.1',
251
+ title: 'ProductionAskResult',
252
+ description: 'Result payload for `moda ask`, sourced from cloud intelligence or local fallback.',
253
+ type: 'object',
254
+ additionalProperties: false,
255
+ required: [
256
+ 'schemaVersion',
257
+ 'generatedAt',
258
+ 'source',
259
+ 'question',
260
+ 'answer',
261
+ 'confidence',
262
+ 'evidenceRefs',
263
+ 'findings',
264
+ 'nextCommands',
265
+ 'warnings',
266
+ 'degraded',
267
+ ],
268
+ properties: {
269
+ schemaVersion: { type: 'string', const: 'production_ask.v0.1' },
270
+ generatedAt: { type: 'string', format: 'date-time' },
271
+ source: { type: 'string', enum: ['cloud', 'local_fallback'] },
272
+ question: { type: 'string' },
273
+ answer: { type: 'string' },
274
+ confidence: { type: 'string', enum: ['high', 'medium', 'low', 'unknown'] },
275
+ evidenceRefs: { type: 'array', items: productionEvidenceRefSchema },
276
+ findings: { type: 'array', items: productionFindingSchema },
277
+ nextCommands: { type: 'array', items: { type: 'string' } },
278
+ warnings: { type: 'array', items: { type: 'string' } },
279
+ degraded: { type: 'boolean' },
280
+ },
281
+ };
282
+ export const AGENT_JSON_SCHEMAS = {
283
+ 'moda.agent.v1': agentEnvelopeSchema,
284
+ 'moda.agent_event.v1': agentEventSchema,
285
+ // Canonical unified payload (MODA-388). Now the shape of the ask command's
286
+ // agent `data`. The legacy `production_ask.v0.1` schema is kept for
287
+ // back-compat with older agent integrations.
288
+ 'moda.intelligence.v1': MODA_INTELLIGENCE_V1_JSON_SCHEMA,
289
+ 'production_ask.v0.1': productionAskResultSchema,
290
+ };
291
+ export function renderJsonSchemas() {
292
+ return JSON.stringify({
293
+ $schema: 'http://json-schema.org/draft-07/schema#',
294
+ generated_by: 'moda-cli',
295
+ schemas: AGENT_JSON_SCHEMAS,
296
+ }, null, 2);
297
+ }
298
+ //# sourceMappingURL=agent-schemas.js.map
@@ -5,7 +5,18 @@ export declare class ApiError extends Error {
5
5
  constructor(message: string, statusCode: number, responseBody: string);
6
6
  }
7
7
  export declare function callDataAPI(endpoint: string): Promise<unknown>;
8
- export declare function callControlAPI(endpoint: string, options?: RequestInit, profileOptions?: ProfileIOOptions): Promise<unknown>;
8
+ /**
9
+ * Per-call transport overrides. Used by expensive, long-running, non-idempotent
10
+ * endpoints (e.g. the agent `ask` POST) that need a longer timeout than the
11
+ * default and must NOT be retried on timeout.
12
+ */
13
+ export interface ModaRequestOptions {
14
+ /** Abort timeout in ms for each attempt (default REQUEST_TIMEOUT_MS). */
15
+ timeoutMs?: number;
16
+ /** Max retries (default MAX_RETRIES). Set 0 to disable retries entirely. */
17
+ retries?: number;
18
+ }
19
+ export declare function callControlAPI(endpoint: string, options?: RequestInit, profileOptions?: ProfileIOOptions, requestOptions?: ModaRequestOptions): Promise<unknown>;
9
20
  export declare function getApiRequestCount(): number;
10
21
  export declare function resetApiRequestCount(): void;
11
22
  //# sourceMappingURL=api-client.d.ts.map
@@ -22,19 +22,21 @@ export async function callDataAPI(endpoint) {
22
22
  const url = `${resolveModaBaseUrl()}/api/v1/data${endpoint}`;
23
23
  return callModaAPI(url, { method: 'GET' });
24
24
  }
25
- export async function callControlAPI(endpoint, options = {}, profileOptions = {}) {
25
+ export async function callControlAPI(endpoint, options = {}, profileOptions = {}, requestOptions = {}) {
26
26
  const url = `${resolveModaBaseUrl(profileOptions)}/api${endpoint}`;
27
- return callModaAPI(url, options, profileOptions);
27
+ return callModaAPI(url, options, profileOptions, requestOptions);
28
28
  }
29
- async function callModaAPI(url, options, profileOptions = {}) {
29
+ async function callModaAPI(url, options, profileOptions = {}, requestOptions = {}) {
30
+ const timeoutMs = requestOptions.timeoutMs ?? REQUEST_TIMEOUT_MS;
31
+ const maxRetries = requestOptions.retries ?? MAX_RETRIES;
30
32
  let lastError;
31
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
33
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
32
34
  if (attempt > 0) {
33
35
  const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
34
36
  await new Promise((resolve) => setTimeout(resolve, backoff));
35
37
  }
36
38
  const controller = new AbortController();
37
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
39
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
38
40
  try {
39
41
  const response = await fetch(url, {
40
42
  ...options,
@@ -48,7 +50,7 @@ async function callModaAPI(url, options, profileOptions = {}) {
48
50
  if (!response.ok) {
49
51
  const errorText = await response.text();
50
52
  const error = new ApiError(`API error (${response.status}): ${errorText}`, response.status, errorText);
51
- if (isRetryable(response.status) && attempt < MAX_RETRIES) {
53
+ if (isRetryable(response.status) && attempt < maxRetries) {
52
54
  lastError = error;
53
55
  continue;
54
56
  }
@@ -61,9 +63,9 @@ async function callModaAPI(url, options, profileOptions = {}) {
61
63
  throw error;
62
64
  const isAbort = error instanceof DOMException && error.name === 'AbortError';
63
65
  const wrapped = new Error(isAbort
64
- ? `Request timed out after ${REQUEST_TIMEOUT_MS}ms: ${url}`
66
+ ? `Request timed out after ${timeoutMs}ms: ${url}`
65
67
  : `Network error: ${error instanceof Error ? error.message : String(error)}`);
66
- if (attempt < MAX_RETRIES) {
68
+ if (attempt < maxRetries) {
67
69
  lastError = wrapped;
68
70
  continue;
69
71
  }
package/dist/auth.js CHANGED
@@ -312,12 +312,44 @@ export async function authFetch(path, options = {}, authOptions = {}) {
312
312
  if (!token) {
313
313
  throw new Error('Not authenticated. Run "moda init" to log in.');
314
314
  }
315
- const url = `${resolveModaBaseUrl()}${path}`;
316
315
  const headers = new Headers(options.headers);
317
316
  headers.set('Authorization', `Bearer ${token}`);
318
317
  if (!headers.has('Content-Type') && options.body) {
319
318
  headers.set('Content-Type', 'application/json');
320
319
  }
321
- return fetch(url, { ...options, headers });
320
+ const origin = new URL(resolveModaBaseUrl()).origin;
321
+ let url = `${resolveModaBaseUrl()}${path}`;
322
+ // Follow redirects manually so EVERY hop is origin-checked, not just the
323
+ // first. Native `redirect: 'follow'` would silently strip the Authorization
324
+ // header on a cross-host hop (per the Fetch spec) — the exact failure that
325
+ // broke moda init (the legacy www → apex 308). Same-host hops keep the header
326
+ // and are safe to follow; any hop that leaves the Moda origin fails fast with
327
+ // an actionable error instead of surfacing as a confusing 401.
328
+ const MAX_REDIRECTS = 5;
329
+ for (let hop = 0;; hop++) {
330
+ const response = await fetch(url, { ...options, headers, redirect: 'manual' });
331
+ if (response.status < 300 || response.status >= 400) {
332
+ return response;
333
+ }
334
+ const location = response.headers.get('location');
335
+ const target = location ? safeResolveUrl(location, url) : null;
336
+ if (!target || target.origin !== origin) {
337
+ throw new Error(`Moda API redirected (${response.status}) to ${location ?? '(unknown)'}. ` +
338
+ 'Cross-host redirects strip Authorization and break moda init. ' +
339
+ 'Use MODA_BASE_URL=https://moda.dev (not www.moda.dev).');
340
+ }
341
+ if (hop >= MAX_REDIRECTS) {
342
+ throw new Error(`Moda API exceeded ${MAX_REDIRECTS} same-host redirects for ${path}.`);
343
+ }
344
+ url = target.toString();
345
+ }
346
+ }
347
+ function safeResolveUrl(location, base) {
348
+ try {
349
+ return new URL(location, base);
350
+ }
351
+ catch {
352
+ return null;
353
+ }
322
354
  }
323
355
  //# sourceMappingURL=auth.js.map
package/dist/cli.d.ts CHANGED
@@ -11,7 +11,7 @@ export interface ParsedArgs {
11
11
  }
12
12
  export declare function parseArgs(argv: string[]): ParsedArgs;
13
13
  export declare function flagsToArgs(flags: Record<string, string>, positionalKey?: string, positionalValue?: string): Record<string, unknown>;
14
- export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>, positionals?: string[], context?: CommandContext): Promise<void>;
14
+ export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>, positionals?: string[], context?: CommandContext): Promise<number>;
15
15
  interface CommandDispatchResult {
16
16
  exitCode: number;
17
17
  apiKey?: string;
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { validateConfig, VERSION, resolveApiKey } from './config.js';
4
4
  import { callDataAPI, ApiError, getApiRequestCount, resetApiRequestCount, } from './api-client.js';
5
- import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
5
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
6
6
  import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
7
7
  import { runPromptsCommand } from './prompts.js';
8
8
  // Coding-agent capture (shadow workspace / Shadow Spark / trace hooks) is temporarily
@@ -17,7 +17,8 @@ import { buildSearchAdoptionTelemetry, isTelemetryDisabled, SEARCH_MODES, sendCl
17
17
  import { isUpdateCheckDisabled, printUpdateNotice, startUpdateCheck, stripUpdateCheckFlag, } from './update-check.js';
18
18
  import { createCommandContext } from './command-context.js';
19
19
  import { createCommandRegistry } from './command-registry.js';
20
- import { createAgentErrorEnvelope, errorToAgentError, outputModeFromFlags, } from './output.js';
20
+ import { createAgentErrorEnvelope, errorToAgentError, resolveOutputMode, } from './output.js';
21
+ import { renderJsonSchemas } from './agent-schemas.js';
21
22
  import { runAuthCommand, runConfigCommand, runKeysCommand, runProfilesCommand } from './profile-commands.js';
22
23
  import { runAgentCommand } from './agent.js';
23
24
  import { runProductionCommand } from './production-intelligence.js';
@@ -51,6 +52,9 @@ export function parseArgs(argv) {
51
52
  const GLOBAL_BOOLEAN_FLAGS = new Set([
52
53
  'agent',
53
54
  'json',
55
+ 'human',
56
+ 'prose',
57
+ 'json-schemas',
54
58
  'help',
55
59
  'version',
56
60
  'quiet',
@@ -140,6 +144,7 @@ Commands:
140
144
  search "<query>" Search conversation messages (keyword/semantic/hybrid)
141
145
  world-state <conversation_id> Get a conversation's world state (slots/threads/events)
142
146
  context <conversation_id> Get windowed conversation context
147
+ audit <conversation_id|trace_id> Raw span/trace audit (spans, hierarchy, orphans, duplicates)
143
148
  frustrations Get user frustration detections
144
149
  tool-failures Get tool failure overview
145
150
  tool-failure-detail <tool_name> Get per-tool failure detail
@@ -169,6 +174,7 @@ Commands:
169
174
  Options:
170
175
  --help Show this help message
171
176
  --version Show version number
177
+ --json-schemas Print JSON Schemas for the agent output contract
172
178
 
173
179
  Common flags:
174
180
  --days-back=N Number of days to look back (1-90)
@@ -179,6 +185,13 @@ Common flags:
179
185
  --json Emit machine-readable JSON
180
186
  --agent Emit schema-versioned agent JSON
181
187
  --stream With --agent, emit NDJSON agent events
188
+ --human Force human prose (overrides auto-detection)
189
+
190
+ Output auto-detection (no flags needed):
191
+ Piped/redirected stdout, CI, or an agent env (CLAUDECODE/CURSOR/MODA_AGENT)
192
+ => schema-versioned agent JSON. An interactive terminal => human prose.
193
+ Override with the flags above or MODA_FORMAT=human|json|agent|agent-stream.
194
+ Exit codes: 0 ok, 3 degraded (local fallback), 1 error.
182
195
  --dry-run Show the planned mutation when supported
183
196
  --yes Accept explicit automation defaults
184
197
  --tui Force the human analyst dashboard when supported
@@ -261,6 +274,8 @@ Examples:
261
274
  moda world-state <conversation_id> --summary-only
262
275
  moda frustrations --days-back=14 --limit=20
263
276
  moda context <conversation_id> --window=3
277
+ moda audit <conversation_id|trace_id>
278
+ moda audit <trace_id> --kind=trace --json
264
279
  moda workflows --time-range=7d
265
280
  moda runs --time-range=24h --outcome=partial
266
281
  moda runs --class-id=cron:helpscout-triage-hourly --limit=20
@@ -386,15 +401,13 @@ export async function runCommand(command, positional, flags, positionals = posit
386
401
  case 'overview': {
387
402
  const args = flagsToArgs(flags);
388
403
  OverviewSchema.parse(args);
389
- await runProductionCommand(context);
390
- break;
404
+ return (await runProductionCommand(context)).exitCode;
391
405
  }
392
406
  case 'investigate':
393
407
  case 'failures':
394
408
  case 'why-failed':
395
409
  case 'ask': {
396
- await runProductionCommand(context);
397
- break;
410
+ return (await runProductionCommand(context)).exitCode;
398
411
  }
399
412
  case 'clusters': {
400
413
  const args = flagsToArgs(flags);
@@ -519,6 +532,25 @@ export async function runCommand(command, positional, flags, positionals = posit
519
532
  context.output.writeData(data);
520
533
  break;
521
534
  }
535
+ case 'audit':
536
+ case 'trace': {
537
+ if (!positional) {
538
+ throw new CliInputError('<conversation_id|trace_id> is required', 'Usage: moda audit <conversation_id|trace_id> [--kind=auto|trace|conversation] [--include-raw] [--limit=N]');
539
+ }
540
+ const args = flagsToArgs(flags, 'id', positional);
541
+ const params = AuditSchema.parse(args);
542
+ const query = new URLSearchParams();
543
+ if (params.kind && params.kind !== 'auto')
544
+ query.set('kind', params.kind);
545
+ if (params.include_raw === true)
546
+ query.set('include_raw', '1');
547
+ if (params.limit)
548
+ query.set('limit', params.limit.toString());
549
+ const queryString = query.toString() ? `?${query.toString()}` : '';
550
+ const data = await callDataAPI(`/audit/${encodeURIComponent(params.id)}${queryString}`);
551
+ context.output.writeData(data);
552
+ break;
553
+ }
522
554
  case 'frustrations': {
523
555
  const args = flagsToArgs(flags);
524
556
  const params = FrustrationsSchema.parse(args);
@@ -631,13 +663,14 @@ export async function runCommand(command, positional, flags, positionals = posit
631
663
  default:
632
664
  throw new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
633
665
  }
666
+ return 0;
634
667
  }
635
668
  function legacyCommand(metadata) {
636
669
  return {
637
670
  ...metadata,
638
671
  handler: async (context) => {
639
- await runCommand(context.command, context.positional, context.flags, context.positionals, context);
640
- return { exitCode: 0 };
672
+ const exitCode = await runCommand(context.command, context.positional, context.flags, context.positionals, context);
673
+ return { exitCode };
641
674
  },
642
675
  };
643
676
  }
@@ -952,6 +985,17 @@ export const commandRegistry = createCommandRegistry([
952
985
  examples: ['moda context <conversation_id> --window=3'],
953
986
  ...dataApiDefaults,
954
987
  }),
988
+ legacyCommand({
989
+ name: 'audit',
990
+ aliases: ['trace'],
991
+ description: 'Raw non-deduped span/trace audit (hierarchy, tool spans, orphans, duplicates)',
992
+ examples: [
993
+ 'moda audit <conversation_id|trace_id>',
994
+ 'moda audit <trace_id> --kind=trace --json',
995
+ 'moda audit <conversation_id> --include-raw',
996
+ ],
997
+ ...dataApiDefaults,
998
+ }),
955
999
  legacyCommand({
956
1000
  name: 'frustrations',
957
1001
  description: 'Get user frustration detections',
@@ -1139,6 +1183,10 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1139
1183
  printHelp();
1140
1184
  return 0;
1141
1185
  }
1186
+ if (command === '--json-schemas' || flags['json-schemas'] === 'true') {
1187
+ console.log(renderJsonSchemas());
1188
+ return 0;
1189
+ }
1142
1190
  if (!command) {
1143
1191
  printHelp();
1144
1192
  return 1;
@@ -1146,7 +1194,11 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1146
1194
  const definition = commandRegistry.find(command);
1147
1195
  if (!definition) {
1148
1196
  const error = new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
1149
- if (outputModeFromFlags(commandFlags) === 'agent' || outputModeFromFlags(commandFlags) === 'agent-stream') {
1197
+ const mode = resolveOutputMode(commandFlags, {
1198
+ isTTY: process.stdout.isTTY,
1199
+ env: process.env,
1200
+ });
1201
+ if (mode === 'agent' || mode === 'agent-stream') {
1150
1202
  printAgentCliError(command, error);
1151
1203
  }
1152
1204
  else {
@@ -1173,6 +1225,13 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
1173
1225
  commandFlags,
1174
1226
  rawFlags: flags,
1175
1227
  startedAt,
1228
+ // Agent-safe output auto-selection happens here (not in createCommandContext)
1229
+ // so direct callers/tests keep flag-only behavior. We resolve from the real
1230
+ // stdout TTY + environment, leaving `commandFlags` (and telemetry) untouched.
1231
+ outputMode: resolveOutputMode(commandFlags, {
1232
+ isTTY: process.stdout.isTTY,
1233
+ env: process.env,
1234
+ }),
1176
1235
  });
1177
1236
  try {
1178
1237
  const result = await definition.handler(context);
@@ -45,5 +45,11 @@ export declare function createCommandContext(input: CommandContextInput, options
45
45
  startedAt?: number;
46
46
  cwd?: string;
47
47
  env?: NodeJS.ProcessEnv;
48
+ /**
49
+ * Pre-resolved output mode from the dispatcher (TTY/env auto-detection).
50
+ * When omitted we fall back to flag-only resolution so direct callers
51
+ * (tests, internal helpers) keep their explicit behavior.
52
+ */
53
+ outputMode?: CliOutputMode;
48
54
  }): CommandContext;
49
55
  //# sourceMappingURL=command-context.d.ts.map
@@ -5,7 +5,7 @@ function flagEnabled(flags, key) {
5
5
  export function createCommandContext(input, options = {}) {
6
6
  const flags = options.commandFlags ?? input.flags;
7
7
  const rawFlags = options.rawFlags ?? input.flags;
8
- const outputMode = outputModeFromFlags(flags);
8
+ const outputMode = options.outputMode ?? outputModeFromFlags(flags);
9
9
  const quiet = flagEnabled(flags, 'quiet');
10
10
  const interactive = outputMode === 'human' &&
11
11
  !quiet &&
package/dist/config.d.ts CHANGED
@@ -27,7 +27,8 @@ export declare function saveCliConfig(update: CliConfig): void;
27
27
  export declare function resolveApiKey(options?: ProfileIOOptions): string | undefined;
28
28
  /**
29
29
  * Resolve the ingest worker base URL used by `moda workspace snapshot`.
30
- * Env override -> persisted config -> production default.
30
+ * Env override -> active profile -> legacy config -> production default.
31
+ * Loopback URLs are rejected unless the active profile is `local`.
31
32
  */
32
33
  export declare function resolveIngestUrl(options?: ProfileIOOptions): string;
33
34
  /**