@moda-ai/cli 1.10.2 → 1.11.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.
package/dist/output.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CliAuthError, CliCancellationError, CliInputError, CliInputRequiredError, isApiLikeError, isNetworkLikeError, } from './errors.js';
3
+ import { redactDeep } from './redact.js';
1
4
  export const AGENT_SCHEMA_VERSION = 'moda.agent.v1';
2
5
  export const AGENT_EVENT_SCHEMA_VERSION = 'moda.agent_event.v1';
3
6
  /**
@@ -5,12 +8,100 @@ export const AGENT_EVENT_SCHEMA_VERSION = 'moda.agent_event.v1';
5
8
  * 0 ok — grounded answer from Moda Cloud intelligence.
6
9
  * 3 degraded — answer produced from a local/degraded fallback (lower trust).
7
10
  * 1 error — command/input/unexpected failure (see envelope `errors[]`).
11
+ * 4 auth — login or token renewal required before retrying.
12
+ * 5 input — non-interactive caller must choose and re-invoke.
13
+ * 130 cancel — interrupted or the output consumer closed the pipe.
8
14
  * `EXIT_DEGRADED` is intentionally distinct so a caller can tell a degraded
9
15
  * answer apart from both success and hard failure without parsing the payload.
10
16
  */
11
17
  export const EXIT_OK = 0;
12
18
  export const EXIT_ERROR = 1;
13
19
  export const EXIT_DEGRADED = 3;
20
+ export const EXIT_AUTH_REQUIRED = 4;
21
+ export const EXIT_INPUT_REQUIRED = 5;
22
+ export const EXIT_CANCELLED = 130;
23
+ export const AGENT_ERROR_CODES = [
24
+ 'input_error',
25
+ 'api_error',
26
+ 'network_error',
27
+ 'auth_required',
28
+ 'input_required',
29
+ 'cancelled',
30
+ 'command_error',
31
+ 'unknown_error',
32
+ 'cloud_unavailable',
33
+ ];
34
+ const FALLBACK_RUN_ID = randomUUID();
35
+ let fallbackSeq = 0;
36
+ let stdoutClosed = false;
37
+ let stderrClosed = false;
38
+ process.stdout.on('error', (error) => {
39
+ if (error.code === 'EPIPE') {
40
+ stdoutClosed = true;
41
+ process.exitCode = EXIT_CANCELLED;
42
+ return;
43
+ }
44
+ throw error;
45
+ });
46
+ process.stderr.on('error', (error) => {
47
+ if (error.code === 'EPIPE') {
48
+ stderrClosed = true;
49
+ process.exitCode = EXIT_CANCELLED;
50
+ return;
51
+ }
52
+ throw error;
53
+ });
54
+ /**
55
+ * True once an EPIPE from a closed stdout/stderr consumer has been observed.
56
+ * The stream error listeners above own this state; expose it so callers can
57
+ * detect consumer-cancelled runs explicitly instead of sniffing the global
58
+ * `process.exitCode`.
59
+ */
60
+ export function wasOutputCancelled() {
61
+ return stdoutClosed || stderrClosed;
62
+ }
63
+ export function safeWriteStdout(text) {
64
+ if (stdoutClosed)
65
+ return false;
66
+ try {
67
+ if (process.env.NODE_ENV === 'test') {
68
+ console.log(text.endsWith('\n') ? text.slice(0, -1) : text);
69
+ }
70
+ else {
71
+ process.stdout.write(text);
72
+ }
73
+ return true;
74
+ }
75
+ catch (error) {
76
+ if (error.code === 'EPIPE') {
77
+ stdoutClosed = true;
78
+ process.exitCode = EXIT_CANCELLED;
79
+ return false;
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ export function safeWriteStderr(text) {
85
+ if (stderrClosed)
86
+ return false;
87
+ try {
88
+ if (process.env.NODE_ENV === 'test') {
89
+ console.error(text.endsWith('\n') ? text.slice(0, -1) : text);
90
+ }
91
+ else {
92
+ process.stderr.write(text);
93
+ }
94
+ return true;
95
+ }
96
+ catch (error) {
97
+ if (error.code === 'EPIPE') {
98
+ stderrClosed = true;
99
+ process.exitCode = EXIT_CANCELLED;
100
+ return false;
101
+ }
102
+ throw error;
103
+ }
104
+ }
14
105
  /**
15
106
  * Output mode requested by explicit flags only, or `undefined` when the caller
16
107
  * did not pick one. Explicit flags always win over auto-detection.
@@ -75,80 +166,198 @@ export function resolveOutputMode(flags, opts = {}) {
75
166
  return 'human';
76
167
  }
77
168
  export function createCliOutput(options) {
78
- return {
169
+ const state = {
170
+ runId: options.runId ?? randomUUID(),
171
+ seq: 0,
172
+ started: false,
173
+ terminal: false,
174
+ startedAt: Date.now(),
175
+ lastEventAt: Date.now(),
176
+ };
177
+ const emitEvent = (event) => {
178
+ if (options.quiet || options.mode !== 'agent-stream')
179
+ return;
180
+ if (state.terminal)
181
+ return;
182
+ if (event.event === 'started' && state.started)
183
+ return;
184
+ if (event.event === 'started')
185
+ state.started = true;
186
+ if (isTerminalEvent(event.event)) {
187
+ state.terminal = true;
188
+ clearHeartbeat(state);
189
+ }
190
+ state.lastEventAt = Date.now();
191
+ safeWriteStdout(`${JSON.stringify(createAgentEvent(options.command, event, state))}\n`);
192
+ };
193
+ const ensureStarted = () => {
194
+ if (options.mode !== 'agent-stream' || state.started || options.quiet)
195
+ return;
196
+ startHeartbeat(options.command, state);
197
+ emitEvent({
198
+ event: 'started',
199
+ phase: options.command,
200
+ message: `Starting ${options.command}.`,
201
+ elapsed_ms: 0,
202
+ });
203
+ };
204
+ const writeEnvelope = (data, writeOptions = {}) => {
205
+ return createAgentEnvelope({
206
+ command: options.command,
207
+ status: writeOptions.status === 'error'
208
+ ? 'error'
209
+ : writeOptions.status === 'degraded'
210
+ ? 'degraded'
211
+ : 'ok',
212
+ data,
213
+ meta: {
214
+ ...(writeOptions.meta ?? {}),
215
+ run_id: state.runId,
216
+ },
217
+ summary: writeOptions.summary,
218
+ findings: writeOptions.findings,
219
+ evidenceRefs: writeOptions.evidenceRefs,
220
+ recommendedActions: writeOptions.recommendedActions,
221
+ nextCommands: writeOptions.nextCommands,
222
+ artifacts: writeOptions.artifacts,
223
+ warnings: writeOptions.warnings,
224
+ errors: writeOptions.errors,
225
+ });
226
+ };
227
+ const output = {
79
228
  mode: options.mode,
80
229
  quiet: options.quiet === true,
81
230
  writeData(data, writeOptions = {}) {
82
231
  if (options.quiet)
83
232
  return;
84
233
  if (options.mode === 'agent' || options.mode === 'agent-stream') {
85
- const envelope = createAgentEnvelope({
86
- command: options.command,
87
- status: writeOptions.status === 'error'
88
- ? 'error'
89
- : writeOptions.status === 'degraded'
90
- ? 'degraded'
91
- : 'ok',
92
- data,
93
- meta: writeOptions.meta,
94
- summary: writeOptions.summary,
95
- findings: writeOptions.findings,
96
- evidenceRefs: writeOptions.evidenceRefs,
97
- recommendedActions: writeOptions.recommendedActions,
98
- nextCommands: writeOptions.nextCommands,
99
- artifacts: writeOptions.artifacts,
100
- warnings: writeOptions.warnings,
101
- errors: writeOptions.errors,
102
- });
234
+ if (state.terminal)
235
+ return;
236
+ const envelope = writeEnvelope(data, writeOptions);
103
237
  if (options.mode === 'agent-stream') {
104
- console.log(JSON.stringify(createAgentEvent(options.command, {
238
+ ensureStarted();
239
+ emitEvent({
105
240
  event: envelope.status === 'error' ? 'error' : 'completed',
106
241
  status: envelope.status,
107
242
  result: envelope,
108
- })));
243
+ ...(envelope.errors[0] ? { error: envelope.errors[0] } : {}),
244
+ });
109
245
  }
110
246
  else {
111
- console.log(JSON.stringify(envelope));
247
+ state.terminal = true;
248
+ safeWriteStdout(`${JSON.stringify(envelope)}\n`);
112
249
  }
113
250
  return;
114
251
  }
115
- console.log(JSON.stringify(data, null, 2));
252
+ safeWriteStdout(`${JSON.stringify(redactDeep(data), null, 2)}\n`);
116
253
  },
117
254
  writeEvent(event) {
118
255
  if (options.quiet)
119
256
  return;
120
257
  if (options.mode !== 'agent-stream')
121
258
  return;
122
- console.log(JSON.stringify(createAgentEvent(options.command, event)));
259
+ if (event.event === 'started') {
260
+ startHeartbeat(options.command, state);
261
+ emitEvent(event);
262
+ return;
263
+ }
264
+ ensureStarted();
265
+ emitEvent(event);
266
+ },
267
+ writeError(error) {
268
+ if (options.quiet || state.terminal)
269
+ return;
270
+ const agentError = errorToAgentError(error);
271
+ const envelope = createAgentErrorEnvelope(options.command || 'unknown', agentError, state.runId);
272
+ if (options.mode === 'agent-stream') {
273
+ ensureStarted();
274
+ if (error instanceof CliInputRequiredError) {
275
+ emitEvent({
276
+ event: 'needs_input',
277
+ status: 'error',
278
+ message: error.message,
279
+ error: agentError,
280
+ input_request: error.input_request,
281
+ result: envelope,
282
+ });
283
+ return;
284
+ }
285
+ if (error instanceof CliAuthError) {
286
+ emitEvent({
287
+ event: 'auth_required',
288
+ status: 'error',
289
+ message: error.message,
290
+ error: agentError,
291
+ auth: error.auth,
292
+ result: envelope,
293
+ });
294
+ return;
295
+ }
296
+ if (error instanceof CliCancellationError) {
297
+ emitEvent({
298
+ event: 'cancelled',
299
+ status: 'error',
300
+ message: error.message,
301
+ error: agentError,
302
+ result: envelope,
303
+ });
304
+ return;
305
+ }
306
+ emitEvent({
307
+ event: 'error',
308
+ status: 'error',
309
+ message: agentError.message,
310
+ error: agentError,
311
+ result: envelope,
312
+ });
313
+ return;
314
+ }
315
+ if (options.mode === 'agent' || options.mode === 'json') {
316
+ state.terminal = true;
317
+ safeWriteStdout(`${JSON.stringify(envelope)}\n`);
318
+ }
319
+ },
320
+ writeCancelled(reason = 'Command cancelled.') {
321
+ output.writeError(new CliCancellationError(reason));
322
+ },
323
+ hasTerminalOutput() {
324
+ return state.terminal;
325
+ },
326
+ close() {
327
+ clearHeartbeat(state);
123
328
  },
124
329
  };
330
+ return output;
125
331
  }
126
332
  export function createAgentEnvelope(options) {
127
- const bounded = boundForAgent(options.data);
128
- return {
333
+ const safeData = redactDeep(options.data);
334
+ const bounded = boundForAgent(safeData);
335
+ const envelope = {
129
336
  schema_version: AGENT_SCHEMA_VERSION,
130
337
  command: options.command,
131
338
  status: options.status,
132
339
  generated_at: new Date().toISOString(),
133
340
  ...(options.profile ? { profile: options.profile } : {}),
134
- summary: normalizeSummary(options.summary, options.command, options.status, options.data),
341
+ summary: normalizeSummary(options.summary, options.command, options.status, safeData),
135
342
  findings: options.findings ?? [],
136
343
  evidence_refs: options.evidenceRefs ?? [],
137
344
  recommended_actions: options.recommendedActions ?? [],
138
345
  next_commands: normalizeNextCommands(options.nextCommands ?? []),
139
346
  artifacts: options.artifacts ?? [],
140
- warnings: options.warnings ?? inferWarnings(options.data),
347
+ warnings: options.warnings ?? inferWarnings(safeData),
141
348
  errors: options.errors ?? [],
142
349
  data: bounded.data,
143
350
  ...(options.meta ? { meta: options.meta } : {}),
144
351
  ...(bounded.truncation ? { truncation: bounded.truncation } : {}),
145
352
  };
353
+ return redactDeep(envelope);
146
354
  }
147
- export function createAgentErrorEnvelope(command, error) {
355
+ export function createAgentErrorEnvelope(command, error, runId) {
148
356
  return createAgentEnvelope({
149
357
  command,
150
358
  status: 'error',
151
359
  data: null,
360
+ meta: runId ? { run_id: runId } : undefined,
152
361
  summary: {
153
362
  text: error.message,
154
363
  confidence: 'high',
@@ -162,18 +371,81 @@ export function createAgentErrorEnvelope(command, error) {
162
371
  })),
163
372
  });
164
373
  }
165
- export function createAgentEvent(command, event) {
166
- return {
374
+ export function createAgentEvent(command, event, state) {
375
+ const stamped = {
167
376
  schema_version: AGENT_EVENT_SCHEMA_VERSION,
168
377
  command,
169
378
  generated_at: new Date().toISOString(),
379
+ run_id: state?.runId ?? FALLBACK_RUN_ID,
380
+ seq: state ? ++state.seq : ++fallbackSeq,
381
+ event_version: event.event_version ?? 1,
170
382
  ...event,
171
383
  };
384
+ return redactDeep(stamped);
172
385
  }
173
386
  export function errorToAgentError(error) {
387
+ if (error instanceof CliInputRequiredError) {
388
+ return {
389
+ code: 'input_required',
390
+ message: error.message,
391
+ cause: error.usage,
392
+ suggested_commands: error.input_request.resume_command
393
+ ? [error.input_request.resume_command.join(' ')]
394
+ : undefined,
395
+ input_request: error.input_request,
396
+ blocking: true,
397
+ };
398
+ }
399
+ if (error instanceof CliAuthError) {
400
+ const suggested = [error.auth.login_command.join(' ')];
401
+ if (error.auth.resume_command)
402
+ suggested.push(error.auth.resume_command.join(' '));
403
+ return {
404
+ code: 'auth_required',
405
+ message: error.message,
406
+ cause: error.usage,
407
+ suggested_commands: suggested,
408
+ auth: error.auth,
409
+ blocking: true,
410
+ };
411
+ }
412
+ if (error instanceof CliCancellationError) {
413
+ return {
414
+ code: 'cancelled',
415
+ message: error.message,
416
+ blocking: true,
417
+ };
418
+ }
174
419
  if (error instanceof Error) {
420
+ if (error instanceof CliInputError || error.name === 'ZodError') {
421
+ return {
422
+ code: 'input_error',
423
+ message: error.message,
424
+ cause: 'usage' in error && typeof error.usage === 'string' ? error.usage : undefined,
425
+ suggested_commands: 'usage' in error && typeof error.usage === 'string'
426
+ ? extractSuggestedCommands(error.usage)
427
+ : undefined,
428
+ blocking: true,
429
+ };
430
+ }
431
+ if (isApiLikeError(error)) {
432
+ const statusCode = 'statusCode' in error ? error.statusCode : undefined;
433
+ return {
434
+ code: 'api_error',
435
+ message: error.message,
436
+ cause: typeof statusCode === 'number' ? `HTTP ${statusCode}` : undefined,
437
+ blocking: true,
438
+ };
439
+ }
440
+ if (isNetworkLikeError(error)) {
441
+ return {
442
+ code: 'network_error',
443
+ message: error.message,
444
+ blocking: true,
445
+ };
446
+ }
175
447
  return {
176
- code: error.name === 'CliInputError' ? 'input_error' : 'command_error',
448
+ code: 'command_error',
177
449
  message: error.message,
178
450
  cause: 'usage' in error && typeof error.usage === 'string' ? error.usage : undefined,
179
451
  suggested_commands: 'usage' in error && typeof error.usage === 'string'
@@ -188,6 +460,39 @@ export function errorToAgentError(error) {
188
460
  blocking: true,
189
461
  };
190
462
  }
463
+ function isTerminalEvent(event) {
464
+ return event === 'completed' ||
465
+ event === 'error' ||
466
+ event === 'cancelled' ||
467
+ event === 'needs_input' ||
468
+ event === 'auth_required';
469
+ }
470
+ function startHeartbeat(command, state) {
471
+ if (state.heartbeat)
472
+ return;
473
+ const intervalMs = Number(process.env.MODA_HEARTBEAT_MS ?? 10_000);
474
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0)
475
+ return;
476
+ state.heartbeat = setInterval(() => {
477
+ if (state.terminal || stdoutClosed)
478
+ return;
479
+ const now = Date.now();
480
+ if (now - state.lastEventAt < intervalMs)
481
+ return;
482
+ state.lastEventAt = now;
483
+ safeWriteStdout(`${JSON.stringify(createAgentEvent(command, {
484
+ event: 'heartbeat',
485
+ elapsed_ms: now - state.startedAt,
486
+ }, state))}\n`);
487
+ }, intervalMs);
488
+ state.heartbeat.unref();
489
+ }
490
+ function clearHeartbeat(state) {
491
+ if (!state.heartbeat)
492
+ return;
493
+ clearInterval(state.heartbeat);
494
+ state.heartbeat = undefined;
495
+ }
191
496
  function normalizeSummary(summary, command, status, data) {
192
497
  if (typeof summary === 'string') {
193
498
  return { text: summary, confidence: 'high' };
package/dist/prompts.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { type CommandContext } from './command-context.js';
1
2
  import type { ProfileIOOptions } from './profiles.js';
2
- export declare function runPromptsCommand(positionals: string[], flags: Record<string, string>, profileOptions?: ProfileIOOptions): Promise<void>;
3
+ export declare function runPromptsCommand(input: CommandContext | string[], legacyFlags?: Record<string, string>, legacyProfileOptions?: ProfileIOOptions): Promise<void>;
3
4
  export declare function initPrompts(options?: {
4
5
  silent?: boolean;
5
6
  }): Promise<void>;
package/dist/prompts.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
4
4
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { callControlAPI } from './api-client.js';
6
+ import { createCommandContext } from './command-context.js';
6
7
  const MODA_DIR = '.moda';
7
8
  const MANIFEST_PATH = join(MODA_DIR, 'prompts.yml');
8
9
  const LOCK_PATH = join(MODA_DIR, 'prompts.lock.json');
@@ -12,23 +13,43 @@ const DEFAULT_PROMPT_PATHS = [
12
13
  'prompts/**/*.prompt.yaml',
13
14
  'prompts/**/*.prompt.yml',
14
15
  ];
15
- export async function runPromptsCommand(positionals, flags, profileOptions = { profile: flags.profile }) {
16
+ export async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
17
+ const context = Array.isArray(input)
18
+ ? createCommandContext({
19
+ command: 'prompts',
20
+ positional: input[0],
21
+ positionals: input,
22
+ flags: legacyFlags ?? {},
23
+ }, {
24
+ commandFlags: legacyFlags ?? {},
25
+ outputMode: legacyFlags?.agent === 'true' ? 'agent' : legacyFlags?.json === 'true' ? 'json' : 'human',
26
+ env: legacyProfileOptions?.env,
27
+ })
28
+ : input;
29
+ const positionals = context.positionals;
30
+ const flags = context.flags;
31
+ const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
16
32
  const subcommand = positionals[0] || 'status';
17
33
  switch (subcommand) {
18
34
  case 'init':
19
- await initPrompts();
35
+ await initPrompts({ silent: true });
36
+ context.output.writeData({
37
+ manifest: MANIFEST_PATH,
38
+ lockfile: LOCK_PATH,
39
+ promptPaths: readManifest().promptPaths,
40
+ });
20
41
  break;
21
42
  case 'status':
22
- printJson(await getPromptStatus());
43
+ context.output.writeData(await getPromptStatus());
23
44
  break;
24
45
  case 'diff':
25
- printJson(await getPromptStatus());
46
+ context.output.writeData(await getPromptStatus());
26
47
  break;
27
48
  case 'sync':
28
- await syncPrompts(flags, profileOptions);
49
+ await syncPrompts(flags, profileOptions, context);
29
50
  break;
30
51
  case 'promote':
31
- await promotePrompt(positionals, flags, profileOptions);
52
+ await promotePrompt(positionals, flags, profileOptions, context);
32
53
  break;
33
54
  default:
34
55
  throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, or promote.`);
@@ -52,12 +73,12 @@ export async function initPrompts(options = {}) {
52
73
  });
53
74
  }
54
75
  }
55
- async function syncPrompts(flags, profileOptions) {
76
+ async function syncPrompts(flags, profileOptions, context) {
56
77
  if (flags.watch === 'true') {
57
78
  await watchPromptSync(flags, profileOptions);
58
79
  return;
59
80
  }
60
- await syncPromptsOnce(flags, profileOptions);
81
+ await syncPromptsOnce(flags, profileOptions, context);
61
82
  }
62
83
  /**
63
84
  * Sync the manifest's prompt files to the backend and update the lockfile.
@@ -88,12 +109,16 @@ export async function runPromptSync(flags, profileOptions) {
88
109
  }
89
110
  return { synced: response.synced ?? [], dryRun };
90
111
  }
91
- async function syncPromptsOnce(flags, profileOptions) {
112
+ async function syncPromptsOnce(flags, profileOptions, context) {
92
113
  const result = await runPromptSync(flags, profileOptions);
93
- printJson({
114
+ const payload = {
94
115
  synced: result.synced,
95
116
  lockfile: result.dryRun ? undefined : LOCK_PATH,
96
- });
117
+ };
118
+ if (context)
119
+ context.output.writeData(payload);
120
+ else
121
+ printJson(payload);
97
122
  }
98
123
  async function watchPromptSync(flags, profileOptions) {
99
124
  console.error('Watching prompt files. Press Ctrl+C to stop.');
@@ -111,7 +136,7 @@ async function watchPromptSync(flags, profileOptions) {
111
136
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
112
137
  }
113
138
  }
114
- async function promotePrompt(positionals, flags, profileOptions) {
139
+ async function promotePrompt(positionals, flags, profileOptions, context) {
115
140
  const key = positionals[1];
116
141
  const label = flags.label;
117
142
  const versionId = flags.version || flags['version-id'];
@@ -125,7 +150,7 @@ async function promotePrompt(positionals, flags, profileOptions) {
125
150
  method: 'POST',
126
151
  body: JSON.stringify({ label, versionId }),
127
152
  }, profileOptions);
128
- printJson(response);
153
+ context.output.writeData(response);
129
154
  }
130
155
  async function getPromptStatus() {
131
156
  await initPrompts({ silent: true });
@@ -0,0 +1,2 @@
1
+ export declare function redactDeep<T>(value: T): T;
2
+ //# sourceMappingURL=redact.d.ts.map
package/dist/redact.js ADDED
@@ -0,0 +1,81 @@
1
+ const REDACTED = '[REDACTED]';
2
+ // Pure stack-safety bound. Cycles are handled separately by the `seen` WeakSet,
3
+ // so this only needs to be high enough that legitimate nested payloads (the
4
+ // agent-envelope wrapper plus arbitrary customer tool-call JSON) are never
5
+ // silently truncated to '[MaxDepth]'. 256 is far below any real recursion limit.
6
+ const MAX_DEPTH = 256;
7
+ const SECRET_KEYS = new Set([
8
+ 'api_key',
9
+ 'apikey',
10
+ 'token',
11
+ 'access_token',
12
+ 'authorization',
13
+ 'secret',
14
+ 'password',
15
+ 'cli_token',
16
+ ]);
17
+ const SECRET_PATTERNS = [
18
+ { pattern: /\bmoda_sk_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
19
+ { pattern: /\bmoda_ct_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
20
+ { pattern: /\bBearer\s+([A-Za-z0-9._~+/-]+=*)\b/gi, prefixLength: 8 },
21
+ { pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
22
+ ];
23
+ export function redactDeep(value) {
24
+ return redactValue(value, 0, new WeakSet());
25
+ }
26
+ function redactValue(value, depth, seen) {
27
+ if (typeof value === 'string')
28
+ return redactString(value);
29
+ if (value === null || typeof value !== 'object')
30
+ return value;
31
+ if (depth >= MAX_DEPTH)
32
+ return '[MaxDepth]';
33
+ if (seen.has(value))
34
+ return '[Circular]';
35
+ seen.add(value);
36
+ if (Array.isArray(value)) {
37
+ try {
38
+ return value.map((item) => redactValue(item, depth + 1, seen));
39
+ }
40
+ finally {
41
+ seen.delete(value);
42
+ }
43
+ }
44
+ const out = {};
45
+ try {
46
+ for (const [key, item] of Object.entries(value)) {
47
+ out[key] = shouldRedactKey(key) ? redactSecretValue(item) : redactValue(item, depth + 1, seen);
48
+ }
49
+ }
50
+ finally {
51
+ seen.delete(value);
52
+ }
53
+ return out;
54
+ }
55
+ function shouldRedactKey(key) {
56
+ return SECRET_KEYS.has(key.replace(/[-\s]/g, '_').toLowerCase());
57
+ }
58
+ function redactSecretValue(value) {
59
+ if (typeof value !== 'string')
60
+ return REDACTED;
61
+ const redacted = redactString(value);
62
+ if (redacted !== value)
63
+ return redacted;
64
+ if (/^(configured|missing|not_configured|none|present|absent)$/i.test(value))
65
+ return value;
66
+ return REDACTED;
67
+ }
68
+ function redactString(input) {
69
+ let output = input;
70
+ for (const { pattern, prefixLength } of SECRET_PATTERNS) {
71
+ output = output.replace(pattern, (match) => {
72
+ if (/^Bearer\s+/i.test(match)) {
73
+ const token = match.replace(/^Bearer\s+/i, '');
74
+ return `Bearer ${token.slice(0, prefixLength)}...[REDACTED]`;
75
+ }
76
+ return `${match.slice(0, prefixLength)}...[REDACTED]`;
77
+ });
78
+ }
79
+ return output;
80
+ }
81
+ //# sourceMappingURL=redact.js.map
package/dist/schemas.d.ts CHANGED
@@ -126,20 +126,20 @@ export declare const ToolFailureDetailSchema: z.ZodObject<{
126
126
  window: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
127
127
  }, "strip", z.ZodTypeAny, {
128
128
  tool_name: string;
129
+ subtype?: string | undefined;
129
130
  days_back?: number | undefined;
130
131
  limit?: number | undefined;
131
132
  offset?: number | undefined;
132
133
  window?: number | undefined;
133
134
  include_window?: boolean | undefined;
134
- subtype?: string | undefined;
135
135
  }, {
136
136
  tool_name: string;
137
+ subtype?: string | undefined;
137
138
  days_back?: number | undefined;
138
139
  limit?: number | undefined;
139
140
  offset?: number | undefined;
140
141
  window?: number | undefined;
141
142
  include_window?: unknown;
142
- subtype?: string | undefined;
143
143
  }>;
144
144
  /**
145
145
  * moda audit <conversation_id|trace_id> — raw, non-deduped span/trace read.