@moda-ai/cli 1.38.0 → 1.39.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/README.md +19 -0
- package/dist/{cli-3kcy61fr.js → cli-a7ypptsf.js} +515 -59
- package/dist/{cli-kegd748v.js → cli-dyeywy9d.js} +1 -1
- package/dist/{cli-q79sq80a.js → cli-rp912ypf.js} +163 -21
- package/dist/{cli-07tv75te.js → cli-xsmsjny3.js} +696 -139
- package/dist/{cli-k38j2fpq.js → cli-yy8fwg1a.js} +2 -2
- package/dist/cli.js +2522 -263
- package/dist/{harness-n2xgzz31.js → harness-191xt7bq.js} +2 -2
- package/dist/{harness-github-actions-qz5d22rz.js → harness-github-actions-1qt4sgxd.js} +2 -2
- package/dist/{index-hwfe7gs8.js → index-qgc37efj.js} +5 -5
- package/dist/prompt-source-mcp.js +2 -2
- package/dist/{provision-64c2n9aw.js → provision-gabtks29.js} +3 -3
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +239 -20
package/dist/cli.js
CHANGED
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
runPromptsCommand,
|
|
9
9
|
runSkillsCommand,
|
|
10
10
|
runStatusCommand
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-a7ypptsf.js";
|
|
12
12
|
import {
|
|
13
13
|
ApiError,
|
|
14
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
15
15
|
HARNESS_REPORT_PATH,
|
|
16
|
+
SIGNALS_MANAGEMENT_SUBCOMMANDS,
|
|
17
|
+
assertFlags,
|
|
16
18
|
callControlAPI,
|
|
17
19
|
callDataAPI,
|
|
18
20
|
getApiRequestCount,
|
|
@@ -24,12 +26,17 @@ import {
|
|
|
24
26
|
renderMarkdownForTerminal,
|
|
25
27
|
resetApiRequestCount,
|
|
26
28
|
runHarnessCommand,
|
|
29
|
+
runProblemReopenCommand,
|
|
30
|
+
runSignalsManagementCommand,
|
|
31
|
+
runUserCommand,
|
|
32
|
+
runUsersCommand,
|
|
27
33
|
shouldColorTerminalOutput,
|
|
28
34
|
stripAnsi,
|
|
29
35
|
summarizeHarness,
|
|
30
36
|
terminalStyles,
|
|
37
|
+
unknownSignalsSubcommandError,
|
|
31
38
|
validateHarnessReport
|
|
32
|
-
} from "./cli-
|
|
39
|
+
} from "./cli-xsmsjny3.js";
|
|
33
40
|
import"./cli-ssfq84k5.js";
|
|
34
41
|
import {
|
|
35
42
|
authFetch,
|
|
@@ -38,7 +45,7 @@ import {
|
|
|
38
45
|
loadAuthSession,
|
|
39
46
|
login,
|
|
40
47
|
revokeCliSessionToken
|
|
41
|
-
} from "./cli-
|
|
48
|
+
} from "./cli-dyeywy9d.js";
|
|
42
49
|
import {
|
|
43
50
|
AGENT_ERROR_CODES,
|
|
44
51
|
CliAccessError,
|
|
@@ -56,10 +63,12 @@ import {
|
|
|
56
63
|
MODA_BASE_URL,
|
|
57
64
|
PROD_BASE_URL,
|
|
58
65
|
VERSION,
|
|
66
|
+
agentEnvelopeFits,
|
|
59
67
|
assertValidProfileName,
|
|
60
68
|
clearStoredApiKeyCredentials,
|
|
61
69
|
createCliOutput,
|
|
62
70
|
createCommandContext,
|
|
71
|
+
describeBaseUrlSource,
|
|
63
72
|
embeddedTenantIdFromApiKey,
|
|
64
73
|
ensureSecretFilePermissions,
|
|
65
74
|
getActiveProfile,
|
|
@@ -70,6 +79,7 @@ import {
|
|
|
70
79
|
loadProfileConfig,
|
|
71
80
|
resolveApiKey,
|
|
72
81
|
resolveApiKeyFromActiveProfile,
|
|
82
|
+
resolveApiTenantId,
|
|
73
83
|
resolveIngestUrl,
|
|
74
84
|
resolveModaBaseUrl,
|
|
75
85
|
resolveOutputMode,
|
|
@@ -84,7 +94,7 @@ import {
|
|
|
84
94
|
validateConfig,
|
|
85
95
|
wasOutputCancelled,
|
|
86
96
|
writeSecretJsonFile
|
|
87
|
-
} from "./cli-
|
|
97
|
+
} from "./cli-rp912ypf.js";
|
|
88
98
|
import {
|
|
89
99
|
__require
|
|
90
100
|
} from "./cli-0v6na3yp.js";
|
|
@@ -121,6 +131,7 @@ var ConversationsSchema = z.object({
|
|
|
121
131
|
world_state: z.string().max(200).optional(),
|
|
122
132
|
outcome: z.enum(["any", "positive", "negative"]).default("any").optional(),
|
|
123
133
|
include_world_state: boolFlag(),
|
|
134
|
+
sort: z.enum(["recent", "longest", "slowest"]).optional(),
|
|
124
135
|
limit: z.number().min(1).max(100).default(20).optional(),
|
|
125
136
|
offset: z.number().min(0).default(0).optional()
|
|
126
137
|
});
|
|
@@ -139,14 +150,19 @@ var WorldStateSchema = z.object({
|
|
|
139
150
|
var ContextSchema = z.object({
|
|
140
151
|
conversation_id: z.string(),
|
|
141
152
|
msg_index: z.number().min(0).optional(),
|
|
142
|
-
window: z.number().min(1).max(5).default(2).optional()
|
|
153
|
+
window: z.number().min(1).max(5).default(2).optional(),
|
|
154
|
+
all: boolFlag(),
|
|
155
|
+
from: z.number().int().min(0).optional(),
|
|
156
|
+
max_messages: z.number().int().min(1).max(5000).optional()
|
|
143
157
|
});
|
|
144
158
|
var FrustrationsSchema = z.object({
|
|
145
159
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
146
160
|
limit: z.number().min(1).max(20).default(10).optional(),
|
|
147
161
|
offset: z.number().min(0).default(0).optional(),
|
|
148
162
|
include_window: boolFlag(),
|
|
149
|
-
window: z.number().min(1).max(5).default(1).optional()
|
|
163
|
+
window: z.number().min(1).max(5).default(1).optional(),
|
|
164
|
+
user_id: z.string().min(1).max(256).optional(),
|
|
165
|
+
conversation_id: z.string().min(1).max(256).optional()
|
|
150
166
|
});
|
|
151
167
|
var ToolFailuresSchema = z.object({
|
|
152
168
|
days_back: z.number().min(1).max(90).default(7).optional()
|
|
@@ -155,6 +171,21 @@ var ProblemsSchema = z.object({
|
|
|
155
171
|
days_back: z.number().min(1).max(90).default(30).optional(),
|
|
156
172
|
limit: z.number().min(1).max(25).default(25).optional()
|
|
157
173
|
});
|
|
174
|
+
var REMAINDER_FAMILIES = [
|
|
175
|
+
"tool_failure",
|
|
176
|
+
"emotion",
|
|
177
|
+
"laziness",
|
|
178
|
+
"hallucination",
|
|
179
|
+
"prm_dip",
|
|
180
|
+
"custom_signal",
|
|
181
|
+
"conversation_reader"
|
|
182
|
+
];
|
|
183
|
+
var ProblemsRemainderSchema = z.object({
|
|
184
|
+
days_back: z.number().min(1).max(90).default(30).optional(),
|
|
185
|
+
limit: z.number().min(1).max(25).default(25).optional(),
|
|
186
|
+
family: z.enum(REMAINDER_FAMILIES).optional(),
|
|
187
|
+
cursor: z.string().min(1).max(4096).optional()
|
|
188
|
+
});
|
|
158
189
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
159
190
|
var ProblemImpactSchema = z.object({
|
|
160
191
|
node_id: z.string().min(1).max(256).optional(),
|
|
@@ -179,13 +210,7 @@ var ProblemImpactSchema = z.object({
|
|
|
179
210
|
}).refine((p) => !p.cursor || p.mode === "evidence" && p.to && p.run_id, {
|
|
180
211
|
message: "An evidence cursor requires --mode=evidence, --to, and --run-id from the previous response"
|
|
181
212
|
});
|
|
182
|
-
var PROBLEM_FAMILIES =
|
|
183
|
-
"tool_failure",
|
|
184
|
-
"emotion",
|
|
185
|
-
"laziness",
|
|
186
|
-
"hallucination",
|
|
187
|
-
"prm_dip"
|
|
188
|
-
];
|
|
213
|
+
var PROBLEM_FAMILIES = REMAINDER_FAMILIES;
|
|
189
214
|
var PROBLEM_DOORS = ["rubric_match", "local_causal_chain"];
|
|
190
215
|
var ProblemSchema = z.object({
|
|
191
216
|
id: z.string().min(1),
|
|
@@ -286,6 +311,7 @@ var FalsePositiveSchema = z.object({
|
|
|
286
311
|
});
|
|
287
312
|
var EMOTION_FAMILIES = [
|
|
288
313
|
"frustration",
|
|
314
|
+
"disapproval",
|
|
289
315
|
"sadness",
|
|
290
316
|
"confusion",
|
|
291
317
|
"anxiety",
|
|
@@ -296,7 +322,9 @@ var EmotionsSchema = z.object({
|
|
|
296
322
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
297
323
|
limit: z.number().min(1).max(20).default(10).optional(),
|
|
298
324
|
offset: z.number().min(0).max(1e4).optional(),
|
|
299
|
-
family: z.enum(EMOTION_FAMILIES).optional()
|
|
325
|
+
family: z.enum(EMOTION_FAMILIES).optional(),
|
|
326
|
+
user_id: z.string().min(1).max(256).optional(),
|
|
327
|
+
conversation_id: z.string().min(1).max(256).optional()
|
|
300
328
|
});
|
|
301
329
|
var HallucinationsSchema = z.object({
|
|
302
330
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
@@ -305,12 +333,33 @@ var HallucinationsSchema = z.object({
|
|
|
305
333
|
conversation_id: z.string().max(200).optional(),
|
|
306
334
|
kind: z.enum(["contradicted", "verified"]).optional()
|
|
307
335
|
});
|
|
336
|
+
var LAZINESS_FAMILIES = [
|
|
337
|
+
"omission_comment",
|
|
338
|
+
"omission_phrase",
|
|
339
|
+
"stub_body",
|
|
340
|
+
"refusal_prefix",
|
|
341
|
+
"deflection",
|
|
342
|
+
"data_quality_truncation",
|
|
343
|
+
"promise_no_delivery",
|
|
344
|
+
"capability_refusal_here",
|
|
345
|
+
"capability_refusal_verb",
|
|
346
|
+
"error_admission",
|
|
347
|
+
"irreversibility",
|
|
348
|
+
"stall_recheck",
|
|
349
|
+
"diy_handoff",
|
|
350
|
+
"tool_selection",
|
|
351
|
+
"completion",
|
|
352
|
+
"code_artifact",
|
|
353
|
+
"repeated_tool_call",
|
|
354
|
+
"target_overshoot",
|
|
355
|
+
"data_regression"
|
|
356
|
+
];
|
|
308
357
|
var LazinessSchema = z.object({
|
|
309
358
|
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
310
359
|
limit: z.number().min(1).max(50).default(10).optional(),
|
|
311
360
|
offset: z.number().min(0).max(1e4).optional(),
|
|
312
361
|
conversation_id: z.string().max(200).optional(),
|
|
313
|
-
family: z.
|
|
362
|
+
family: z.enum(LAZINESS_FAMILIES).optional()
|
|
314
363
|
});
|
|
315
364
|
var StepScoresSchema = z.object({
|
|
316
365
|
conversation_id: z.string().min(1)
|
|
@@ -331,6 +380,7 @@ var ToolFailureDetailSchema = z.object({
|
|
|
331
380
|
limit: z.number().min(1).max(20).default(5).optional(),
|
|
332
381
|
offset: z.number().min(0).default(0).optional(),
|
|
333
382
|
include_window: boolFlag(),
|
|
383
|
+
compact: boolFlag(),
|
|
334
384
|
window: z.number().min(1).max(5).default(1).optional()
|
|
335
385
|
});
|
|
336
386
|
var AuditSchema = z.object({
|
|
@@ -347,7 +397,8 @@ var SearchSchema = z.object({
|
|
|
347
397
|
user_id: z.string().optional(),
|
|
348
398
|
time_range: z.enum(TIME_RANGES).default("all").optional(),
|
|
349
399
|
limit: z.number().min(1).max(100).default(20).optional(),
|
|
350
|
-
include_tool_io: boolFlag()
|
|
400
|
+
include_tool_io: boolFlag(),
|
|
401
|
+
offset: z.number().min(0).max(1e4).optional()
|
|
351
402
|
});
|
|
352
403
|
var FEEDBACK_CATEGORIES = [
|
|
353
404
|
"bad_cluster_label",
|
|
@@ -383,21 +434,27 @@ function asString(value) {
|
|
|
383
434
|
function asNumber(value) {
|
|
384
435
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
385
436
|
}
|
|
437
|
+
function frustrationAnchorIndex(row) {
|
|
438
|
+
if (!row)
|
|
439
|
+
return null;
|
|
440
|
+
const userQuotes = Array.isArray(row.user_quotes) ? row.user_quotes : [];
|
|
441
|
+
const firstQuote = asRecord(userQuotes[0]);
|
|
442
|
+
const resolved = asNumber(firstQuote?.msg_index) ?? asNumber(row.anchor_msg_index);
|
|
443
|
+
if (resolved !== null)
|
|
444
|
+
return resolved;
|
|
445
|
+
const keyTurns = Array.isArray(row.key_turns) ? row.key_turns : [];
|
|
446
|
+
const firstTurn = keyTurns.find((t) => typeof t === "number");
|
|
447
|
+
if (firstTurn !== undefined)
|
|
448
|
+
return firstTurn;
|
|
449
|
+
return asNumber(firstQuote?.turn);
|
|
450
|
+
}
|
|
386
451
|
function buildFrustrationAnchor(row) {
|
|
387
452
|
const conversation_id = asString(row.conversation_id) ?? "";
|
|
388
453
|
const userQuotes = Array.isArray(row.user_quotes) ? row.user_quotes : [];
|
|
389
454
|
const firstQuote = asRecord(userQuotes[0]);
|
|
390
455
|
const keyTurnsRaw = Array.isArray(row.key_turns) ? row.key_turns : [];
|
|
391
456
|
const all_turns = keyTurnsRaw.filter((t) => typeof t === "number");
|
|
392
|
-
|
|
393
|
-
if (all_turns.length > 0) {
|
|
394
|
-
msg_index = all_turns[0];
|
|
395
|
-
}
|
|
396
|
-
if (msg_index === null && firstQuote) {
|
|
397
|
-
const turn = asNumber(firstQuote.turn);
|
|
398
|
-
if (turn !== null)
|
|
399
|
-
msg_index = turn;
|
|
400
|
-
}
|
|
457
|
+
const msg_index = frustrationAnchorIndex(row);
|
|
401
458
|
let signal = null;
|
|
402
459
|
let quote_preview = null;
|
|
403
460
|
if (firstQuote) {
|
|
@@ -431,6 +488,43 @@ function buildToolFailureAnchor(row, toolName) {
|
|
|
431
488
|
no_anchor: msg_index === null
|
|
432
489
|
};
|
|
433
490
|
}
|
|
491
|
+
function frustrationsFromEmotions(response) {
|
|
492
|
+
if (!Array.isArray(response.detections) || Array.isArray(response.frustrations))
|
|
493
|
+
return response;
|
|
494
|
+
const summary = asRecord(response.summary) ?? {};
|
|
495
|
+
const pagination = asRecord(response.pagination) ?? {};
|
|
496
|
+
const byFamily = asRecord(summary.by_family) ?? {};
|
|
497
|
+
const totalAnalyzed = asNumber(summary.conversations_analyzed) ?? 0;
|
|
498
|
+
const frustrated = asNumber(byFamily.frustration) ?? 0;
|
|
499
|
+
const frustrations = response.detections.map((item) => asRecord(item)).filter((row) => row !== null && (row.family === undefined || row.family === "frustration")).map(({ conversation_context, score, is_detected, ...row }) => ({
|
|
500
|
+
...row,
|
|
501
|
+
is_frustrated: is_detected === true,
|
|
502
|
+
frustration_score: score,
|
|
503
|
+
target: row.elicitor === "bot" || row.elicitor === "external" ? row.elicitor : "ambiguous",
|
|
504
|
+
conversation: conversation_context ?? null
|
|
505
|
+
}));
|
|
506
|
+
const atRisk = asNumber(pagination.at_risk) ?? Math.max(0, (asNumber(pagination.total) ?? 0) - frustrated);
|
|
507
|
+
const signalBreakdown = {};
|
|
508
|
+
for (const item of Array.isArray(response.signal_breakdown) ? response.signal_breakdown : []) {
|
|
509
|
+
const row = asRecord(item);
|
|
510
|
+
if (row?.family === "frustration" && typeof row.signal === "string") {
|
|
511
|
+
signalBreakdown[row.signal] = asNumber(row.count) ?? 0;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const { detections: _detections, ...rest } = response;
|
|
515
|
+
return {
|
|
516
|
+
...rest,
|
|
517
|
+
summary: {
|
|
518
|
+
total_analyzed: totalAnalyzed,
|
|
519
|
+
frustrated_count: frustrated,
|
|
520
|
+
at_risk_count: atRisk,
|
|
521
|
+
frustration_rate_pct: totalAnalyzed > 0 ? Math.round(frustrated / totalAnalyzed * 1000) / 10 : 0
|
|
522
|
+
},
|
|
523
|
+
signal_breakdown: signalBreakdown,
|
|
524
|
+
frustrations,
|
|
525
|
+
source: "emotion_detections"
|
|
526
|
+
};
|
|
527
|
+
}
|
|
434
528
|
function withFrustrationAnchors(response) {
|
|
435
529
|
if (!Array.isArray(response.frustrations))
|
|
436
530
|
return response;
|
|
@@ -443,6 +537,76 @@ function withFrustrationAnchors(response) {
|
|
|
443
537
|
});
|
|
444
538
|
return { ...response, frustrations };
|
|
445
539
|
}
|
|
540
|
+
var TOOL_FAILURE_TEXT_LIMIT = 300;
|
|
541
|
+
var TOOL_FAILURE_CONTEXT_RADIUS = 2;
|
|
542
|
+
function shellWord(value) {
|
|
543
|
+
return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
544
|
+
}
|
|
545
|
+
function trimFailureWindow(value, radius, fallbackCenter) {
|
|
546
|
+
const conv = asRecord(value);
|
|
547
|
+
if (!conv)
|
|
548
|
+
return value ?? null;
|
|
549
|
+
const { coverage: _coverage, context, ...convRest } = conv;
|
|
550
|
+
const ctx = asRecord(context);
|
|
551
|
+
if (!ctx)
|
|
552
|
+
return convRest;
|
|
553
|
+
const center = asNumber(ctx.center_index) ?? fallbackCenter;
|
|
554
|
+
const messages = (Array.isArray(ctx.messages) ? ctx.messages : []).map((message) => asRecord(message)).filter((message) => {
|
|
555
|
+
const index = asNumber(message?.index);
|
|
556
|
+
return message !== null && (center === null || index === null || Math.abs(index - center) <= radius);
|
|
557
|
+
}).map(({ content_blocks, ...message }) => {
|
|
558
|
+
const covered = typeof message.content === "string" && message.content.length > 0 || Array.isArray(message.tool_calls) && message.tool_calls.length > 0 || Array.isArray(message.tool_results) && message.tool_results.length > 0;
|
|
559
|
+
return covered || content_blocks === undefined ? message : { ...message, content_blocks };
|
|
560
|
+
});
|
|
561
|
+
const indexes = messages.map((message) => asNumber(message.index)).filter((index) => index !== null);
|
|
562
|
+
return {
|
|
563
|
+
...convRest,
|
|
564
|
+
context: {
|
|
565
|
+
...ctx,
|
|
566
|
+
...indexes.length > 0 ? { from_index: Math.min(...indexes), to_index: Math.max(...indexes) } : {},
|
|
567
|
+
messages
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function trimToolFailureConversations(response, opts) {
|
|
572
|
+
if (!Array.isArray(response.examples))
|
|
573
|
+
return response;
|
|
574
|
+
const examples = response.examples.map((item) => {
|
|
575
|
+
const row = asRecord(item);
|
|
576
|
+
if (!row)
|
|
577
|
+
return item;
|
|
578
|
+
const { conversation, window, ...rest } = row;
|
|
579
|
+
const conversationId = asString(row.conversation_id);
|
|
580
|
+
const msgIndex = asNumber(asRecord(row.anchor)?.msg_index) ?? asNumber(row.msg_index);
|
|
581
|
+
return {
|
|
582
|
+
...rest,
|
|
583
|
+
...conversationId && msgIndex !== null ? { context_command: `moda context ${shellWord(conversationId)} --msg-index=${msgIndex} --window=${TOOL_FAILURE_CONTEXT_RADIUS}` } : {},
|
|
584
|
+
...opts.drop ? {} : { conversation: trimFailureWindow(conversation, TOOL_FAILURE_CONTEXT_RADIUS, msgIndex) },
|
|
585
|
+
..."window" in row ? { window: trimFailureWindow(window, opts.windowRadius ?? TOOL_FAILURE_CONTEXT_RADIUS, msgIndex) } : {}
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
return { ...response, examples };
|
|
589
|
+
}
|
|
590
|
+
function compactToolFailureExamples(response, maxChars = TOOL_FAILURE_TEXT_LIMIT) {
|
|
591
|
+
if (!Array.isArray(response.examples))
|
|
592
|
+
return { data: response, truncated: 0 };
|
|
593
|
+
let truncated = 0;
|
|
594
|
+
const cap = (value) => {
|
|
595
|
+
if (typeof value === "string") {
|
|
596
|
+
if (value.length <= maxChars)
|
|
597
|
+
return value;
|
|
598
|
+
truncated += 1;
|
|
599
|
+
return `${value.slice(0, maxChars)}… [truncated ${value.length - maxChars} chars]`;
|
|
600
|
+
}
|
|
601
|
+
if (Array.isArray(value))
|
|
602
|
+
return value.map(cap);
|
|
603
|
+
if (value && typeof value === "object") {
|
|
604
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cap(child)]));
|
|
605
|
+
}
|
|
606
|
+
return value;
|
|
607
|
+
};
|
|
608
|
+
return { data: { ...response, examples: response.examples.map(cap) }, truncated };
|
|
609
|
+
}
|
|
446
610
|
function withToolFailureAnchors(response) {
|
|
447
611
|
if (!Array.isArray(response.examples))
|
|
448
612
|
return response;
|
|
@@ -482,8 +646,11 @@ import {
|
|
|
482
646
|
openSync,
|
|
483
647
|
readFileSync,
|
|
484
648
|
realpathSync,
|
|
649
|
+
renameSync,
|
|
650
|
+
writeFileSync,
|
|
485
651
|
writeSync
|
|
486
652
|
} from "node:fs";
|
|
653
|
+
import { createHash } from "node:crypto";
|
|
487
654
|
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
488
655
|
var O_NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
489
656
|
var ADVANCE_TIMEOUT_MS = 300000;
|
|
@@ -495,22 +662,60 @@ var DRIVE_PASS_INTERVAL_MS = 20000;
|
|
|
495
662
|
var DRIVE_LIST_LIMIT = 100;
|
|
496
663
|
var DRIVE_LIST_MAX_PAGES = 10;
|
|
497
664
|
var DRAFT_BATCH_LIMIT_MAX = 25;
|
|
665
|
+
var DRAFT_BATCH_LIMIT_DEFAULT = 5;
|
|
666
|
+
var DRIVE_MAX_FIXES_DEFAULT = 3;
|
|
667
|
+
var DRIVE_MAX_FIXES_MAX = 10;
|
|
668
|
+
var VERIFY_COOLDOWN_MS = 10 * 60 * 1000;
|
|
669
|
+
var REGATE_COOLDOWN_STATUSES = new Set(["VERIFIED", "GATE_FAILED", "GATE_INCONCLUSIVE"]);
|
|
498
670
|
var DRAFT_BATCH_TIMEOUT_MS = 300000;
|
|
499
671
|
var ADVANCING_STATUSES = new Set(["DRAFT", "SCOPING", "PROPOSING", "PROPOSED", "GATING"]);
|
|
500
672
|
var FIX_USAGE = [
|
|
501
673
|
"Usage: moda fixes [--status=S] [--limit=N] [--cursor=TOKEN]",
|
|
502
|
-
" moda fixes draft-batch [--limit=N] (N is 1-25, default
|
|
503
|
-
" moda fixes drive [--status=S] [--timeout=ms] [--pass-interval=ms]",
|
|
674
|
+
" moda fixes draft-batch [--limit=N] (N is 1-25, default 5)",
|
|
675
|
+
" moda fixes drive [--status=S] [--max-fixes=N] [--timeout=ms] [--pass-interval=ms] (N is 1-10, default 3)",
|
|
504
676
|
" moda fix <fix_id> [--packet] [--wait]",
|
|
505
677
|
" moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
|
|
506
|
-
" moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms]",
|
|
678
|
+
" moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--yes]",
|
|
507
679
|
" moda fix checkout <fix_id>",
|
|
508
680
|
" moda fix submit <fix_id> --pr | --local-ref=<branch>",
|
|
509
681
|
" moda fix mark-applied <fix_id> [--note=TEXT]",
|
|
510
682
|
" moda fix dismiss <fix_id> --reason=TEXT"
|
|
511
683
|
].join(`
|
|
512
684
|
`);
|
|
685
|
+
var FIXES_SUBCOMMAND_FLAGS = {
|
|
686
|
+
list: ["cursor", "limit", "status"],
|
|
687
|
+
"draft-batch": ["limit"],
|
|
688
|
+
drive: ["status", "timeout", "pass-interval", "max-fixes"]
|
|
689
|
+
};
|
|
690
|
+
var FIX_SUBCOMMAND_FLAGS = {
|
|
691
|
+
detail: ["packet", "timeout", "wait", "poll-interval"],
|
|
692
|
+
start: ["timeout", "type", "wait", "poll-interval", "problem-id"],
|
|
693
|
+
verify: ["timeout", "wait", "fix-id", "no-wait", "poll-interval", "prompt-file"],
|
|
694
|
+
checkout: ["fix-id"],
|
|
695
|
+
submit: ["pr", "fix-id", "local-ref"],
|
|
696
|
+
"mark-applied": ["note", "fix-id"],
|
|
697
|
+
dismiss: ["reason", "fix-id"]
|
|
698
|
+
};
|
|
699
|
+
function assertWaitFlags(flags) {
|
|
700
|
+
const maxByFlag = {
|
|
701
|
+
timeout: MAX_WAIT_TIMEOUT_MS,
|
|
702
|
+
"poll-interval": MAX_POLL_INTERVAL_MS,
|
|
703
|
+
"pass-interval": MAX_POLL_INTERVAL_MS
|
|
704
|
+
};
|
|
705
|
+
for (const [name, max] of Object.entries(maxByFlag)) {
|
|
706
|
+
const raw = flags[name];
|
|
707
|
+
if (raw === undefined)
|
|
708
|
+
continue;
|
|
709
|
+
const value = Number(raw);
|
|
710
|
+
if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(value) || value < 1 || value > max) {
|
|
711
|
+
throw new CliInputError(`--${name}=${raw} must be a whole number of milliseconds between 1 and ${max}, so nothing was sent.`, FIX_USAGE);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
513
715
|
async function runFixesCommand(context) {
|
|
716
|
+
const fixesSub = context.positional && FIXES_SUBCOMMAND_FLAGS[context.positional] ? context.positional : "list";
|
|
717
|
+
assertFlags(fixesSub === "list" ? "fixes" : `fixes ${fixesSub}`, context.flags, [...FIXES_SUBCOMMAND_FLAGS[fixesSub], "tenant-id"]);
|
|
718
|
+
assertWaitFlags(context.flags);
|
|
514
719
|
validateConfig();
|
|
515
720
|
const profileOptions = profileOptionsFor(context);
|
|
516
721
|
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
@@ -564,8 +769,8 @@ async function runFixesCommand(context) {
|
|
|
564
769
|
return EXIT_OK;
|
|
565
770
|
}
|
|
566
771
|
async function runFixesDraftBatch(context, profileOptions, tenantId) {
|
|
567
|
-
const usage = "Usage: moda fixes draft-batch [--limit=N] (N is 1-25, default
|
|
568
|
-
let limit;
|
|
772
|
+
const usage = "Usage: moda fixes draft-batch [--limit=N] (N is 1-25, default 5)";
|
|
773
|
+
let limit = DRAFT_BATCH_LIMIT_DEFAULT;
|
|
569
774
|
if (context.flags.limit) {
|
|
570
775
|
const parsed = Number(context.flags.limit);
|
|
571
776
|
if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRAFT_BATCH_LIMIT_MAX) {
|
|
@@ -573,7 +778,7 @@ async function runFixesDraftBatch(context, profileOptions, tenantId) {
|
|
|
573
778
|
}
|
|
574
779
|
limit = parsed;
|
|
575
780
|
}
|
|
576
|
-
const result = await callControlAPI(`${fixesBase(tenantId)}/draft-batch`, { method: "POST", body: JSON.stringify(
|
|
781
|
+
const result = await callControlAPI(`${fixesBase(tenantId)}/draft-batch`, { method: "POST", body: JSON.stringify({ limit }) }, profileOptions, { timeoutMs: DRAFT_BATCH_TIMEOUT_MS, retries: 0 });
|
|
577
782
|
const drafted = result.drafted ?? [];
|
|
578
783
|
const skipped = result.skipped ?? [];
|
|
579
784
|
if (context.outputMode === "human") {
|
|
@@ -618,10 +823,53 @@ async function runFixesDraftBatch(context, profileOptions, tenantId) {
|
|
|
618
823
|
});
|
|
619
824
|
return EXIT_OK;
|
|
620
825
|
}
|
|
826
|
+
function driveStatePath(tenantId, profileOptions) {
|
|
827
|
+
const safeTenant = tenantId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
828
|
+
return join(getModaPaths(profileOptions).dir, `fixes-drive-last.${safeTenant}.json`);
|
|
829
|
+
}
|
|
830
|
+
function lastDrivenTimes(tenantId, profileOptions) {
|
|
831
|
+
try {
|
|
832
|
+
const raw = JSON.parse(readFileSync(driveStatePath(tenantId, profileOptions), "utf8"));
|
|
833
|
+
if (Array.isArray(raw)) {
|
|
834
|
+
return new Map(raw.filter((id) => typeof id === "string").map((id) => [id, Date.now()]));
|
|
835
|
+
}
|
|
836
|
+
const out = new Map;
|
|
837
|
+
for (const [id, at] of Object.entries(raw ?? {})) {
|
|
838
|
+
if (typeof at === "number" && Number.isFinite(at))
|
|
839
|
+
out.set(id, at);
|
|
840
|
+
}
|
|
841
|
+
return out;
|
|
842
|
+
} catch {
|
|
843
|
+
return new Map;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function recordDrivenFixIds(tenantId, ids, previous, profileOptions) {
|
|
847
|
+
try {
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
const merged = new Map(previous);
|
|
850
|
+
for (const id of ids)
|
|
851
|
+
merged.set(id, now);
|
|
852
|
+
const entries = [...merged.entries()].sort((left, right) => right[1] - left[1]).slice(0, 500);
|
|
853
|
+
const path = driveStatePath(tenantId, profileOptions);
|
|
854
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
855
|
+
const tmp = `${path}.${process.pid}.${now}.tmp`;
|
|
856
|
+
writeFileSync(tmp, `${JSON.stringify(Object.fromEntries(entries))}
|
|
857
|
+
`, { mode: 384 });
|
|
858
|
+
renameSync(tmp, path);
|
|
859
|
+
} catch {}
|
|
860
|
+
}
|
|
621
861
|
async function runFixesDrive(context, profileOptions, tenantId) {
|
|
622
862
|
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
623
863
|
const passIntervalMs = parsePositiveInt(context.flags["pass-interval"], DRIVE_PASS_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
624
864
|
const deadline = Date.now() + timeoutMs;
|
|
865
|
+
let maxFixes = DRIVE_MAX_FIXES_DEFAULT;
|
|
866
|
+
if (context.flags["max-fixes"] !== undefined) {
|
|
867
|
+
const parsed = Number(context.flags["max-fixes"]);
|
|
868
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRIVE_MAX_FIXES_MAX) {
|
|
869
|
+
throw new CliInputError(`--max-fixes must be an integer between 1 and ${DRIVE_MAX_FIXES_MAX} (got ${context.flags["max-fixes"]}) — each driven fix can run hundreds of replay playouts`, FIX_USAGE);
|
|
870
|
+
}
|
|
871
|
+
maxFixes = parsed;
|
|
872
|
+
}
|
|
625
873
|
const statusFilter = (context.flags.status ?? "").trim().toUpperCase();
|
|
626
874
|
if (statusFilter && !ADVANCING_STATUSES.has(statusFilter)) {
|
|
627
875
|
throw new CliInputError(`--status must be one of the advancing statuses (${[...ADVANCING_STATUSES].join(", ")}) — a resting fix has nothing to drive (got ${context.flags.status})`, FIX_USAGE);
|
|
@@ -669,10 +917,27 @@ async function runFixesDrive(context, profileOptions, tenantId) {
|
|
|
669
917
|
}
|
|
670
918
|
}
|
|
671
919
|
const tracked = new Map;
|
|
672
|
-
|
|
673
|
-
|
|
920
|
+
const skipped = [];
|
|
921
|
+
const advancing = snapshot.filter((fix) => ADVANCING_STATUSES.has(fix.status) && !isRestingProposed(fix));
|
|
922
|
+
const drivenAt = lastDrivenTimes(tenantId, profileOptions);
|
|
923
|
+
const rankOf = new Map(advancing.map((fix, index) => [fix.id, index]));
|
|
924
|
+
const byLeastRecent = [...advancing].sort((left, right) => {
|
|
925
|
+
const leftAt = drivenAt.get(left.id) ?? -1;
|
|
926
|
+
const rightAt = drivenAt.get(right.id) ?? -1;
|
|
927
|
+
return leftAt !== rightAt ? leftAt - rightAt : (rankOf.get(left.id) ?? 0) - (rankOf.get(right.id) ?? 0);
|
|
928
|
+
});
|
|
929
|
+
const picked = new Set(byLeastRecent.slice(0, maxFixes).map((fix) => fix.id));
|
|
930
|
+
for (const fix of advancing) {
|
|
931
|
+
if (picked.has(fix.id))
|
|
674
932
|
tracked.set(fix.id, fix);
|
|
675
|
-
|
|
933
|
+
else
|
|
934
|
+
skipped.push(fix);
|
|
935
|
+
}
|
|
936
|
+
recordDrivenFixIds(tenantId, [...picked], drivenAt, profileOptions);
|
|
937
|
+
const gatingCount = [...tracked.values()].filter((fix) => fix.status === "GATING").length;
|
|
938
|
+
if (skipped.length > 0) {
|
|
939
|
+
coverageTruncated = true;
|
|
940
|
+
snapshotWarnings.push(`Drove ${tracked.size} fix(es) (--max-fixes=${maxFixes}; ${gatingCount} already gating; least-recently-driven first); skipped ${skipped.length} advancing fix(es): ${skipped.map((fix) => `MODA-FIX-${fix.shortRef}`).join(", ")}. Re-run \`moda fixes drive\` to drive the next ones.`);
|
|
676
941
|
}
|
|
677
942
|
if (tracked.size === 0) {
|
|
678
943
|
context.output.writeData({
|
|
@@ -753,6 +1018,7 @@ async function runFixesDrive(context, profileOptions, tenantId) {
|
|
|
753
1018
|
statusReason: fix.statusReason ?? null
|
|
754
1019
|
}));
|
|
755
1020
|
const stragglers = [...tracked.values()].filter((fix) => !isRested(fix));
|
|
1021
|
+
const skippedRefs = skipped.map((fix) => fix.shortRef);
|
|
756
1022
|
const warnings = [
|
|
757
1023
|
...snapshotWarnings,
|
|
758
1024
|
...[...errors.entries()].map(([fixId, message]) => {
|
|
@@ -779,7 +1045,9 @@ ${coverageTruncated ? `Rested all ${tracked.size} scanned fix(es)` : `All ${trac
|
|
|
779
1045
|
rested: restedCount,
|
|
780
1046
|
stragglers: stragglers.map((fix) => fix.shortRef),
|
|
781
1047
|
timedOut,
|
|
782
|
-
coverageTruncated
|
|
1048
|
+
coverageTruncated,
|
|
1049
|
+
maxFixes,
|
|
1050
|
+
skipped: skippedRefs
|
|
783
1051
|
}, {
|
|
784
1052
|
...degraded ? { status: "degraded" } : {},
|
|
785
1053
|
summary: {
|
|
@@ -788,27 +1056,38 @@ ${coverageTruncated ? `Rested all ${tracked.size} scanned fix(es)` : `All ${trac
|
|
|
788
1056
|
},
|
|
789
1057
|
...warnings.length > 0 ? { warnings } : {},
|
|
790
1058
|
nextCommands: [
|
|
791
|
-
timedOut ? {
|
|
1059
|
+
...timedOut ? [{
|
|
792
1060
|
command: "moda fixes drive",
|
|
793
1061
|
purpose: "Resume driving the stragglers — advance-on-poll picks up where it left off.",
|
|
794
1062
|
mutability: "write",
|
|
795
1063
|
requires_approval: false
|
|
796
|
-
} : coverageTruncated ?
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1064
|
+
}] : coverageTruncated ? [
|
|
1065
|
+
...skipped.length > 0 || !scanTruncated ? [{
|
|
1066
|
+
command: "moda fixes drive",
|
|
1067
|
+
purpose: skipped.length > 0 ? `Drive the next ${Math.min(skipped.length, maxFixes)} of the ${skipped.length} skipped fix(es) (--max-fixes spend cap).` : "The queue was re-ranked mid-page — re-run to pick up any fix that shifted past the boundary.",
|
|
1068
|
+
mutability: "write",
|
|
1069
|
+
requires_approval: false
|
|
1070
|
+
}] : [],
|
|
1071
|
+
...scanTruncated ? [{
|
|
1072
|
+
command: statusFilter ? "moda fix <fix_id> --wait" : "moda fixes drive --status=SCOPING",
|
|
1073
|
+
purpose: statusFilter ? `Status ${statusFilter} overflows the scan window — advance the older fixes individually.` : "Drive each advancing status separately to reach fixes outside the unfiltered scan window.",
|
|
1074
|
+
mutability: "write",
|
|
1075
|
+
requires_approval: false
|
|
1076
|
+
}] : []
|
|
1077
|
+
] : [{
|
|
802
1078
|
command: "moda fixes",
|
|
803
1079
|
purpose: "Review where every fix landed (VERIFIED fixes are ready to deliver).",
|
|
804
1080
|
mutability: "read",
|
|
805
1081
|
requires_approval: false
|
|
806
|
-
}
|
|
1082
|
+
}]
|
|
807
1083
|
]
|
|
808
1084
|
});
|
|
809
1085
|
return degraded ? EXIT_DEGRADED : EXIT_OK;
|
|
810
1086
|
}
|
|
811
1087
|
async function runFixCommand(context) {
|
|
1088
|
+
const fixSub = context.positional && FIX_SUBCOMMAND_FLAGS[context.positional] ? context.positional : "detail";
|
|
1089
|
+
assertFlags(fixSub === "detail" ? "fix" : `fix ${fixSub}`, context.flags, [...FIX_SUBCOMMAND_FLAGS[fixSub], "tenant-id"]);
|
|
1090
|
+
assertWaitFlags(context.flags);
|
|
812
1091
|
validateConfig();
|
|
813
1092
|
const profileOptions = profileOptionsFor(context);
|
|
814
1093
|
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
@@ -916,6 +1195,10 @@ async function runFixVerify(context, profileOptions, tenantId) {
|
|
|
916
1195
|
throw new CliInputError("--prompt-file content is shorter than 40 characters — refusing to gate a truncated candidate");
|
|
917
1196
|
}
|
|
918
1197
|
}
|
|
1198
|
+
const candidateHash = candidateContent === undefined ? undefined : sha256(candidateContent.trim());
|
|
1199
|
+
if (!context.yes) {
|
|
1200
|
+
await assertRegateCooldown(tenantId, fixId, context, profileOptions, candidateHash);
|
|
1201
|
+
}
|
|
919
1202
|
const enqueue = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/verify`, { method: "POST", body: JSON.stringify(candidateContent ? { candidateContent } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
920
1203
|
const noWait = context.flags.wait === "false" || context.flags["no-wait"] === "true";
|
|
921
1204
|
if (noWait) {
|
|
@@ -1267,7 +1550,7 @@ function profileOptionsFor(context) {
|
|
|
1267
1550
|
return { profile: context.profile, env: context.env };
|
|
1268
1551
|
}
|
|
1269
1552
|
function requireTenantId(flags, profileOptions) {
|
|
1270
|
-
const tenantId = flags["tenant-id"] ||
|
|
1553
|
+
const tenantId = flags["tenant-id"] || resolveApiTenantId(profileOptions);
|
|
1271
1554
|
if (!tenantId) {
|
|
1272
1555
|
throw new CliInputError("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
1273
1556
|
}
|
|
@@ -1276,6 +1559,29 @@ function requireTenantId(flags, profileOptions) {
|
|
|
1276
1559
|
function fixesBase(tenantId) {
|
|
1277
1560
|
return `/tenants/${encodeURIComponent(tenantId)}/fixes`;
|
|
1278
1561
|
}
|
|
1562
|
+
async function assertRegateCooldown(tenantId, fixId, context, profileOptions, candidateHash) {
|
|
1563
|
+
const current = await getFix(tenantId, fixId, profileOptions);
|
|
1564
|
+
if (candidateHash && current.candidateRef?.contentSha256 !== candidateHash)
|
|
1565
|
+
return;
|
|
1566
|
+
const gate = current.gateResult ?? null;
|
|
1567
|
+
if (!REGATE_COOLDOWN_STATUSES.has(current.status) || !gate?.verdict || gate.pending)
|
|
1568
|
+
return;
|
|
1569
|
+
const landedAt = typeof gate.completedAt === "string" ? Date.parse(gate.completedAt) : Number.NaN;
|
|
1570
|
+
if (Number.isNaN(landedAt))
|
|
1571
|
+
return;
|
|
1572
|
+
const ageMs = Date.now() - landedAt;
|
|
1573
|
+
if (ageMs >= VERIFY_COOLDOWN_MS)
|
|
1574
|
+
return;
|
|
1575
|
+
const waitMin = Math.ceil((VERIFY_COOLDOWN_MS - ageMs) / 60000);
|
|
1576
|
+
const rerun = ["moda", "fix", "verify", fixId, ...Object.entries(context.flags).filter(([key]) => key !== "yes" && key !== "fix-id").map(([key, value]) => value === "true" ? `--${key}` : `--${key}=${shellQuote(value)}`), "--yes"].join(" ");
|
|
1577
|
+
throw new CliInputError(`MODA-FIX-${current.shortRef} was gated ${Math.max(0, Math.floor(ageMs / 60000))} min ago (verdict ${gate.verdict}); re-gating within 10 minutes re-runs the replay. Nothing was sent.`, `Wait ~${waitMin} min, or re-run with --yes to gate now: ${rerun}`);
|
|
1578
|
+
}
|
|
1579
|
+
function sha256(content) {
|
|
1580
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1581
|
+
}
|
|
1582
|
+
function shellQuote(value) {
|
|
1583
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1584
|
+
}
|
|
1279
1585
|
async function getFix(tenantId, fixId, profileOptions) {
|
|
1280
1586
|
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}`, { method: "GET" }, profileOptions);
|
|
1281
1587
|
}
|
|
@@ -1753,6 +2059,443 @@ async function runSyncCommand(context) {
|
|
|
1753
2059
|
throw new CliInputError(`Unknown sync resource '${resource}'.`, "Usage: moda sync [harness|prompts|skills] [--dry-run] [--profile=NAME]");
|
|
1754
2060
|
}
|
|
1755
2061
|
|
|
2062
|
+
// src/registry.ts
|
|
2063
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2064
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "node:path";
|
|
2065
|
+
var REGISTRY_SCHEMA = "moda_registry.v1";
|
|
2066
|
+
var DEFAULT_REGISTRY_PATH = ".moda/registry.json";
|
|
2067
|
+
var SKILL_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
2068
|
+
var MAX_ITEMS_PER_SECTION = 500;
|
|
2069
|
+
var REGISTRY_TEMPLATE = {
|
|
2070
|
+
schema: REGISTRY_SCHEMA,
|
|
2071
|
+
prompts: [
|
|
2072
|
+
{
|
|
2073
|
+
key: "support.triage",
|
|
2074
|
+
name: "Support triage system prompt",
|
|
2075
|
+
description: "Routes inbound support messages",
|
|
2076
|
+
content: "You are a support triage agent. Classify the request and ...",
|
|
2077
|
+
sourcePath: "src/agents/triage/prompt.ts"
|
|
2078
|
+
},
|
|
2079
|
+
{
|
|
2080
|
+
key: "support.reply",
|
|
2081
|
+
file: "prompts/reply.md"
|
|
2082
|
+
}
|
|
2083
|
+
],
|
|
2084
|
+
skills: [
|
|
2085
|
+
{
|
|
2086
|
+
key: "refund-policy",
|
|
2087
|
+
description: "How to process refund requests",
|
|
2088
|
+
skillMd: `---
|
|
2089
|
+
name: refund-policy
|
|
2090
|
+
description: How to process refund requests
|
|
2091
|
+
---
|
|
2092
|
+
|
|
2093
|
+
# Refund policy
|
|
2094
|
+
...`
|
|
2095
|
+
}
|
|
2096
|
+
],
|
|
2097
|
+
tools: [
|
|
2098
|
+
{
|
|
2099
|
+
schema: {
|
|
2100
|
+
name: "lookup_customer",
|
|
2101
|
+
description: "Find a customer by email",
|
|
2102
|
+
parameters: {
|
|
2103
|
+
type: "object",
|
|
2104
|
+
properties: { email: { type: "string" } },
|
|
2105
|
+
required: ["email"]
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
]
|
|
2110
|
+
};
|
|
2111
|
+
var REGISTRY_SUBCOMMAND_FLAGS = {
|
|
2112
|
+
template: ["out", "dry-run"],
|
|
2113
|
+
validate: ["file", "manifest"],
|
|
2114
|
+
push: ["file", "manifest", "live", "dry-run"]
|
|
2115
|
+
};
|
|
2116
|
+
async function runRegistryCommand(context) {
|
|
2117
|
+
const subcommand = context.positionals[0] ?? "push";
|
|
2118
|
+
assertFlags(`registry ${subcommand}`, context.flags, REGISTRY_SUBCOMMAND_FLAGS[subcommand] ?? []);
|
|
2119
|
+
switch (subcommand) {
|
|
2120
|
+
case "template":
|
|
2121
|
+
return writeTemplate(context);
|
|
2122
|
+
case "validate": {
|
|
2123
|
+
const registry = loadRegistry(context);
|
|
2124
|
+
context.output.writeData({
|
|
2125
|
+
schema: REGISTRY_SCHEMA,
|
|
2126
|
+
manifest: displayPath(context, registry.manifestPath),
|
|
2127
|
+
valid: true,
|
|
2128
|
+
counts: countsOf(registry)
|
|
2129
|
+
});
|
|
2130
|
+
return { exitCode: 0 };
|
|
2131
|
+
}
|
|
2132
|
+
case "push":
|
|
2133
|
+
return pushRegistry(context);
|
|
2134
|
+
default:
|
|
2135
|
+
throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push [--file=.moda/registry.json] [--dry-run] [--live]");
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
function writeTemplate(context) {
|
|
2139
|
+
const out = context.flags.out;
|
|
2140
|
+
if (out === "true") {
|
|
2141
|
+
throw new CliInputError("Missing output path for --out.", "Usage: moda registry template --out=.moda/registry.json");
|
|
2142
|
+
}
|
|
2143
|
+
if (!out) {
|
|
2144
|
+
context.output.writeData(REGISTRY_TEMPLATE);
|
|
2145
|
+
return { exitCode: 0 };
|
|
2146
|
+
}
|
|
2147
|
+
const target = resolve2(context.cwd, out);
|
|
2148
|
+
if (existsSync2(target) && !context.yes) {
|
|
2149
|
+
throw new CliInputError(`${out} already exists; refusing to overwrite.`, "Pass --yes to overwrite it.");
|
|
2150
|
+
}
|
|
2151
|
+
if (!context.dryRun) {
|
|
2152
|
+
mkdirSync2(dirname2(target), { recursive: true });
|
|
2153
|
+
writeFileSync2(target, `${JSON.stringify(REGISTRY_TEMPLATE, null, 2)}
|
|
2154
|
+
`);
|
|
2155
|
+
}
|
|
2156
|
+
context.output.writeData({ written: context.dryRun ? [] : [displayPath(context, target)], dryRun: context.dryRun });
|
|
2157
|
+
return { exitCode: 0 };
|
|
2158
|
+
}
|
|
2159
|
+
async function pushRegistry(context) {
|
|
2160
|
+
const registry = loadRegistry(context);
|
|
2161
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
2162
|
+
const hasKey = Boolean(resolveApiKey(profileOptions));
|
|
2163
|
+
if (!hasKey && !context.dryRun) {
|
|
2164
|
+
throw new CliInputError("No Moda API key found for registry push.", "Set MODA_API_KEY, run `moda auth login --api-key=<key>`, or select a profile with an API key.");
|
|
2165
|
+
}
|
|
2166
|
+
const sections = [];
|
|
2167
|
+
const push = async (section, endpoint, count, body) => {
|
|
2168
|
+
if (count === 0) {
|
|
2169
|
+
sections.push({ section, status: "skipped", count, endpoint });
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
if (context.dryRun && !hasKey) {
|
|
2173
|
+
sections.push({ section, status: "validated", count, endpoint });
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
const response = await callControlAPI(endpoint, { method: "POST", body: JSON.stringify({ ...body, ...context.dryRun ? { dryRun: true } : {} }) }, profileOptions);
|
|
2178
|
+
sections.push({ section, status: context.dryRun ? "dry_run" : "synced", count, endpoint, response });
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
sections.push({
|
|
2181
|
+
section,
|
|
2182
|
+
status: "error",
|
|
2183
|
+
count,
|
|
2184
|
+
endpoint,
|
|
2185
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
await push("prompts", "/prompts/sync", registry.prompts.length, { prompts: registry.prompts });
|
|
2190
|
+
await push("skills", "/skills/sync", registry.skills.length, { skills: registry.skills });
|
|
2191
|
+
await push("tools", "/tool-definitions/sync", registry.tools.length, { tools: registry.tools });
|
|
2192
|
+
const failed = sections.filter((section) => section.status === "error");
|
|
2193
|
+
if (context.outputMode === "human" && !context.quiet) {
|
|
2194
|
+
for (const section of sections) {
|
|
2195
|
+
const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
|
|
2196
|
+
console.error(` ${section.section}: ${section.status} (${section.count})${detail}`);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
context.output.writeData({
|
|
2200
|
+
schema: REGISTRY_SCHEMA,
|
|
2201
|
+
manifest: displayPath(context, registry.manifestPath),
|
|
2202
|
+
dryRun: context.dryRun,
|
|
2203
|
+
counts: countsOf(registry),
|
|
2204
|
+
sections
|
|
2205
|
+
}, {
|
|
2206
|
+
status: failed.length > 0 ? "error" : "success",
|
|
2207
|
+
...failed.length > 0 ? {
|
|
2208
|
+
errors: failed.map((section) => ({
|
|
2209
|
+
code: "registry_section_failed",
|
|
2210
|
+
message: `${section.section}: ${section.error}`,
|
|
2211
|
+
blocking: false
|
|
2212
|
+
}))
|
|
2213
|
+
} : {}
|
|
2214
|
+
});
|
|
2215
|
+
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
2216
|
+
}
|
|
2217
|
+
function describeActions(response) {
|
|
2218
|
+
const synced = isRecord(response) && Array.isArray(response.synced) ? response.synced : [];
|
|
2219
|
+
const counts = new Map;
|
|
2220
|
+
for (const item of synced) {
|
|
2221
|
+
if (!isRecord(item))
|
|
2222
|
+
continue;
|
|
2223
|
+
const action = typeof item.action === "string" ? `${item.action}${typeof item.reason === "string" ? ` (${item.reason})` : ""}` : item.changed === false ? "unchanged" : item.policyChanged === true && item.changed === true ? "changed (live setting)" : item.changed === true ? "changed" : "synced";
|
|
2224
|
+
counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
2225
|
+
}
|
|
2226
|
+
return counts.size > 0 ? ` — ${[...counts].map(([action, count]) => `${count} ${action}`).join(", ")}` : "";
|
|
2227
|
+
}
|
|
2228
|
+
function loadRegistry(context) {
|
|
2229
|
+
const fileFlag = context.flags.file ?? context.flags.manifest;
|
|
2230
|
+
if (fileFlag === "true") {
|
|
2231
|
+
throw new CliInputError("Missing path for --file.", "Usage: moda registry push --file=.moda/registry.json");
|
|
2232
|
+
}
|
|
2233
|
+
const manifestPath = resolve2(context.cwd, fileFlag ?? DEFAULT_REGISTRY_PATH);
|
|
2234
|
+
if (!existsSync2(manifestPath)) {
|
|
2235
|
+
throw new CliInputError(`Registry manifest not found: ${fileFlag ?? DEFAULT_REGISTRY_PATH}`, "Create one with `moda registry template --out=.moda/registry.json`, fill it in, then rerun.");
|
|
2236
|
+
}
|
|
2237
|
+
let parsed;
|
|
2238
|
+
try {
|
|
2239
|
+
parsed = JSON.parse(readFileSync2(manifestPath, "utf8"));
|
|
2240
|
+
} catch (error) {
|
|
2241
|
+
throw new CliInputError(`Registry manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
2242
|
+
}
|
|
2243
|
+
return resolveRegistry(parsed, {
|
|
2244
|
+
manifestPath,
|
|
2245
|
+
cwd: context.cwd,
|
|
2246
|
+
forceLive: context.flags.live === "true"
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
var PROMPT_FIELDS = {
|
|
2250
|
+
key: "string",
|
|
2251
|
+
name: "string",
|
|
2252
|
+
description: "string",
|
|
2253
|
+
category: "string",
|
|
2254
|
+
content: "string",
|
|
2255
|
+
systemPrompt: "string",
|
|
2256
|
+
messages: "array",
|
|
2257
|
+
variables: "array",
|
|
2258
|
+
variablesSchema: "object",
|
|
2259
|
+
modelConfig: "object",
|
|
2260
|
+
toolRefs: "array",
|
|
2261
|
+
responseSchema: "object",
|
|
2262
|
+
sourcePath: "string",
|
|
2263
|
+
file: "string"
|
|
2264
|
+
};
|
|
2265
|
+
var SKILL_FIELDS = {
|
|
2266
|
+
key: "string",
|
|
2267
|
+
name: "string",
|
|
2268
|
+
description: "string",
|
|
2269
|
+
skillMd: "string",
|
|
2270
|
+
sourcePath: "string",
|
|
2271
|
+
file: "string",
|
|
2272
|
+
live: "boolean"
|
|
2273
|
+
};
|
|
2274
|
+
var TOOL_FIELDS = {
|
|
2275
|
+
name: "string",
|
|
2276
|
+
description: "string",
|
|
2277
|
+
schema: "object",
|
|
2278
|
+
isActive: "boolean"
|
|
2279
|
+
};
|
|
2280
|
+
var MAX_SKILL_KEY_LENGTH = 256;
|
|
2281
|
+
var MAX_SKILL_BYTES = 1e6;
|
|
2282
|
+
function checkFields(label, item, spec, errors) {
|
|
2283
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2284
|
+
const expected = spec[key];
|
|
2285
|
+
if (!expected) {
|
|
2286
|
+
errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
const actual = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
2290
|
+
if (actual !== expected)
|
|
2291
|
+
errors.push(`${label}.${key} must be ${expected === "array" || expected === "object" ? "an" : "a"} ${expected} (got ${actual})`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function resolveRegistry(input, options) {
|
|
2295
|
+
const errors = [];
|
|
2296
|
+
if (!isRecord(input)) {
|
|
2297
|
+
throw new CliInputError("Registry manifest must be a JSON object with prompts, skills, and/or tools arrays.");
|
|
2298
|
+
}
|
|
2299
|
+
if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA) {
|
|
2300
|
+
errors.push(`schema must be "${REGISTRY_SCHEMA}" (got ${JSON.stringify(input.schema)})`);
|
|
2301
|
+
}
|
|
2302
|
+
const known = new Set(["schema", "prompts", "skills", "tools"]);
|
|
2303
|
+
for (const key of Object.keys(input)) {
|
|
2304
|
+
if (!known.has(key))
|
|
2305
|
+
errors.push(`unknown top-level field "${key}"`);
|
|
2306
|
+
}
|
|
2307
|
+
const readRef = (label, file) => {
|
|
2308
|
+
if (typeof file !== "string" || file.trim() === "") {
|
|
2309
|
+
errors.push(`${label}.file must be a non-empty string`);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const target = resolve2(options.cwd, file);
|
|
2313
|
+
if (!isInside(options.cwd, target)) {
|
|
2314
|
+
errors.push(`${label}.file "${file}" resolves outside the project directory`);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
if (!existsSync2(target)) {
|
|
2318
|
+
errors.push(`${label}.file "${file}" does not exist`);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (!isInside(realpathSync2(options.cwd), realpathSync2(target))) {
|
|
2322
|
+
errors.push(`${label}.file "${file}" is a symlink to outside the project directory`);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
if (!statSync(target).isFile()) {
|
|
2326
|
+
errors.push(`${label}.file "${file}" is not a file`);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
return readFileSync2(target, "utf8");
|
|
2330
|
+
};
|
|
2331
|
+
const sourcePathFor = (file) => relative(options.cwd, resolve2(options.cwd, file)).split("\\").join("/");
|
|
2332
|
+
const prompts = arrayOf(input.prompts, "prompts", errors).map((raw, index) => {
|
|
2333
|
+
const label = `prompts[${index}]`;
|
|
2334
|
+
if (!isRecord(raw)) {
|
|
2335
|
+
errors.push(`${label} must be an object`);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
checkFields(label, raw, PROMPT_FIELDS, errors);
|
|
2339
|
+
const { file, ...rest } = raw;
|
|
2340
|
+
if (!nonEmptyString(rest.key))
|
|
2341
|
+
errors.push(`${label}.key is required`);
|
|
2342
|
+
const prompt = { ...rest };
|
|
2343
|
+
if (file !== undefined) {
|
|
2344
|
+
if (rest.content !== undefined)
|
|
2345
|
+
errors.push(`${label}: set content or file, not both`);
|
|
2346
|
+
const content = readRef(label, file);
|
|
2347
|
+
if (content !== undefined)
|
|
2348
|
+
prompt.content = content;
|
|
2349
|
+
if (typeof file === "string" && !prompt.sourcePath)
|
|
2350
|
+
prompt.sourcePath = sourcePathFor(file);
|
|
2351
|
+
}
|
|
2352
|
+
if (!nonEmptyString(prompt.content) && !nonEmptyString(prompt.systemPrompt) && !(Array.isArray(prompt.messages) && prompt.messages.length > 0)) {
|
|
2353
|
+
errors.push(`${label}: one of content, file, systemPrompt, or messages is required`);
|
|
2354
|
+
}
|
|
2355
|
+
return prompt;
|
|
2356
|
+
});
|
|
2357
|
+
const skills = arrayOf(input.skills, "skills", errors).map((raw, index) => {
|
|
2358
|
+
const label = `skills[${index}]`;
|
|
2359
|
+
if (!isRecord(raw)) {
|
|
2360
|
+
errors.push(`${label} must be an object`);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
checkFields(label, raw, SKILL_FIELDS, errors);
|
|
2364
|
+
const skill = raw;
|
|
2365
|
+
if (!nonEmptyString(skill.key)) {
|
|
2366
|
+
errors.push(`${label}.key is required`);
|
|
2367
|
+
} else if (!SKILL_KEY_PATTERN.test(skill.key)) {
|
|
2368
|
+
errors.push(`${label}.key "${skill.key}" may only contain letters, digits, "_" and "-"`);
|
|
2369
|
+
} else if (skill.key.length > MAX_SKILL_KEY_LENGTH) {
|
|
2370
|
+
errors.push(`${label}.key must be ${MAX_SKILL_KEY_LENGTH} characters or fewer`);
|
|
2371
|
+
}
|
|
2372
|
+
let skillMd = skill.skillMd;
|
|
2373
|
+
let sourcePath = skill.sourcePath;
|
|
2374
|
+
if (skill.file !== undefined) {
|
|
2375
|
+
if (skill.skillMd !== undefined)
|
|
2376
|
+
errors.push(`${label}: set skillMd or file, not both`);
|
|
2377
|
+
skillMd = readRef(label, skill.file);
|
|
2378
|
+
if (typeof skill.file === "string" && !sourcePath)
|
|
2379
|
+
sourcePath = sourcePathFor(skill.file);
|
|
2380
|
+
}
|
|
2381
|
+
if (!nonEmptyString(skillMd)) {
|
|
2382
|
+
errors.push(`${label}: skillMd or file is required`);
|
|
2383
|
+
} else if (Buffer.byteLength(skillMd, "utf8") > MAX_SKILL_BYTES) {
|
|
2384
|
+
errors.push(`${label}: skill content exceeds ${MAX_SKILL_BYTES} bytes`);
|
|
2385
|
+
}
|
|
2386
|
+
if (skill.live !== undefined && typeof skill.live !== "boolean")
|
|
2387
|
+
errors.push(`${label}.live must be a boolean`);
|
|
2388
|
+
const live = options.forceLive || skill.live === true;
|
|
2389
|
+
return {
|
|
2390
|
+
key: skill.key,
|
|
2391
|
+
...skill.name !== undefined ? { name: skill.name } : {},
|
|
2392
|
+
...skill.description !== undefined ? { description: skill.description } : {},
|
|
2393
|
+
skillMd: skillMd ?? "",
|
|
2394
|
+
...sourcePath !== undefined ? { sourcePath } : {},
|
|
2395
|
+
...live ? { surfacePolicy: "include" } : skill.live === false ? { surfacePolicy: "local_only" } : {}
|
|
2396
|
+
};
|
|
2397
|
+
});
|
|
2398
|
+
const toolNames = new Set;
|
|
2399
|
+
const tools = arrayOf(input.tools, "tools", errors).map((raw, index) => {
|
|
2400
|
+
const label = `tools[${index}]`;
|
|
2401
|
+
if (!isRecord(raw) || !isRecord(raw.schema)) {
|
|
2402
|
+
errors.push(`${label}.schema must be an object (OpenAI function schema)`);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
checkFields(label, raw, TOOL_FIELDS, errors);
|
|
2406
|
+
const tool = raw;
|
|
2407
|
+
const inner = tool.schema.type === "function" && isRecord(tool.schema.function) ? tool.schema.function : tool.schema;
|
|
2408
|
+
const schemaName = typeof inner.name === "string" ? inner.name.trim() : undefined;
|
|
2409
|
+
const name = (typeof tool.name === "string" ? tool.name.trim() : "") || schemaName;
|
|
2410
|
+
if (typeof tool.name === "string" && tool.name.trim() && schemaName && tool.name.trim() !== schemaName) {
|
|
2411
|
+
errors.push(`${label}: name "${tool.name}" does not match schema.name "${schemaName}"`);
|
|
2412
|
+
}
|
|
2413
|
+
if (!nonEmptyString(name)) {
|
|
2414
|
+
errors.push(`${label}: name or schema.name is required`);
|
|
2415
|
+
} else if (toolNames.has(name)) {
|
|
2416
|
+
errors.push(`${label}: duplicate tool name "${name}"`);
|
|
2417
|
+
} else {
|
|
2418
|
+
toolNames.add(name);
|
|
2419
|
+
}
|
|
2420
|
+
if (inner.name !== undefined && typeof inner.name !== "string")
|
|
2421
|
+
errors.push(`${label}: schema.name must be a string`);
|
|
2422
|
+
if (inner.description !== undefined && typeof inner.description !== "string") {
|
|
2423
|
+
errors.push(`${label}: schema.description must be a string`);
|
|
2424
|
+
}
|
|
2425
|
+
if (inner.parameters !== undefined) {
|
|
2426
|
+
if (!isRecord(inner.parameters) || inner.parameters.type !== "object") {
|
|
2427
|
+
errors.push(`${label}: schema.parameters.type must be "object"`);
|
|
2428
|
+
} else if (inner.parameters.properties !== undefined && !isRecord(inner.parameters.properties)) {
|
|
2429
|
+
errors.push(`${label}: schema.parameters.properties must be an object`);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return tool;
|
|
2433
|
+
});
|
|
2434
|
+
const promptKeys = new Set;
|
|
2435
|
+
for (const prompt of prompts) {
|
|
2436
|
+
if (prompt && nonEmptyString(prompt.key)) {
|
|
2437
|
+
if (promptKeys.has(prompt.key))
|
|
2438
|
+
errors.push(`duplicate prompt key "${prompt.key}"`);
|
|
2439
|
+
promptKeys.add(prompt.key);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
const skillKeys = new Set;
|
|
2443
|
+
for (const skill of skills) {
|
|
2444
|
+
if (skill && nonEmptyString(skill.key)) {
|
|
2445
|
+
if (skillKeys.has(skill.key))
|
|
2446
|
+
errors.push(`duplicate skill key "${skill.key}"`);
|
|
2447
|
+
skillKeys.add(skill.key);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (errors.length > 0) {
|
|
2451
|
+
throw new CliInputError(`Registry manifest is invalid:
|
|
2452
|
+
- ${errors.join(`
|
|
2453
|
+
- `)}`, "Fix the manifest and rerun `moda registry validate`. See `moda registry template` for the format.");
|
|
2454
|
+
}
|
|
2455
|
+
const resolved = {
|
|
2456
|
+
manifestPath: options.manifestPath,
|
|
2457
|
+
prompts: prompts.filter(isDefined),
|
|
2458
|
+
skills: skills.filter(isDefined),
|
|
2459
|
+
tools: tools.filter(isDefined)
|
|
2460
|
+
};
|
|
2461
|
+
if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0) {
|
|
2462
|
+
throw new CliInputError("Registry manifest has no prompts, skills, or tools to push.");
|
|
2463
|
+
}
|
|
2464
|
+
return resolved;
|
|
2465
|
+
}
|
|
2466
|
+
function arrayOf(value, label, errors) {
|
|
2467
|
+
if (value === undefined)
|
|
2468
|
+
return [];
|
|
2469
|
+
if (!Array.isArray(value)) {
|
|
2470
|
+
errors.push(`${label} must be an array`);
|
|
2471
|
+
return [];
|
|
2472
|
+
}
|
|
2473
|
+
if (value.length > MAX_ITEMS_PER_SECTION) {
|
|
2474
|
+
errors.push(`${label} has ${value.length} entries; the limit is ${MAX_ITEMS_PER_SECTION} per push`);
|
|
2475
|
+
}
|
|
2476
|
+
return value;
|
|
2477
|
+
}
|
|
2478
|
+
function countsOf(registry) {
|
|
2479
|
+
return { prompts: registry.prompts.length, skills: registry.skills.length, tools: registry.tools.length };
|
|
2480
|
+
}
|
|
2481
|
+
function displayPath(context, path) {
|
|
2482
|
+
const rel = relative(context.cwd, path);
|
|
2483
|
+
return rel && !rel.startsWith("..") && !isAbsolute2(rel) ? rel.split("\\").join("/") : path;
|
|
2484
|
+
}
|
|
2485
|
+
function isInside(root, target) {
|
|
2486
|
+
const rel = relative(resolve2(root), target);
|
|
2487
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
2488
|
+
}
|
|
2489
|
+
function isRecord(value) {
|
|
2490
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2491
|
+
}
|
|
2492
|
+
function nonEmptyString(value) {
|
|
2493
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2494
|
+
}
|
|
2495
|
+
function isDefined(value) {
|
|
2496
|
+
return value !== undefined;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
1756
2499
|
// src/telemetry.ts
|
|
1757
2500
|
import { randomUUID } from "node:crypto";
|
|
1758
2501
|
var TELEMETRY_TIMEOUT_MS = 500;
|
|
@@ -1897,7 +2640,7 @@ async function sendCliUsageTelemetry(input) {
|
|
|
1897
2640
|
}
|
|
1898
2641
|
|
|
1899
2642
|
// src/update-check.ts
|
|
1900
|
-
import { mkdirSync as
|
|
2643
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1901
2644
|
import { join as join2 } from "node:path";
|
|
1902
2645
|
var UPDATE_CHECK_FLAG = "no-update-check";
|
|
1903
2646
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -1916,7 +2659,7 @@ function cachePath(options = {}) {
|
|
|
1916
2659
|
}
|
|
1917
2660
|
function readCache(options = {}) {
|
|
1918
2661
|
try {
|
|
1919
|
-
const parsed = JSON.parse(
|
|
2662
|
+
const parsed = JSON.parse(readFileSync3(cachePath(options), "utf8"));
|
|
1920
2663
|
if (parsed && typeof parsed === "object" && typeof parsed.checked_at === "number" && typeof parsed.latest === "string") {
|
|
1921
2664
|
return parsed;
|
|
1922
2665
|
}
|
|
@@ -1925,8 +2668,8 @@ function readCache(options = {}) {
|
|
|
1925
2668
|
}
|
|
1926
2669
|
function writeCache(cache, options = {}) {
|
|
1927
2670
|
try {
|
|
1928
|
-
|
|
1929
|
-
|
|
2671
|
+
mkdirSync3(getModaDir(options), { recursive: true });
|
|
2672
|
+
writeFileSync3(cachePath(options), `${JSON.stringify(cache, null, 2)}
|
|
1930
2673
|
`, "utf8");
|
|
1931
2674
|
} catch {}
|
|
1932
2675
|
}
|
|
@@ -2003,6 +2746,597 @@ function createCommandRegistry(definitions) {
|
|
|
2003
2746
|
};
|
|
2004
2747
|
}
|
|
2005
2748
|
|
|
2749
|
+
// src/subcommand-help.ts
|
|
2750
|
+
var DRY_RUN = { name: "--dry-run", description: "Print the exact request and send nothing." };
|
|
2751
|
+
var TENANT_ID = { name: "--tenant-id=<id>", description: "Tenant to act on; must match the API key's tenant (default: the key's)." };
|
|
2752
|
+
var SIGNAL_FIELD_FLAGS = [
|
|
2753
|
+
{ name: "--name=<text>", description: "Signal name." },
|
|
2754
|
+
{ name: "--description=<text>", description: "What the signal catches." },
|
|
2755
|
+
{ name: "--pass-criteria=<text>", description: "When a trace passes (the judge reads this)." },
|
|
2756
|
+
{ name: "--fail-criteria=<text>", description: "When a trace fails (the judge reads this)." },
|
|
2757
|
+
{ name: "--polarity=violation|expectation", description: "Whether a hit is something that should not happen, or something that should." },
|
|
2758
|
+
{ name: "--locus=agent|tool_output|user_input", description: "Where in the trace the judge looks." },
|
|
2759
|
+
{ name: "--tool-names=a,b", description: "Scope: only traces that call these tools." },
|
|
2760
|
+
{ name: "--keyword-hints=a,b", description: "Scope: keywords that make a trace a candidate." },
|
|
2761
|
+
{ name: "--use-case-id=<id>", description: "Scope: one use case (cluster id)." },
|
|
2762
|
+
{ name: "--message-types=user,assistant,tool_input,tool_output", description: "Scope: message types the judge reads." },
|
|
2763
|
+
{ name: "--note=<text>", description: "Free-text note stored with the signal." },
|
|
2764
|
+
{ name: "--file=<body.json>", description: "JSON request body; flags given on the command line override its fields." }
|
|
2765
|
+
];
|
|
2766
|
+
var SIGNALS_SUBCOMMAND_HELP = [
|
|
2767
|
+
{
|
|
2768
|
+
name: "draft",
|
|
2769
|
+
description: "AI-draft name and criteria from a plain description (one LLM call; nothing is saved)",
|
|
2770
|
+
usage: 'moda signals draft --note="<what to catch>" [--file=<body.json>] [--dry-run]',
|
|
2771
|
+
flags: [
|
|
2772
|
+
{ name: "--note=<text>", description: "Required. What the signal should catch." },
|
|
2773
|
+
{ name: "--file=<body.json>", description: "JSON request body (e.g. with seed/answers); --note overrides its note." },
|
|
2774
|
+
DRY_RUN,
|
|
2775
|
+
TENANT_ID
|
|
2776
|
+
],
|
|
2777
|
+
examples: ['moda signals draft --note="agent promises a refund it cannot issue"']
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
name: "create",
|
|
2781
|
+
description: "Create a custom signal (starts as a draft; activate it to run)",
|
|
2782
|
+
usage: "moda signals create --name=<text> --description=<text> --pass-criteria=<text> --fail-criteria=<text> [scope flags] [--file=<body.json>] [--dry-run]",
|
|
2783
|
+
flags: [...SIGNAL_FIELD_FLAGS, DRY_RUN, TENANT_ID],
|
|
2784
|
+
examples: [
|
|
2785
|
+
'moda signals create --name="Refund promise" --description="…" --pass-criteria="…" --fail-criteria="…" --dry-run',
|
|
2786
|
+
"moda signals create --file=signal.json"
|
|
2787
|
+
]
|
|
2788
|
+
},
|
|
2789
|
+
{
|
|
2790
|
+
name: "update",
|
|
2791
|
+
description: "Edit a signal's fields (pass at least one field flag or --file)",
|
|
2792
|
+
usage: "moda signals update <signal_id> [field flags] [--file=<body.json>] [--dry-run]",
|
|
2793
|
+
flags: [...SIGNAL_FIELD_FLAGS, DRY_RUN, TENANT_ID],
|
|
2794
|
+
examples: ['moda signals update <signal_id> --fail-criteria="…" --dry-run']
|
|
2795
|
+
},
|
|
2796
|
+
{
|
|
2797
|
+
name: "activate",
|
|
2798
|
+
description: "Start running a signal on new traces",
|
|
2799
|
+
usage: "moda signals activate <signal_id> [--dry-run]",
|
|
2800
|
+
flags: [DRY_RUN, TENANT_ID],
|
|
2801
|
+
examples: ["moda signals activate <signal_id>"]
|
|
2802
|
+
},
|
|
2803
|
+
{
|
|
2804
|
+
name: "pause",
|
|
2805
|
+
description: "Stop running a signal (keeps it and its detections)",
|
|
2806
|
+
usage: "moda signals pause <signal_id> [--dry-run]",
|
|
2807
|
+
flags: [DRY_RUN, TENANT_ID],
|
|
2808
|
+
examples: ["moda signals pause <signal_id>"]
|
|
2809
|
+
},
|
|
2810
|
+
{
|
|
2811
|
+
name: "delete",
|
|
2812
|
+
description: "Delete a draft signal (drafts only; archiving a live signal is dashboard-only, use pause)",
|
|
2813
|
+
usage: "moda signals delete <signal_id> [--dry-run]",
|
|
2814
|
+
flags: [DRY_RUN, TENANT_ID],
|
|
2815
|
+
examples: ["moda signals delete <signal_id> --dry-run"]
|
|
2816
|
+
},
|
|
2817
|
+
{
|
|
2818
|
+
name: "examples",
|
|
2819
|
+
description: "Add, relabel, or remove a labelled example trace on a signal",
|
|
2820
|
+
usage: "moda signals examples <signal_id> add|update|remove --conversation-id=<id> [flags] [--dry-run]",
|
|
2821
|
+
flags: [
|
|
2822
|
+
{ name: "--conversation-id=<id>", description: "Required. The trace (conversation_id) the example points at." },
|
|
2823
|
+
{ name: "--label=fits|does_not_fit", description: "add: required. update: optional new label." },
|
|
2824
|
+
{ name: "--note=<text>", description: "add/update: why the trace fits or not." },
|
|
2825
|
+
{ name: "--msg-index=<n>", description: "add: anchor message index in the trace." },
|
|
2826
|
+
{ name: "--msg-dedup-token=<token>", description: "add: stable anchor message token (preferred over --msg-index)." },
|
|
2827
|
+
{ name: "--criteria=pass|fail", description: "add: which criteria the example illustrates." },
|
|
2828
|
+
{ name: "--verdict=accept|reject", description: "add: your verdict on a proposed match." },
|
|
2829
|
+
{ name: "--blurb=<text>", description: "update: replace the example summary." },
|
|
2830
|
+
DRY_RUN,
|
|
2831
|
+
TENANT_ID
|
|
2832
|
+
],
|
|
2833
|
+
examples: [
|
|
2834
|
+
'moda signals examples <signal_id> add --conversation-id=<id> --label=does_not_fit --note="refund was approved"',
|
|
2835
|
+
"moda signals examples <signal_id> remove --conversation-id=<id>"
|
|
2836
|
+
]
|
|
2837
|
+
},
|
|
2838
|
+
{
|
|
2839
|
+
name: "refine",
|
|
2840
|
+
description: "Propose a criteria edit from a plain request (one LLM call; review with `proposals`)",
|
|
2841
|
+
usage: 'moda signals refine <signal_id> --request="<change>" [--dry-run]',
|
|
2842
|
+
flags: [
|
|
2843
|
+
{ name: "--request=<text>", description: "Required, 1-2000 characters. The change you want." },
|
|
2844
|
+
DRY_RUN,
|
|
2845
|
+
TENANT_ID
|
|
2846
|
+
],
|
|
2847
|
+
examples: ['moda signals refine <signal_id> --request="ignore refunds the agent escalates to a human"']
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
name: "revise",
|
|
2851
|
+
description: "Re-derive criteria from labelled example traces (LLM call; rate-limited)",
|
|
2852
|
+
usage: "moda signals revise <signal_id> --conversation-ids=a,b [--dry-run]",
|
|
2853
|
+
flags: [
|
|
2854
|
+
{ name: "--conversation-ids=a,b", description: "Required. Labelled traces to learn from." },
|
|
2855
|
+
DRY_RUN,
|
|
2856
|
+
TENANT_ID
|
|
2857
|
+
],
|
|
2858
|
+
examples: ["moda signals revise <signal_id> --conversation-ids=conv_1,conv_2"]
|
|
2859
|
+
},
|
|
2860
|
+
{
|
|
2861
|
+
name: "proposals",
|
|
2862
|
+
description: "List pending criteria proposals for a signal (read-only)",
|
|
2863
|
+
usage: "moda signals proposals <signal_id>",
|
|
2864
|
+
flags: [TENANT_ID],
|
|
2865
|
+
examples: ["moda signals proposals <signal_id>"]
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
name: "accept",
|
|
2869
|
+
description: "Apply a criteria proposal to the signal",
|
|
2870
|
+
usage: "moda signals accept <signal_id> <proposal_id> [--dry-run]",
|
|
2871
|
+
flags: [DRY_RUN, TENANT_ID],
|
|
2872
|
+
examples: ["moda signals accept <signal_id> <proposal_id>"]
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
name: "reject",
|
|
2876
|
+
description: "Discard a criteria proposal",
|
|
2877
|
+
usage: "moda signals reject <signal_id> <proposal_id> [--dry-run]",
|
|
2878
|
+
flags: [DRY_RUN, TENANT_ID],
|
|
2879
|
+
examples: ["moda signals reject <signal_id> <proposal_id>"]
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
name: "search",
|
|
2883
|
+
description: "Search recent traces for matches with the judge (up to ~1,000 judge calls; one search per signal at a time)",
|
|
2884
|
+
usage: "moda signals search <signal_id> [--mode=wizard|full] [--exclude-conversation-ids=a,b] [--dry-run]",
|
|
2885
|
+
flags: [
|
|
2886
|
+
{ name: "--mode=wizard|full", description: "full (default): the whole search. wizard: stops at about 6 matches (at most ~42 traces judged), for labelling." },
|
|
2887
|
+
{ name: "--exclude-conversation-ids=a,b", description: "Traces to skip (e.g. already labelled)." },
|
|
2888
|
+
DRY_RUN,
|
|
2889
|
+
TENANT_ID
|
|
2890
|
+
],
|
|
2891
|
+
examples: ["moda signals search <signal_id> --mode=wizard"]
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
name: "search-status",
|
|
2895
|
+
description: "Poll a signal search (read-only)",
|
|
2896
|
+
usage: "moda signals search-status <signal_id> <search_id>",
|
|
2897
|
+
flags: [TENANT_ID],
|
|
2898
|
+
examples: ["moda signals search-status <signal_id> <search_id>"]
|
|
2899
|
+
},
|
|
2900
|
+
{
|
|
2901
|
+
name: "search-stop",
|
|
2902
|
+
description: "Stop a running signal search",
|
|
2903
|
+
usage: "moda signals search-stop <signal_id> <search_id> [--dry-run]",
|
|
2904
|
+
flags: [DRY_RUN, TENANT_ID],
|
|
2905
|
+
examples: ["moda signals search-stop <signal_id> <search_id>"]
|
|
2906
|
+
},
|
|
2907
|
+
{
|
|
2908
|
+
name: "preview",
|
|
2909
|
+
description: "Run a deterministic (non-judge) detector over recent traces and show what it matches; saves nothing",
|
|
2910
|
+
usage: "moda signals preview --file=<detector.json> [--conversation-ids=a,b] [--max-examples=N]",
|
|
2911
|
+
flags: [
|
|
2912
|
+
{
|
|
2913
|
+
name: "--file=<detector.json>",
|
|
2914
|
+
description: 'Required. JSON {"detector":{"type":…,"config":{…}},"scope"?:{…}}. Types: rule, tool_call, routing, metric, all ' + '(judge detectors preview through `search`). Rule config: {"pattern":"<JS regex>","flags":"i","target":"assistant|user|any|tool_input|tool_output"}; ' + 'regex flags go in "flags" (i, m, s, u), not inline like (?i).'
|
|
2915
|
+
},
|
|
2916
|
+
{ name: "--conversation-ids=a,b", description: "Check these traces (at most 200) instead of the recent sample; every match comes back." },
|
|
2917
|
+
{ name: "--max-examples=<n>", description: "Examples returned from the recent sample (1-50, default 5)." },
|
|
2918
|
+
TENANT_ID
|
|
2919
|
+
],
|
|
2920
|
+
examples: [
|
|
2921
|
+
`echo '{"detector":{"type":"rule","config":{"pattern":"cancel(l)?ed","flags":"i","target":"assistant"}}}' > detector.json`,
|
|
2922
|
+
"moda signals preview --file=detector.json --max-examples=10",
|
|
2923
|
+
`echo '{"detector":{"type":"tool_call","config":{"toolName":"lookupCustomer","condition":"errors"}}}' > detector.json`
|
|
2924
|
+
]
|
|
2925
|
+
}
|
|
2926
|
+
];
|
|
2927
|
+
var SKILLS_SUBCOMMAND_HELP = [
|
|
2928
|
+
{
|
|
2929
|
+
name: "gen",
|
|
2930
|
+
description: "Generate skills from production traces (paid: clustering + LLM distillation; no dry-run)",
|
|
2931
|
+
usage: "moda skills gen [--source=all|sdk] [--max-sessions=N] [--lookback-hours=N] [--max-traces=N] [--improvement-rounds=1-3] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--wait]",
|
|
2932
|
+
flags: [
|
|
2933
|
+
{ name: "--source=all|sdk", description: "Which traces to learn from (default all)." },
|
|
2934
|
+
{ name: "--max-sessions=<n>", description: "Sessions to process (server default 200). Must be 1 or more." },
|
|
2935
|
+
{ name: "--lookback-hours=<n>", description: "Window to read (server default 720 = 30 days). Must be 1 or more." },
|
|
2936
|
+
{ name: "--start-at=<iso>, --end-at=<iso>", description: "Explicit window instead of --lookback-hours." },
|
|
2937
|
+
{ name: "--max-traces=<n>", description: "Cap on cluster traces loaded for distillation." },
|
|
2938
|
+
{ name: "--improve-candidates", description: "Run improvement rounds on candidate skills." },
|
|
2939
|
+
{ name: "--improvement-rounds=<1-3>", description: "Improvement rounds (default 2, max 3)." },
|
|
2940
|
+
{ name: "--replay", description: "Replay candidates against a replay set (extra paid playouts)." },
|
|
2941
|
+
{ name: "--replay-set-id=<id>", description: "Replay set for --replay." },
|
|
2942
|
+
{ name: "--reprocess-segments", description: "Re-segment sessions that were already processed." },
|
|
2943
|
+
{ name: "--force-reprocess", description: "Reprocess everything in the window (implies --reprocess-segments)." },
|
|
2944
|
+
{ name: "--no-clustering", description: "Skip clustering; reuse --clustering-run-id." },
|
|
2945
|
+
{ name: "--clustering-run-id=<id>", description: "Use an existing clustering run." },
|
|
2946
|
+
{ name: "--wait", description: "Wait for the run to finish instead of returning once it is accepted." },
|
|
2947
|
+
TENANT_ID
|
|
2948
|
+
],
|
|
2949
|
+
examples: ["moda skills gen", "moda skills gen --lookback-hours=168 --max-sessions=50 --wait"]
|
|
2950
|
+
},
|
|
2951
|
+
{
|
|
2952
|
+
name: "status",
|
|
2953
|
+
description: "Show a skill generation run (default: the latest) and its events",
|
|
2954
|
+
usage: "moda skills status [<run_id>] [--run-id=<id>]",
|
|
2955
|
+
examples: ["moda skills status", "moda skills status <run_id>"]
|
|
2956
|
+
},
|
|
2957
|
+
{
|
|
2958
|
+
name: "pull",
|
|
2959
|
+
description: "Write generated skills into the detected coding agents' skill directories",
|
|
2960
|
+
usage: "moda skills pull [--status=approved|proposed|all]",
|
|
2961
|
+
flags: [{ name: "--status=approved|proposed|all", description: "Which skills to pull (default approved)." }],
|
|
2962
|
+
examples: ["moda skills pull", "moda skills pull --status=all"]
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
name: "sync",
|
|
2966
|
+
description: "Upload locally authored .claude/skills/**/SKILL.md files to Moda",
|
|
2967
|
+
usage: "moda skills sync [--dry-run]",
|
|
2968
|
+
flags: [{ name: "--dry-run", description: "Server-side preview; nothing is saved." }],
|
|
2969
|
+
examples: ["moda skills sync --dry-run", "moda skills sync"]
|
|
2970
|
+
},
|
|
2971
|
+
{
|
|
2972
|
+
name: "list",
|
|
2973
|
+
description: "List the tenant's skills (the dashboard skill library; read-only)",
|
|
2974
|
+
usage: "moda skills list",
|
|
2975
|
+
examples: ["moda skills list"]
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
name: "show",
|
|
2979
|
+
description: "Show one skill with its content (read-only)",
|
|
2980
|
+
usage: "moda skills show <key_or_id>",
|
|
2981
|
+
examples: ["moda skills show refund-policy"]
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
name: "policy",
|
|
2985
|
+
description: "Switch a skill between live (served to agents) and local-only",
|
|
2986
|
+
usage: "moda skills policy <key_or_id> --live|--local [--dry-run]",
|
|
2987
|
+
flags: [
|
|
2988
|
+
{ name: "--live", description: "Include the skill in the served skill surface." },
|
|
2989
|
+
{ name: "--local", description: "Keep the skill local-only (not served)." },
|
|
2990
|
+
DRY_RUN
|
|
2991
|
+
],
|
|
2992
|
+
examples: ["moda skills policy refund-policy --live --dry-run"]
|
|
2993
|
+
},
|
|
2994
|
+
{
|
|
2995
|
+
name: "proposals",
|
|
2996
|
+
description: "List skill improvement proposals (read-only)",
|
|
2997
|
+
usage: "moda skills proposals list [--status=ready_for_pr|all] [--generation-run-id=<id>]",
|
|
2998
|
+
flags: [
|
|
2999
|
+
{ name: "--status=<status>", description: "Proposal status to list (default ready_for_pr; all for every status)." },
|
|
3000
|
+
{ name: "--generation-run-id=<id>", description: "Only proposals from one generation run." },
|
|
3001
|
+
TENANT_ID
|
|
3002
|
+
],
|
|
3003
|
+
examples: ["moda skills proposals list"]
|
|
3004
|
+
},
|
|
3005
|
+
{
|
|
3006
|
+
name: "proposal",
|
|
3007
|
+
description: "Apply a skill proposal: write its SKILL.md locally and acknowledge it on the server",
|
|
3008
|
+
usage: "moda skills proposal apply <proposal_id>",
|
|
3009
|
+
flags: [TENANT_ID],
|
|
3010
|
+
examples: ["moda skills proposal apply <proposal_id>"]
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
name: "install",
|
|
3014
|
+
description: "Install bundled integration guides into .claude/skills (offline; no API key)",
|
|
3015
|
+
usage: "moda skills install [--list] <id...>",
|
|
3016
|
+
flags: [{ name: "--list", description: "List the bundled guides." }],
|
|
3017
|
+
examples: ["moda skills install --list", "moda skills install integration-node-openai"]
|
|
3018
|
+
}
|
|
3019
|
+
];
|
|
3020
|
+
var PROMPTS_REGISTRY_SUBCOMMAND_HELP = [
|
|
3021
|
+
{
|
|
3022
|
+
name: "list",
|
|
3023
|
+
description: "List the tenant's registered prompts (read-only)",
|
|
3024
|
+
usage: "moda prompts list [--label=prod|staging|dev|unlabeled] [--search=<text>]",
|
|
3025
|
+
flags: [
|
|
3026
|
+
{ name: "--label=<label>", description: "Only prompts with this label set (prod, staging, dev), or unlabeled ones." },
|
|
3027
|
+
{ name: "--search=<text>", description: "Case-insensitive match on key, name, and description." }
|
|
3028
|
+
],
|
|
3029
|
+
examples: ["moda prompts list", "moda prompts list --label=prod"]
|
|
3030
|
+
},
|
|
3031
|
+
{
|
|
3032
|
+
name: "show",
|
|
3033
|
+
description: "Show one prompt with its versions and labels (read-only)",
|
|
3034
|
+
usage: "moda prompts show <key_or_id>",
|
|
3035
|
+
examples: ["moda prompts show support.triage"]
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
name: "proposals",
|
|
3039
|
+
description: "List revision proposals for a prompt (read-only)",
|
|
3040
|
+
usage: "moda prompts proposals <key_or_id>",
|
|
3041
|
+
examples: ["moda prompts proposals support.triage"]
|
|
3042
|
+
},
|
|
3043
|
+
{
|
|
3044
|
+
name: "usage",
|
|
3045
|
+
description: "Per-version call, trace, and token counts for a prompt from traces (read-only)",
|
|
3046
|
+
usage: "moda prompts usage <key_or_id>",
|
|
3047
|
+
examples: ["moda prompts usage support.triage"]
|
|
3048
|
+
},
|
|
3049
|
+
{
|
|
3050
|
+
name: "replay-runs",
|
|
3051
|
+
description: "List replay (A/B) runs for a prompt (read-only)",
|
|
3052
|
+
usage: "moda prompts replay-runs <key_or_id>",
|
|
3053
|
+
examples: ["moda prompts replay-runs support.triage"]
|
|
3054
|
+
},
|
|
3055
|
+
{
|
|
3056
|
+
name: "decide",
|
|
3057
|
+
description: "Merge, reject, or reopen a prompt revision proposal",
|
|
3058
|
+
usage: "moda prompts decide <key_or_id> <proposal_id> --action=merge|reject|reopen [--dry-run]",
|
|
3059
|
+
flags: [
|
|
3060
|
+
{ name: "--action=merge|reject|reopen", description: "Required. The decision." },
|
|
3061
|
+
DRY_RUN
|
|
3062
|
+
],
|
|
3063
|
+
examples: ["moda prompts decide support.triage <proposal_id> --action=merge --dry-run"]
|
|
3064
|
+
}
|
|
3065
|
+
];
|
|
3066
|
+
var HARNESS_PATH_FLAG = { name: "--path=<dir>", description: "Project root to read (default: the current directory)." };
|
|
3067
|
+
var HARNESS_REPORT_FLAG = { name: "--report=<path>", description: "Harness report to use (default .moda/harness-report.json)." };
|
|
3068
|
+
var HARNESS_SUBCOMMAND_HELP = [
|
|
3069
|
+
{
|
|
3070
|
+
name: "scan",
|
|
3071
|
+
description: "Deterministic local scan; writes .moda/harness.json (local, no API key)",
|
|
3072
|
+
usage: "moda harness scan [--path=<dir>] [--summary] [--dry-run] [--yes]",
|
|
3073
|
+
flags: [
|
|
3074
|
+
HARNESS_PATH_FLAG,
|
|
3075
|
+
{ name: "--summary", description: "Also write .moda/HARNESS.md." },
|
|
3076
|
+
{ name: "--dry-run", description: "Scan and print; write nothing." },
|
|
3077
|
+
{ name: "--yes", description: "Approve a broad scan without the prompt." }
|
|
3078
|
+
],
|
|
3079
|
+
examples: ["moda harness scan", "moda harness scan --path=services/agent --dry-run"]
|
|
3080
|
+
},
|
|
3081
|
+
{
|
|
3082
|
+
name: "sync",
|
|
3083
|
+
description: "Upload the harness graph (an approved report, or a fresh local scan) to Moda Cloud",
|
|
3084
|
+
usage: "moda harness sync [--from-report=<path>] [--path=<dir>] [--summary] [--allow-no-agents] [--dry-run]",
|
|
3085
|
+
flags: [
|
|
3086
|
+
{ name: "--from-report=<path>", description: "Sync an approved harness report (see `harness approve`). Without it, syncs a fresh local scan." },
|
|
3087
|
+
HARNESS_PATH_FLAG,
|
|
3088
|
+
{ name: "--summary", description: "Also write .moda/HARNESS.md." },
|
|
3089
|
+
{ name: "--allow-no-agents", description: "Sync a graph with no runtime agents (normally refused)." },
|
|
3090
|
+
{ name: "--dry-run", description: "Server-side preview with the same graph hash and changed flag the real sync would report; writes nothing." },
|
|
3091
|
+
{ name: "--tenant-id=<id>", description: "Tenant to record on the graph (default: the configured tenant)." }
|
|
3092
|
+
],
|
|
3093
|
+
examples: [
|
|
3094
|
+
"moda harness sync --dry-run",
|
|
3095
|
+
"moda harness sync --from-report .moda/harness-report.json"
|
|
3096
|
+
]
|
|
3097
|
+
},
|
|
3098
|
+
{
|
|
3099
|
+
name: "delete",
|
|
3100
|
+
description: "Permanently delete a synced harness and all its versions from Moda Cloud",
|
|
3101
|
+
usage: "moda harness delete <harness_id_or_key> [--yes] [--dry-run]",
|
|
3102
|
+
flags: [
|
|
3103
|
+
{ name: "--yes", description: "Confirm the deletion (required outside an interactive terminal)." },
|
|
3104
|
+
{ name: "--dry-run", description: "Print what would be deleted; send nothing." }
|
|
3105
|
+
],
|
|
3106
|
+
examples: ["moda harness delete my-harness-key --dry-run", "moda harness delete my-harness-key --yes"]
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
name: "analyze",
|
|
3110
|
+
description: "Run Moda Analyst to produce a cited harness report (experimental; the GitHub App analyzes automatically)",
|
|
3111
|
+
usage: "moda harness analyze --experimental [--remote | --analyst=local-scan|claude|codex|cursor|auto] [--concern=prompts] [--path=<dir>] [--summary] [--dry-run] [--yes]",
|
|
3112
|
+
flags: [
|
|
3113
|
+
{ name: "--experimental", description: "Required to run the CLI analyze (or set MODA_HARNESS_ANALYZE_CLI=1)." },
|
|
3114
|
+
{ name: "--remote", description: "Run the analysis Moda-hosted (paid); fetch the result with `harness pull`." },
|
|
3115
|
+
{ name: "--local, --no-remote", description: "Force a local run even when MODA_HARNESS_REMOTE is set." },
|
|
3116
|
+
{ name: "--analyst=<adapter>", description: "local-scan (no LLM), claude, codex, cursor, or auto (an installed coding agent)." },
|
|
3117
|
+
{ name: "--analyst-model=<model>", description: "Model for the local coding-agent analyst." },
|
|
3118
|
+
{ name: "--shards=<n>", description: "Remote: split the analysis across n shards." },
|
|
3119
|
+
{ name: "--concern=prompts", description: "Narrow the analysis to prompts (local coding-agent analyst only)." },
|
|
3120
|
+
{ name: "--github-actions", description: "Run inside GitHub Actions with OIDC (no API key on the runner)." },
|
|
3121
|
+
HARNESS_PATH_FLAG,
|
|
3122
|
+
{ name: "--summary", description: "Also write markdown summaries." },
|
|
3123
|
+
{ name: "--dry-run", description: "Analyze and print; write nothing." },
|
|
3124
|
+
{ name: "--yes", description: "Approve a broad scan without the prompt." }
|
|
3125
|
+
],
|
|
3126
|
+
examples: ["moda harness analyze --experimental --summary", "moda harness analyze --experimental --remote"]
|
|
3127
|
+
},
|
|
3128
|
+
{
|
|
3129
|
+
name: "pull",
|
|
3130
|
+
description: "Fetch a Moda-hosted analysis run once; write its report when complete",
|
|
3131
|
+
usage: "moda harness pull [--run-id=<id>] [--sync]",
|
|
3132
|
+
flags: [
|
|
3133
|
+
{ name: "--run-id=<id>", description: "Run to fetch (default: the one in .moda/harness-remote-run.json)." },
|
|
3134
|
+
{ name: "--sync", description: "When complete, approve the report and sync it (like `moda init`)." }
|
|
3135
|
+
],
|
|
3136
|
+
examples: ["moda harness pull", "moda harness pull --sync"]
|
|
3137
|
+
},
|
|
3138
|
+
{
|
|
3139
|
+
name: "validate-report",
|
|
3140
|
+
description: "Validate a harness report against the code and write the validation result (local)",
|
|
3141
|
+
usage: "moda harness validate-report [<report_path>] [--report=<path>] [--dry-run]",
|
|
3142
|
+
flags: [HARNESS_REPORT_FLAG, { name: "--dry-run", description: "Validate only; write nothing." }],
|
|
3143
|
+
examples: ["moda harness validate-report"]
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
name: "approve",
|
|
3147
|
+
description: "Record your approval of a validated harness report so `sync --from-report` will upload it (local)",
|
|
3148
|
+
usage: "moda harness approve [<report_path>] [--report=<path>] [--yes]",
|
|
3149
|
+
flags: [HARNESS_REPORT_FLAG, { name: "--yes", description: "Approve without the interactive prompt." }],
|
|
3150
|
+
examples: ["moda harness approve --yes"]
|
|
3151
|
+
},
|
|
3152
|
+
{
|
|
3153
|
+
name: "candidates",
|
|
3154
|
+
description: "Static pre-pass listing likely agents, prompts, and tools (local; no LLM, no network)",
|
|
3155
|
+
usage: "moda harness candidates [--path=<dir>] [--out=<file>] [--dry-run] [--yes]",
|
|
3156
|
+
flags: [
|
|
3157
|
+
HARNESS_PATH_FLAG,
|
|
3158
|
+
{ name: "--out=<file>", description: "Also write the candidates report to a file." },
|
|
3159
|
+
{ name: "--dry-run", description: "Do not write --out." },
|
|
3160
|
+
{ name: "--yes", description: "Approve a broad scan without the prompt." }
|
|
3161
|
+
],
|
|
3162
|
+
examples: ["moda harness candidates --out=.moda/candidates.json"]
|
|
3163
|
+
},
|
|
3164
|
+
{
|
|
3165
|
+
name: "agents",
|
|
3166
|
+
description: "List runtime agents and families from .moda/harness.json (or a live scan) (local)",
|
|
3167
|
+
usage: "moda harness agents [--path=<dir>]",
|
|
3168
|
+
flags: [HARNESS_PATH_FLAG],
|
|
3169
|
+
examples: ["moda harness agents"]
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
name: "graph",
|
|
3173
|
+
description: "Print the full local harness graph (local)",
|
|
3174
|
+
usage: "moda harness graph [--path=<dir>]",
|
|
3175
|
+
flags: [HARNESS_PATH_FLAG],
|
|
3176
|
+
examples: ["moda harness graph --json"]
|
|
3177
|
+
},
|
|
3178
|
+
{
|
|
3179
|
+
name: "explain",
|
|
3180
|
+
description: "Explain the harness, or one runtime agent (local)",
|
|
3181
|
+
usage: "moda harness explain [--agent-id=<id>] [--path=<dir>]",
|
|
3182
|
+
flags: [
|
|
3183
|
+
{ name: "--agent-id=<id>, --agent=<id>", description: "Explain one runtime agent (ids from `harness agents`)." },
|
|
3184
|
+
HARNESS_PATH_FLAG
|
|
3185
|
+
],
|
|
3186
|
+
examples: ["moda harness explain", "moda harness explain --agent-id=agent_support"]
|
|
3187
|
+
},
|
|
3188
|
+
{
|
|
3189
|
+
name: "write-summary",
|
|
3190
|
+
description: "Write .moda/HARNESS.md from the local harness graph (local)",
|
|
3191
|
+
usage: "moda harness write-summary [--path=<dir>]",
|
|
3192
|
+
flags: [HARNESS_PATH_FLAG],
|
|
3193
|
+
examples: ["moda harness write-summary"]
|
|
3194
|
+
},
|
|
3195
|
+
{
|
|
3196
|
+
name: "status",
|
|
3197
|
+
description: "Local harness state: graph hash, last sync, and whether the graph changed since (local)",
|
|
3198
|
+
usage: "moda harness status [--path=<dir>]",
|
|
3199
|
+
flags: [HARNESS_PATH_FLAG],
|
|
3200
|
+
examples: ["moda harness status"]
|
|
3201
|
+
},
|
|
3202
|
+
{
|
|
3203
|
+
name: "list",
|
|
3204
|
+
description: "List the tenant's synced harnesses (read-only)",
|
|
3205
|
+
usage: "moda harness list [--limit=N] [--offset=N]",
|
|
3206
|
+
flags: [
|
|
3207
|
+
{ name: "--limit=<n>", description: "Page size." },
|
|
3208
|
+
{ name: "--offset=<n>", description: "Rows to skip." }
|
|
3209
|
+
],
|
|
3210
|
+
examples: ["moda harness list"]
|
|
3211
|
+
},
|
|
3212
|
+
{
|
|
3213
|
+
name: "show",
|
|
3214
|
+
description: "Show one synced harness, at its current or a given version (read-only)",
|
|
3215
|
+
usage: "moda harness show <harness_id_or_key> [--version=<version_id>]",
|
|
3216
|
+
flags: [{ name: "--version=<version_id>", description: "Show this version instead of the current one." }],
|
|
3217
|
+
examples: ["moda harness show my-harness-key"]
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
name: "versions",
|
|
3221
|
+
description: "List a synced harness's versions (read-only)",
|
|
3222
|
+
usage: "moda harness versions <harness_id_or_key> [--limit=N] [--offset=N]",
|
|
3223
|
+
flags: [
|
|
3224
|
+
{ name: "--limit=<n>", description: "Page size." },
|
|
3225
|
+
{ name: "--offset=<n>", description: "Rows to skip." }
|
|
3226
|
+
],
|
|
3227
|
+
examples: ["moda harness versions my-harness-key"]
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
name: "diff",
|
|
3231
|
+
description: "Diff two versions of a synced harness (default: the latest against its parent) (read-only)",
|
|
3232
|
+
usage: "moda harness diff <harness_id_or_key> [--from=<version_id>] [--to=<version_id>]",
|
|
3233
|
+
flags: [
|
|
3234
|
+
{ name: "--from=<version_id>", description: "Older version." },
|
|
3235
|
+
{ name: "--to=<version_id>", description: "Newer version." }
|
|
3236
|
+
],
|
|
3237
|
+
examples: ["moda harness diff my-harness-key --from=hver_a --to=hver_b"]
|
|
3238
|
+
}
|
|
3239
|
+
];
|
|
3240
|
+
var PROBLEMS_SUBCOMMAND_HELP = [
|
|
3241
|
+
{
|
|
3242
|
+
name: "remainder",
|
|
3243
|
+
description: "Signals no Problem explains yet, grouped by family (read-only)",
|
|
3244
|
+
usage: "moda problems remainder [--family=<family>] [--days-back=N] [--limit=N] [--cursor=<token>]",
|
|
3245
|
+
flags: [
|
|
3246
|
+
{ name: "--family=<family>", description: "One of tool_failure, emotion, laziness, hallucination, prm_dip, custom_signal, conversation_reader (frustration is under emotion)." },
|
|
3247
|
+
{ name: "--days-back=<n>", description: "Window in days, 1-90 (default 30)." },
|
|
3248
|
+
{ name: "--limit=<n>", description: "Groups per page, 1-25 (default 25)." },
|
|
3249
|
+
{ name: "--cursor=<token>", description: "The next_cursor from the previous page, verbatim." }
|
|
3250
|
+
],
|
|
3251
|
+
examples: ["moda problems remainder", "moda problems remainder --family=emotion --days-back=7"]
|
|
3252
|
+
}
|
|
3253
|
+
];
|
|
3254
|
+
var FIX_TIMEOUT = { name: "--timeout=<ms>", description: "How long to wait, in milliseconds (max 86400000)." };
|
|
3255
|
+
var FIX_POLL = { name: "--poll-interval=<ms>", description: "Polling interval while waiting, in milliseconds." };
|
|
3256
|
+
var FIXES_SUBCOMMAND_HELP = [
|
|
3257
|
+
{
|
|
3258
|
+
name: "draft-batch",
|
|
3259
|
+
description: "Draft Fixes for the top unfixed Problems (no LLM call itself; spend happens when they are driven)",
|
|
3260
|
+
usage: "moda fixes draft-batch [--limit=N]",
|
|
3261
|
+
flags: [{ name: "--limit=N", description: "How many Problems to draft for (1-25, default 5)." }, TENANT_ID],
|
|
3262
|
+
examples: ["moda fixes draft-batch --limit=3"]
|
|
3263
|
+
},
|
|
3264
|
+
{
|
|
3265
|
+
name: "drive",
|
|
3266
|
+
description: "Advance active Fixes through scoping, proposing, and gating (paid: each fix runs LLM calls and replay playouts)",
|
|
3267
|
+
usage: "moda fixes drive [--status=S] [--max-fixes=N] [--timeout=ms] [--pass-interval=ms]",
|
|
3268
|
+
flags: [
|
|
3269
|
+
{ name: "--max-fixes=N", description: "Spend cap: advance at most N fixes this run (1-10, default 3). GATING fixes count too; each run takes the least-recently-driven fixes first, so re-runs reach every fix." },
|
|
3270
|
+
{ name: "--status=S", description: "Only drive fixes in this status." },
|
|
3271
|
+
FIX_TIMEOUT,
|
|
3272
|
+
{ name: "--pass-interval=<ms>", description: "Pause between passes, in milliseconds." },
|
|
3273
|
+
TENANT_ID
|
|
3274
|
+
],
|
|
3275
|
+
examples: ["moda fixes drive", "moda fixes drive --max-fixes=1 --status=SCOPING"]
|
|
3276
|
+
}
|
|
3277
|
+
];
|
|
3278
|
+
var FIX_SUBCOMMAND_HELP = [
|
|
3279
|
+
{
|
|
3280
|
+
name: "start",
|
|
3281
|
+
description: "Start a Fix for a Problem (paid: scoping, a proposal, and a replay gate)",
|
|
3282
|
+
usage: "moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
|
|
3283
|
+
flags: [
|
|
3284
|
+
{ name: "--type=auto|prompt", description: "Fix type (default auto)." },
|
|
3285
|
+
{ name: "--wait", description: "Wait for the gate verdict." },
|
|
3286
|
+
FIX_TIMEOUT,
|
|
3287
|
+
FIX_POLL,
|
|
3288
|
+
TENANT_ID
|
|
3289
|
+
],
|
|
3290
|
+
examples: ["moda fix start <problem_id> --wait"]
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
name: "verify",
|
|
3294
|
+
description: "Re-gate a Fix, optionally with a new candidate prompt (paid: replay playouts)",
|
|
3295
|
+
usage: "moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--yes]",
|
|
3296
|
+
flags: [
|
|
3297
|
+
{ name: "--prompt-file=PATH", description: "Candidate prompt to gate. New content skips the 10-minute re-gate cooldown; unchanged content does not." },
|
|
3298
|
+
{ name: "--no-wait", description: "Return once the gate is enqueued." },
|
|
3299
|
+
{ name: "--yes", description: "Re-gate inside the 10-minute cooldown anyway." },
|
|
3300
|
+
FIX_TIMEOUT,
|
|
3301
|
+
FIX_POLL,
|
|
3302
|
+
TENANT_ID
|
|
3303
|
+
],
|
|
3304
|
+
examples: ["moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md"]
|
|
3305
|
+
},
|
|
3306
|
+
{
|
|
3307
|
+
name: "checkout",
|
|
3308
|
+
description: "Write the Fix's candidate into your working tree",
|
|
3309
|
+
usage: "moda fix checkout <fix_id>",
|
|
3310
|
+
flags: [TENANT_ID],
|
|
3311
|
+
examples: ["moda fix checkout <fix_id>"]
|
|
3312
|
+
},
|
|
3313
|
+
{
|
|
3314
|
+
name: "submit",
|
|
3315
|
+
description: "Open a PR for a verified Fix, or record a local branch",
|
|
3316
|
+
usage: "moda fix submit <fix_id> --pr | --local-ref=<branch>",
|
|
3317
|
+
flags: [
|
|
3318
|
+
{ name: "--pr", description: "Open a pull request." },
|
|
3319
|
+
{ name: "--local-ref=<branch>", description: "Record a branch you pushed yourself." },
|
|
3320
|
+
TENANT_ID
|
|
3321
|
+
],
|
|
3322
|
+
examples: ["moda fix submit <fix_id> --pr"]
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
name: "mark-applied",
|
|
3326
|
+
description: "Mark a Fix as applied outside Moda",
|
|
3327
|
+
usage: "moda fix mark-applied <fix_id> [--note=TEXT]",
|
|
3328
|
+
flags: [{ name: "--note=TEXT", description: "Where or how it was applied." }, TENANT_ID],
|
|
3329
|
+
examples: ['moda fix mark-applied <fix_id> --note="tool description updated in our agent config"']
|
|
3330
|
+
},
|
|
3331
|
+
{
|
|
3332
|
+
name: "dismiss",
|
|
3333
|
+
description: "Dismiss a Fix (no dry-run mode)",
|
|
3334
|
+
usage: "moda fix dismiss <fix_id> --reason=TEXT",
|
|
3335
|
+
flags: [{ name: "--reason=TEXT", description: "Required. Why it is dismissed." }, TENANT_ID],
|
|
3336
|
+
examples: ['moda fix dismiss <fix_id> --reason="not actionable"']
|
|
3337
|
+
}
|
|
3338
|
+
];
|
|
3339
|
+
|
|
2006
3340
|
// src/intelligence-schema.ts
|
|
2007
3341
|
var INTELLIGENCE_SCHEMA_VERSION = "moda.intelligence.v1";
|
|
2008
3342
|
var provenanceSchema = { type: "string", enum: ["measured", "inferred"] };
|
|
@@ -2534,15 +3868,9 @@ async function probeApiKey(key, options = {}) {
|
|
|
2534
3868
|
}
|
|
2535
3869
|
|
|
2536
3870
|
// src/keys.ts
|
|
2537
|
-
import { createHash } from "node:crypto";
|
|
2538
|
-
import { existsSync as
|
|
3871
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3872
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
2539
3873
|
import { join as join3 } from "node:path";
|
|
2540
|
-
var KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
|
|
2541
|
-
list: "GET /api/cli/keys?profile=<profile>",
|
|
2542
|
-
create: "POST /api/cli/keys",
|
|
2543
|
-
rotate: "POST /api/cli/keys/<key_id>/rotate",
|
|
2544
|
-
revoke: "POST /api/cli/keys/<key_id>/revoke"
|
|
2545
|
-
};
|
|
2546
3874
|
function keySecretRef(profile, keyId) {
|
|
2547
3875
|
assertValidProfileName(profile);
|
|
2548
3876
|
assertSafeKeyId(keyId);
|
|
@@ -2608,9 +3936,9 @@ function keyDescriptorStorePath(options = {}) {
|
|
|
2608
3936
|
function loadLocalKeyDescriptors(options = {}) {
|
|
2609
3937
|
const path = keyDescriptorStorePath(options);
|
|
2610
3938
|
try {
|
|
2611
|
-
if (!
|
|
3939
|
+
if (!existsSync3(path))
|
|
2612
3940
|
return [];
|
|
2613
|
-
const parsed = JSON.parse(
|
|
3941
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2614
3942
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2615
3943
|
return [];
|
|
2616
3944
|
}
|
|
@@ -2648,7 +3976,7 @@ function upsertLocalKeyDescriptor(descriptor, options = {}) {
|
|
|
2648
3976
|
return next;
|
|
2649
3977
|
}
|
|
2650
3978
|
function stableKeyId(profile, name, now) {
|
|
2651
|
-
const hash =
|
|
3979
|
+
const hash = createHash2("sha256").update(`${profile}\x00${name}\x00${now.toISOString()}`).digest("hex").slice(0, 12);
|
|
2652
3980
|
return `key_${hash}`;
|
|
2653
3981
|
}
|
|
2654
3982
|
function assertSafeKeyId(keyId) {
|
|
@@ -2937,11 +4265,15 @@ async function runProfilesCommand(context) {
|
|
|
2937
4265
|
switch (subcommand(context)) {
|
|
2938
4266
|
case "list": {
|
|
2939
4267
|
const config = loadProfileConfig({ env: context.env });
|
|
4268
|
+
const { name: effective, source } = effectiveProfile(context, config.active_profile);
|
|
4269
|
+
const missing = !config.profiles[effective];
|
|
2940
4270
|
context.output.writeData({
|
|
2941
|
-
active_profile:
|
|
2942
|
-
|
|
4271
|
+
active_profile: effective,
|
|
4272
|
+
active_profile_source: source,
|
|
4273
|
+
default_profile: config.active_profile,
|
|
4274
|
+
profiles: Object.values(config.profiles).map((profile) => serializeProfile(profile, effective)),
|
|
2943
4275
|
migrations: config.migrations
|
|
2944
|
-
});
|
|
4276
|
+
}, missing ? { warnings: [`The active profile '${effective}' (from ${source}) does not exist. Create it with \`moda profiles create ${effective}\` or select an existing profile.`] } : undefined);
|
|
2945
4277
|
return { exitCode: 0 };
|
|
2946
4278
|
}
|
|
2947
4279
|
case "use": {
|
|
@@ -3011,36 +4343,55 @@ async function runProfilesCommand(context) {
|
|
|
3011
4343
|
throw unknownSubcommandError("profiles", subcommand(context), ["list", "use", "create", "doctor"]);
|
|
3012
4344
|
}
|
|
3013
4345
|
}
|
|
4346
|
+
function effectiveProfile(context, storedDefault) {
|
|
4347
|
+
const envProfile = context.env.MODA_PROFILE?.trim();
|
|
4348
|
+
if (context.profile)
|
|
4349
|
+
return { name: context.profile, source: "flag" };
|
|
4350
|
+
if (envProfile)
|
|
4351
|
+
return { name: envProfile, source: "env:MODA_PROFILE" };
|
|
4352
|
+
return { name: storedDefault, source: "config" };
|
|
4353
|
+
}
|
|
3014
4354
|
async function runConfigCommand(context) {
|
|
3015
4355
|
if (subcommand(context) !== "show") {
|
|
3016
4356
|
throw unknownSubcommandError("config", subcommand(context), ["show"]);
|
|
3017
4357
|
}
|
|
3018
4358
|
const config = loadProfileConfig({ env: context.env });
|
|
3019
4359
|
const profile = getActiveProfile({ profile: context.profile, env: context.env });
|
|
4360
|
+
const options = { profile: profile.name, env: context.env };
|
|
4361
|
+
const envProfile = context.env.MODA_PROFILE?.trim();
|
|
4362
|
+
const apiKey = resolveApiKey(options);
|
|
4363
|
+
const baseUrl = resolveModaBaseUrl(options);
|
|
3020
4364
|
context.output.writeData({
|
|
3021
4365
|
moda_dir: getModaPaths({ env: context.env }).dir,
|
|
3022
4366
|
active_profile: profile.name,
|
|
3023
|
-
|
|
4367
|
+
active_profile_source: context.profile ? "flag" : envProfile ? "env:MODA_PROFILE" : "config",
|
|
4368
|
+
default_profile: config.active_profile,
|
|
4369
|
+
profile: serializeProfile(profile, profile.name),
|
|
3024
4370
|
resolved: {
|
|
3025
|
-
api_key: redactSecret(
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
4371
|
+
api_key: redactSecret(apiKey),
|
|
4372
|
+
api_key_source: context.env.MODA_API_KEY ? "env:MODA_API_KEY" : profile.api_key_ref === "legacy:config.api_key" && apiKey ? "legacy_config" : resolveApiKeyFromActiveProfile(options) ? "profile" : apiKey ? "legacy_config" : "none",
|
|
4373
|
+
base_url: baseUrl,
|
|
4374
|
+
base_url_source: describeBaseUrlSource(options),
|
|
4375
|
+
ingest_url: resolveIngestUrl(options),
|
|
4376
|
+
tenant_id: resolveApiTenantId(options),
|
|
3029
4377
|
workspace_id: profile.workspace_id
|
|
3030
4378
|
},
|
|
3031
4379
|
migrations: config.migrations
|
|
3032
4380
|
});
|
|
3033
4381
|
return { exitCode: 0 };
|
|
3034
4382
|
}
|
|
4383
|
+
var KEYS_LOCAL_ONLY_NOTE = "moda keys manages local key descriptors in ~/.moda only; it does not list or change your tenant's server-side API keys (see Settings → Ingestion keys in the dashboard).";
|
|
3035
4384
|
async function runKeysCommand(context) {
|
|
3036
4385
|
switch (subcommand(context)) {
|
|
3037
4386
|
case "list": {
|
|
3038
|
-
const profile = context.profile ?? loadProfileConfig({ env: context.env }).active_profile;
|
|
4387
|
+
const profile = context.profile ?? (context.env.MODA_PROFILE?.trim() || undefined) ?? loadProfileConfig({ env: context.env }).active_profile;
|
|
3039
4388
|
const keys = loadLocalKeyDescriptors({ env: context.env }).filter((key) => !profile || key.profile === profile);
|
|
3040
4389
|
context.output.writeData({
|
|
4390
|
+
scope: "local_descriptors",
|
|
3041
4391
|
profile,
|
|
3042
|
-
keys
|
|
3043
|
-
|
|
4392
|
+
keys
|
|
4393
|
+
}, {
|
|
4394
|
+
warnings: [KEYS_LOCAL_ONLY_NOTE]
|
|
3044
4395
|
});
|
|
3045
4396
|
return { exitCode: 0 };
|
|
3046
4397
|
}
|
|
@@ -3061,7 +4412,9 @@ async function runKeysCommand(context) {
|
|
|
3061
4412
|
context.output.writeData({
|
|
3062
4413
|
key: descriptor,
|
|
3063
4414
|
secret_path: secretPath,
|
|
3064
|
-
|
|
4415
|
+
scope: "local_descriptors"
|
|
4416
|
+
}, {
|
|
4417
|
+
warnings: [KEYS_LOCAL_ONLY_NOTE]
|
|
3065
4418
|
});
|
|
3066
4419
|
return { exitCode: 0 };
|
|
3067
4420
|
}
|
|
@@ -3069,7 +4422,7 @@ async function runKeysCommand(context) {
|
|
|
3069
4422
|
case "revoke": {
|
|
3070
4423
|
const action = subcommand(context);
|
|
3071
4424
|
const keyId = requirePositional(context, 1, `Usage: moda keys ${action} <key_id>`);
|
|
3072
|
-
throw new CliInputError(`moda keys ${action} is
|
|
4425
|
+
throw new CliInputError(`moda keys ${action} is not supported: the CLI cannot ${action} server-side API keys (${keyId} was not changed).`, "Manage API keys in the dashboard at Settings → Ingestion keys.");
|
|
3073
4426
|
}
|
|
3074
4427
|
default:
|
|
3075
4428
|
throw unknownSubcommandError("keys", subcommand(context), ["list", "create", "rotate", "revoke"]);
|
|
@@ -3077,8 +4430,8 @@ async function runKeysCommand(context) {
|
|
|
3077
4430
|
}
|
|
3078
4431
|
|
|
3079
4432
|
// src/agent.ts
|
|
3080
|
-
import { existsSync as
|
|
3081
|
-
import { join as join4, relative, resolve as
|
|
4433
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
4434
|
+
import { join as join4, relative as relative2, resolve as resolve3 } from "node:path";
|
|
3082
4435
|
var MODA_DIR = ".moda";
|
|
3083
4436
|
var HARNESS_PATH = join4(MODA_DIR, "harness.json");
|
|
3084
4437
|
var HARNESS_REPORT_MARKDOWN_PATH = join4(MODA_DIR, "HARNESS_REPORT.md");
|
|
@@ -3093,25 +4446,8 @@ async function runAgentCommand(context) {
|
|
|
3093
4446
|
return { exitCode: 0 };
|
|
3094
4447
|
}
|
|
3095
4448
|
case "investigate": {
|
|
3096
|
-
const
|
|
3097
|
-
|
|
3098
|
-
context.output.writeData({
|
|
3099
|
-
schemaVersion: "agent_investigation.v0.1",
|
|
3100
|
-
generatedAt: loaded.payload.generatedAt,
|
|
3101
|
-
mode: "local",
|
|
3102
|
-
question: question || null,
|
|
3103
|
-
status: loaded.payload.doctor.status,
|
|
3104
|
-
findings: loaded.payload.doctor.topFindings,
|
|
3105
|
-
recommendedActions: loaded.payload.recommendedActions,
|
|
3106
|
-
evidenceRefs: loaded.payload.evidenceRefs,
|
|
3107
|
-
limitations: [
|
|
3108
|
-
...loaded.payload.limitations,
|
|
3109
|
-
"Production investigation is not yet implemented in P08; use P09/cloud commands for production-rooted analysis once shipped."
|
|
3110
|
-
]
|
|
3111
|
-
}, agentWriteOptions(loaded, {
|
|
3112
|
-
summary: summarizeAgentContext(loaded.payload, "investigation")
|
|
3113
|
-
}));
|
|
3114
|
-
return { exitCode: 0 };
|
|
4449
|
+
const passthrough = Object.entries(context.flags).filter(([key]) => ["days-back", "tool", "trace", "conversation"].includes(key)).map(([key, value]) => `--${key}=${value}`);
|
|
4450
|
+
throw new CliInputError("'moda agent investigate' has been removed. Use 'moda investigate' for the ranked production investigation.", `Run: ${["moda investigate", ...passthrough].join(" ")} (flags: --days-back, --tool, --trace). For local readiness use 'moda agent context'.`);
|
|
3115
4451
|
}
|
|
3116
4452
|
case "next-action":
|
|
3117
4453
|
case "next-actions": {
|
|
@@ -3161,7 +4497,7 @@ async function runAgentCommand(context) {
|
|
|
3161
4497
|
return { exitCode: 0 };
|
|
3162
4498
|
}
|
|
3163
4499
|
default:
|
|
3164
|
-
throw new CliInputError(`Unknown agent subcommand '${subcommand2}'`, "Usage: moda agent context|
|
|
4500
|
+
throw new CliInputError(`Unknown agent subcommand '${subcommand2}'`, "Usage: moda agent context|next-action|evidence (for production issues use moda investigate)");
|
|
3165
4501
|
}
|
|
3166
4502
|
}
|
|
3167
4503
|
async function buildAgentContext(context) {
|
|
@@ -3263,17 +4599,17 @@ function summarizeAgentContext(payload, noun) {
|
|
|
3263
4599
|
}
|
|
3264
4600
|
function readHarnessGraphIfPresent(cwd) {
|
|
3265
4601
|
const path = join4(cwd, HARNESS_PATH);
|
|
3266
|
-
if (!
|
|
4602
|
+
if (!existsSync4(path))
|
|
3267
4603
|
return;
|
|
3268
4604
|
try {
|
|
3269
|
-
return JSON.parse(
|
|
4605
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
3270
4606
|
} catch {
|
|
3271
4607
|
return;
|
|
3272
4608
|
}
|
|
3273
4609
|
}
|
|
3274
4610
|
function readHarnessReportState(cwd) {
|
|
3275
4611
|
const path = join4(cwd, HARNESS_REPORT_PATH);
|
|
3276
|
-
if (!
|
|
4612
|
+
if (!existsSync4(path)) {
|
|
3277
4613
|
return {
|
|
3278
4614
|
state: {
|
|
3279
4615
|
path: HARNESS_REPORT_PATH,
|
|
@@ -3318,10 +4654,10 @@ function readHarnessReportState(cwd) {
|
|
|
3318
4654
|
}
|
|
3319
4655
|
function readApproval(cwd) {
|
|
3320
4656
|
const path = join4(cwd, HARNESS_REPORT_APPROVAL_PATH);
|
|
3321
|
-
if (!
|
|
4657
|
+
if (!existsSync4(path))
|
|
3322
4658
|
return;
|
|
3323
4659
|
try {
|
|
3324
|
-
return JSON.parse(
|
|
4660
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
3325
4661
|
} catch {
|
|
3326
4662
|
return;
|
|
3327
4663
|
}
|
|
@@ -3399,7 +4735,7 @@ function existingArtifacts(cwd) {
|
|
|
3399
4735
|
[HARNESS_SUMMARY_PATH, "harness_summary", "Human-readable harness graph summary."],
|
|
3400
4736
|
[join4(MODA_DIR, "sync-state.json"), "sync_state", "Last local harness sync state."],
|
|
3401
4737
|
[join4(MODA_DIR, "prompts.yml"), "prompt_manifest", "Local prompt discovery manifest."]
|
|
3402
|
-
].flatMap(([path, kind, description]) =>
|
|
4738
|
+
].flatMap(([path, kind, description]) => existsSync4(join4(cwd, path)) ? [{ path, kind, description }] : []);
|
|
3403
4739
|
}
|
|
3404
4740
|
function expandEvidenceRef(ref, loaded, cwd) {
|
|
3405
4741
|
if (ref.startsWith("doctor:")) {
|
|
@@ -3480,9 +4816,9 @@ function readCitationExcerpt(cwd, citation) {
|
|
|
3480
4816
|
if (!citation.path || citation.type !== "file" && citation.type !== "generated_doc")
|
|
3481
4817
|
return citation.excerpt;
|
|
3482
4818
|
const absolute = safeResolve(cwd, citation.path);
|
|
3483
|
-
if (!absolute || !
|
|
4819
|
+
if (!absolute || !existsSync4(absolute))
|
|
3484
4820
|
return citation.excerpt;
|
|
3485
|
-
const text =
|
|
4821
|
+
const text = readFileSync5(absolute, "utf8");
|
|
3486
4822
|
if (citation.line && citation.line > 0) {
|
|
3487
4823
|
return text.split(/\r?\n/)[citation.line - 1]?.slice(0, 1000) ?? citation.excerpt;
|
|
3488
4824
|
}
|
|
@@ -3493,10 +4829,10 @@ function readBoundedModaArtifact(cwd, artifactPath) {
|
|
|
3493
4829
|
throw new CliInputError(`Artifact '${artifactPath}' is outside .moda and cannot be expanded directly.`, "Use a citation evidence ref for source-file evidence.");
|
|
3494
4830
|
}
|
|
3495
4831
|
const absolute = safeResolve(cwd, artifactPath);
|
|
3496
|
-
if (!absolute || !
|
|
4832
|
+
if (!absolute || !existsSync4(absolute))
|
|
3497
4833
|
throw missingEvidence(`artifact:${artifactPath}`);
|
|
3498
|
-
const size =
|
|
3499
|
-
const text =
|
|
4834
|
+
const size = statSync2(absolute).size;
|
|
4835
|
+
const text = readFileSync5(absolute, "utf8");
|
|
3500
4836
|
return {
|
|
3501
4837
|
path: artifactPath,
|
|
3502
4838
|
bytes: size,
|
|
@@ -3505,8 +4841,8 @@ function readBoundedModaArtifact(cwd, artifactPath) {
|
|
|
3505
4841
|
};
|
|
3506
4842
|
}
|
|
3507
4843
|
function safeResolve(cwd, path) {
|
|
3508
|
-
const absolute =
|
|
3509
|
-
const rel =
|
|
4844
|
+
const absolute = resolve3(cwd, path);
|
|
4845
|
+
const rel = relative2(cwd, absolute);
|
|
3510
4846
|
if (rel.startsWith("..") || rel === ".." || rel.startsWith("/"))
|
|
3511
4847
|
return;
|
|
3512
4848
|
return absolute;
|
|
@@ -3539,29 +4875,25 @@ function purposeForCommand(command) {
|
|
|
3539
4875
|
}
|
|
3540
4876
|
|
|
3541
4877
|
// src/production-intelligence.ts
|
|
3542
|
-
import { existsSync as
|
|
4878
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
3543
4879
|
import { join as join5 } from "node:path";
|
|
3544
4880
|
|
|
3545
4881
|
// src/ask-stream.ts
|
|
3546
4882
|
class AskStreamError extends Error {
|
|
3547
4883
|
elapsedMs;
|
|
3548
4884
|
toolEvents;
|
|
4885
|
+
accepted;
|
|
3549
4886
|
constructor(message) {
|
|
3550
4887
|
super(message);
|
|
3551
4888
|
this.name = "AskStreamError";
|
|
3552
4889
|
}
|
|
3553
4890
|
}
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
const toolEvents = error.toolEvents ?? 0;
|
|
3561
|
-
if (elapsedMs < SUBSTANTIAL_STREAM_PROGRESS_MS && toolEvents <= SUBSTANTIAL_STREAM_TOOL_EVENTS) {
|
|
3562
|
-
return null;
|
|
3563
|
-
}
|
|
3564
|
-
return `stream interrupted after ${Math.round(elapsedMs / 1000)}s; ` + "retrying via buffered request — this may take another minute or two.";
|
|
4891
|
+
function canFallBackToBufferedAsk(error) {
|
|
4892
|
+
return error instanceof AskStreamError && error.accepted !== true;
|
|
4893
|
+
}
|
|
4894
|
+
function askStreamInterruptedError(error) {
|
|
4895
|
+
const secs = Math.round((error.elapsedMs ?? 0) / 1000);
|
|
4896
|
+
return new Error(`The live ask stream dropped ${secs}s into a run the server had already accepted (${error.message}). ` + "Not re-running it, to avoid paying for a second agent run; the run may still complete server-side. " + 'Retry with `moda ask "<question>" --no-stream` in a minute (the buffered path has no live progress but survives drops).');
|
|
3565
4897
|
}
|
|
3566
4898
|
|
|
3567
4899
|
class SseParser {
|
|
@@ -3677,6 +5009,7 @@ class AskStreamCollector {
|
|
|
3677
5009
|
lastPhase;
|
|
3678
5010
|
toolNames = new Map;
|
|
3679
5011
|
toolEvents = 0;
|
|
5012
|
+
events = 0;
|
|
3680
5013
|
constructor(onProgress = () => {}) {
|
|
3681
5014
|
this.onProgress = onProgress;
|
|
3682
5015
|
}
|
|
@@ -3687,6 +5020,7 @@ class AskStreamCollector {
|
|
|
3687
5020
|
if (!event || typeof event !== "object" || typeof event.type !== "string") {
|
|
3688
5021
|
throw new AskStreamError("stream emitted a frame without an event type");
|
|
3689
5022
|
}
|
|
5023
|
+
this.events += 1;
|
|
3690
5024
|
switch (event.type) {
|
|
3691
5025
|
case "phase": {
|
|
3692
5026
|
const phase = typeof event.phase === "string" ? event.phase : "";
|
|
@@ -3752,6 +5086,9 @@ class AskStreamCollector {
|
|
|
3752
5086
|
isTerminal() {
|
|
3753
5087
|
return this.terminal;
|
|
3754
5088
|
}
|
|
5089
|
+
get eventCount() {
|
|
5090
|
+
return this.events;
|
|
5091
|
+
}
|
|
3755
5092
|
get toolEventCount() {
|
|
3756
5093
|
return this.toolEvents;
|
|
3757
5094
|
}
|
|
@@ -3848,6 +5185,7 @@ async function streamAsk(options) {
|
|
|
3848
5185
|
const collector = new AskStreamCollector(onProgress);
|
|
3849
5186
|
const ticker = new IdleTicker(onProgress);
|
|
3850
5187
|
ticker.start();
|
|
5188
|
+
let accepted = false;
|
|
3851
5189
|
try {
|
|
3852
5190
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
3853
5191
|
let response;
|
|
@@ -3877,6 +5215,7 @@ async function streamAsk(options) {
|
|
|
3877
5215
|
if (!contentType.includes("text/event-stream")) {
|
|
3878
5216
|
throw new AskStreamError(`stream endpoint returned unexpected content type '${contentType || "unknown"}'`);
|
|
3879
5217
|
}
|
|
5218
|
+
accepted = true;
|
|
3880
5219
|
if (!response.body) {
|
|
3881
5220
|
throw new AskStreamError("stream response had no body");
|
|
3882
5221
|
}
|
|
@@ -3893,6 +5232,8 @@ async function streamAsk(options) {
|
|
|
3893
5232
|
error.elapsedMs = Date.now() - startedAt;
|
|
3894
5233
|
if (error.toolEvents === undefined)
|
|
3895
5234
|
error.toolEvents = collector.toolEventCount;
|
|
5235
|
+
if (error.accepted === undefined)
|
|
5236
|
+
error.accepted = accepted || collector.eventCount > 0;
|
|
3896
5237
|
}
|
|
3897
5238
|
throw error;
|
|
3898
5239
|
} finally {
|
|
@@ -3905,8 +5246,10 @@ async function streamAsk(options) {
|
|
|
3905
5246
|
var INVESTIGATION_SCHEMA_VERSION = "production_investigation.v0.1";
|
|
3906
5247
|
var OVERVIEW_SCHEMA_VERSION = "production_overview.v0.1";
|
|
3907
5248
|
var ASK_SCHEMA_VERSION = "production_ask.v0.1";
|
|
5249
|
+
var INVESTIGATION_PROBLEM_LIMIT = 3;
|
|
5250
|
+
var INVESTIGATION_PROBLEM_SLOTS = INVESTIGATION_PROBLEM_LIMIT;
|
|
3908
5251
|
async function runProductionCommand(context) {
|
|
3909
|
-
const daysBack =
|
|
5252
|
+
const daysBack = daysBackFlag(context.flags);
|
|
3910
5253
|
if (context.command === "overview") {
|
|
3911
5254
|
const briefing = await loadOverviewBriefing({ daysBack, cwd: context.cwd });
|
|
3912
5255
|
if (context.flags.raw === "true") {
|
|
@@ -3925,7 +5268,7 @@ async function runProductionCommand(context) {
|
|
|
3925
5268
|
if (context.command === "ask") {
|
|
3926
5269
|
const question = context.positionals.join(" ").trim();
|
|
3927
5270
|
if (!question) {
|
|
3928
|
-
throw new CliInputError("<question> is required", 'Usage: moda ask "what should I fix first?" [--days-back=N]');
|
|
5271
|
+
throw new CliInputError("<question> is required", 'Usage: moda ask "what should I fix first?" [--days-back=N] [--no-stream]');
|
|
3929
5272
|
}
|
|
3930
5273
|
const result = await askModaForContext(context, question, daysBack);
|
|
3931
5274
|
if (context.outputMode === "human") {
|
|
@@ -4006,30 +5349,45 @@ async function investigateProduction(options) {
|
|
|
4006
5349
|
const hints = loadHarnessHints(options.cwd);
|
|
4007
5350
|
const overviewEndpoint = `/overview?days_back=${options.daysBack}`;
|
|
4008
5351
|
const toolEndpoint = options.toolName ? `/tool-failures/${encodeURIComponent(options.toolName)}?days_back=${options.daysBack}&limit=5` : `/tool-failures?days_back=${options.daysBack}`;
|
|
4009
|
-
const frustrationsEndpoint = `/
|
|
4010
|
-
const
|
|
5352
|
+
const frustrationsEndpoint = `/emotions?family=frustration&days_back=${options.daysBack}&limit=5${options.conversationId ? `&conversation_id=${encodeURIComponent(options.conversationId)}` : ""}`;
|
|
5353
|
+
const problemsEndpoint = `/problems?days_back=${options.daysBack}&limit=${INVESTIGATION_PROBLEM_LIMIT}`;
|
|
5354
|
+
const skipped = { data: null, status: "ok" };
|
|
5355
|
+
const [overview, tools, frustrations, problems] = await Promise.all([
|
|
4011
5356
|
safeData(overviewEndpoint),
|
|
4012
5357
|
safeData(toolEndpoint),
|
|
4013
|
-
safeData(frustrationsEndpoint)
|
|
5358
|
+
options.failureOnly ? Promise.resolve(skipped) : safeData(frustrationsEndpoint),
|
|
5359
|
+
options.failureOnly ? Promise.resolve(skipped) : safeData(problemsEndpoint)
|
|
4014
5360
|
]);
|
|
4015
|
-
|
|
5361
|
+
const sources = options.failureOnly ? { overview, tools } : { overview, tools, frustrations, problems };
|
|
5362
|
+
for (const [key, result] of Object.entries(sources)) {
|
|
4016
5363
|
sourceStatus[key] = result.status;
|
|
4017
5364
|
if (result.warning)
|
|
4018
5365
|
warnings.push(result.warning);
|
|
4019
5366
|
}
|
|
5367
|
+
if (options.toolName && tools.status === "ok" && toolRows(tools.data, options.toolName).every((row) => row.count <= 0)) {
|
|
5368
|
+
warnings.push(`No failures found for tool "${options.toolName}" in the last ${options.daysBack} day(s) — the tool may have no failures in this window, or the name may not exist in this tenant. ` + "Run `moda tool-failures` to list tools with recorded failures, or widen --days-back.");
|
|
5369
|
+
}
|
|
5370
|
+
if (options.conversationId) {
|
|
5371
|
+
warnings.push(`Scoped to trace ${options.conversationId} for frustration evidence only; overview and tool failures are tenant-wide. ` + `Open the trace itself with \`moda audit ${options.conversationId}\`.`);
|
|
5372
|
+
}
|
|
4020
5373
|
const evidenceRefs = [];
|
|
4021
5374
|
const findings = [];
|
|
4022
5375
|
addOverviewEvidence(overview.data, overviewEndpoint, evidenceRefs);
|
|
4023
5376
|
findings.push(...toolFailureFindings(tools.data, options.daysBack, hints, evidenceRefs, options.toolName));
|
|
4024
5377
|
if (!options.failureOnly) {
|
|
4025
5378
|
findings.push(...frustrationFindings(frustrations.data, options.daysBack, evidenceRefs));
|
|
5379
|
+
findings.push(...problemFindings(problems.data, options.daysBack, evidenceRefs));
|
|
4026
5380
|
}
|
|
4027
5381
|
if (findings.length === 0) {
|
|
4028
5382
|
findings.push(insufficientDataFinding(overview.data, evidenceRefs));
|
|
4029
5383
|
}
|
|
4030
5384
|
findings.sort(compareFindings);
|
|
4031
|
-
const boundedFindings = findings
|
|
4032
|
-
const nextCommands = nextCommandsForFindings(boundedFindings, options.daysBack);
|
|
5385
|
+
const boundedFindings = capFindings(findings, 8, INVESTIGATION_PROBLEM_SLOTS);
|
|
5386
|
+
const nextCommands = nextCommandsForFindings(boundedFindings, options.daysBack, !options.failureOnly);
|
|
5387
|
+
const summary = summarizeInvestigation(boundedFindings);
|
|
5388
|
+
if (options.conversationId) {
|
|
5389
|
+
summary.text = `${summary.text} (Only frustration evidence is scoped to trace ${options.conversationId}.)`;
|
|
5390
|
+
}
|
|
4033
5391
|
return {
|
|
4034
5392
|
schemaVersion: INVESTIGATION_SCHEMA_VERSION,
|
|
4035
5393
|
generatedAt: new Date().toISOString(),
|
|
@@ -4039,7 +5397,7 @@ async function investigateProduction(options) {
|
|
|
4039
5397
|
...options.toolName ? { toolName: options.toolName } : {},
|
|
4040
5398
|
...options.question ? { question: options.question } : {}
|
|
4041
5399
|
},
|
|
4042
|
-
summary
|
|
5400
|
+
summary,
|
|
4043
5401
|
findings: boundedFindings,
|
|
4044
5402
|
evidenceRefs: uniqueEvidenceRefs(evidenceRefs),
|
|
4045
5403
|
nextCommands,
|
|
@@ -4140,11 +5498,10 @@ async function askModaForContext(context, question, daysBack) {
|
|
|
4140
5498
|
} catch (error) {
|
|
4141
5499
|
if (error instanceof CliAuthError)
|
|
4142
5500
|
throw error;
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
} else if (context.verbose) {
|
|
5501
|
+
if (error instanceof AskStreamError && !canFallBackToBufferedAsk(error)) {
|
|
5502
|
+
throw askStreamInterruptedError(error);
|
|
5503
|
+
}
|
|
5504
|
+
if (context.verbose) {
|
|
4148
5505
|
safeWriteStderr(`[moda] live progress unavailable (${error instanceof Error ? error.message : String(error)}); using buffered ask.
|
|
4149
5506
|
`);
|
|
4150
5507
|
}
|
|
@@ -4330,7 +5687,7 @@ function toolFailureFindings(data, daysBack, hints, evidenceRefs, scopedTool) {
|
|
|
4330
5687
|
id: `action_tool_detail_${slug(name)}`,
|
|
4331
5688
|
title: `Inspect ${name} failure examples.`,
|
|
4332
5689
|
rationale: "Examples include trace anchors and error subtypes.",
|
|
4333
|
-
command: `moda tool-failure-detail ${name} --include-window`,
|
|
5690
|
+
command: `moda tool-failure-detail ${name} --days-back=${daysBack} --include-window`,
|
|
4334
5691
|
mutability: "read",
|
|
4335
5692
|
requiresApproval: false
|
|
4336
5693
|
}]
|
|
@@ -4338,6 +5695,7 @@ function toolFailureFindings(data, daysBack, hints, evidenceRefs, scopedTool) {
|
|
|
4338
5695
|
});
|
|
4339
5696
|
}
|
|
4340
5697
|
function frustrationFindings(data, daysBack, evidenceRefs) {
|
|
5698
|
+
data = frustrationsFromEmotions(asRecord(data) ?? {});
|
|
4341
5699
|
const summary = asRecord(valueAt(data, ["summary"]));
|
|
4342
5700
|
const frustrations = Array.isArray(valueAt(data, ["frustrations"])) ? valueAt(data, ["frustrations"]) : [];
|
|
4343
5701
|
const count = numberValue(summary?.frustrated_count) || frustrations.length;
|
|
@@ -4347,12 +5705,13 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
|
|
|
4347
5705
|
const first = asRecord(frustrations[0]);
|
|
4348
5706
|
const conversationId = asString(first?.conversation_id);
|
|
4349
5707
|
const quote = firstQuote(first);
|
|
5708
|
+
const anchorIndex = frustrationAnchorIndex(first);
|
|
4350
5709
|
const evidenceId = conversationId ? `frustration:${conversationId}` : "frustration:summary";
|
|
4351
5710
|
evidenceRefs.push({
|
|
4352
5711
|
id: evidenceId,
|
|
4353
5712
|
kind: "frustration",
|
|
4354
5713
|
label: quote ? `Frustration anchor: "${quote}"` : `${count} frustrated trace(s)`,
|
|
4355
|
-
endpoint: `/
|
|
5714
|
+
endpoint: `/emotions?family=frustration&days_back=${daysBack}&limit=5`,
|
|
4356
5715
|
...conversationId ? { conversationId } : {},
|
|
4357
5716
|
value: first ?? summary ?? data
|
|
4358
5717
|
});
|
|
@@ -4371,12 +5730,78 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
|
|
|
4371
5730
|
id: "action_frustration_window",
|
|
4372
5731
|
title: "Read the frustration window.",
|
|
4373
5732
|
rationale: "The user quote and nearby turns reveal what confused or blocked the user.",
|
|
4374
|
-
command: conversationId ? `moda context ${conversationId} --window=3` : `moda frustrations --days-back=${daysBack} --include-window`,
|
|
5733
|
+
command: conversationId ? `moda context ${conversationId}${anchorIndex !== null ? ` --msg-index=${anchorIndex}` : ""} --window=3` : `moda frustrations --days-back=${daysBack} --include-window`,
|
|
4375
5734
|
mutability: "read",
|
|
4376
5735
|
requiresApproval: false
|
|
4377
5736
|
}]
|
|
4378
5737
|
}];
|
|
4379
5738
|
}
|
|
5739
|
+
function problemRows(data) {
|
|
5740
|
+
const record = asRecord(data);
|
|
5741
|
+
const rows = [];
|
|
5742
|
+
const push = (items, trust) => {
|
|
5743
|
+
if (!Array.isArray(items))
|
|
5744
|
+
return;
|
|
5745
|
+
for (const item of items) {
|
|
5746
|
+
const row = asRecord(item);
|
|
5747
|
+
const id = asString(row?.problem_id);
|
|
5748
|
+
if (!row || !id)
|
|
5749
|
+
continue;
|
|
5750
|
+
const lifecycle = asString(row.lifecycle) ?? "open";
|
|
5751
|
+
if (lifecycle === "resolved")
|
|
5752
|
+
continue;
|
|
5753
|
+
rows.push({
|
|
5754
|
+
id,
|
|
5755
|
+
name: asString(row.display_name) || id,
|
|
5756
|
+
cause: asString(row.cause_statement) || asString(row.display_description) || undefined,
|
|
5757
|
+
lifecycle,
|
|
5758
|
+
trust,
|
|
5759
|
+
verification: asString(row.verification_status) ?? undefined,
|
|
5760
|
+
affectedConversations: numberValue(row.affected_conversations) || numberValue(asRecord(row.evidence_support)?.attribution_count),
|
|
5761
|
+
raw: row
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
};
|
|
5765
|
+
push(record?.problems, "trusted");
|
|
5766
|
+
push(record?.untrusted, "untrusted");
|
|
5767
|
+
push(record?.emerging, "emerging");
|
|
5768
|
+
return rows.slice(0, INVESTIGATION_PROBLEM_LIMIT);
|
|
5769
|
+
}
|
|
5770
|
+
function problemFindings(data, daysBack, evidenceRefs) {
|
|
5771
|
+
return problemRows(data).map((problem, index) => {
|
|
5772
|
+
const evidenceId = `problem:${problem.id}`;
|
|
5773
|
+
const trusted = problem.trust === "trusted";
|
|
5774
|
+
const trustNote = trusted ? "verified" : problem.trust === "emerging" ? "emerging, not yet ranked" : `untrusted (${problem.verification ?? "unverified"})`;
|
|
5775
|
+
evidenceRefs.push({
|
|
5776
|
+
id: evidenceId,
|
|
5777
|
+
kind: "problem",
|
|
5778
|
+
label: `Problem "${problem.name}" (${trustNote})`,
|
|
5779
|
+
endpoint: `/problems?days_back=${daysBack}&limit=${INVESTIGATION_PROBLEM_LIMIT}`,
|
|
5780
|
+
value: problem.raw
|
|
5781
|
+
});
|
|
5782
|
+
const affected = problem.affectedConversations;
|
|
5783
|
+
return {
|
|
5784
|
+
id: `finding_problem_${slug(problem.id)}`,
|
|
5785
|
+
kind: "problem",
|
|
5786
|
+
title: `Problem: ${problem.name}`,
|
|
5787
|
+
severity: trusted ? affected >= 10 ? "critical" : "high" : "medium",
|
|
5788
|
+
confidence: trusted ? "high" : "low",
|
|
5789
|
+
impactScore: (trusted ? 100 : 60) + Math.min(affected, 25) - index,
|
|
5790
|
+
rankReason: trusted ? `Rank #${index + 1} verified Problem in \`moda problems\`${affected > 0 ? `, affecting ${affected} trace(s)` : ""}.` : `Listed by \`moda problems\` as ${trustNote}; treat the cause as a hypothesis.`,
|
|
5791
|
+
summary: problem.cause ? `${problem.cause}${problem.lifecycle !== "open" ? ` (${problem.lifecycle})` : ""}` : `Problem ${problem.id} is ${problem.lifecycle} in the last ${daysBack} day(s).`,
|
|
5792
|
+
evidenceRefIds: [evidenceId],
|
|
5793
|
+
likelyLocations: [],
|
|
5794
|
+
recommendedActions: [{
|
|
5795
|
+
id: `action_problem_${slug(problem.id)}`,
|
|
5796
|
+
title: `Open the Problem dossier for ${problem.name}.`,
|
|
5797
|
+
rationale: "The dossier has the cause, evidence, affected traces and verification state.",
|
|
5798
|
+
command: `moda problem ${problem.id}`,
|
|
5799
|
+
mutability: "read",
|
|
5800
|
+
requiresApproval: false
|
|
5801
|
+
}]
|
|
5802
|
+
};
|
|
5803
|
+
});
|
|
5804
|
+
}
|
|
4380
5805
|
function insufficientDataFinding(overview, evidenceRefs) {
|
|
4381
5806
|
const evidenceId = "overview:data";
|
|
4382
5807
|
if (!evidenceRefs.some((ref) => ref.id === evidenceId)) {
|
|
@@ -4879,11 +6304,14 @@ function toolRows(data, scopedTool) {
|
|
|
4879
6304
|
const directToolName = asString(record?.tool_name) ?? scopedTool;
|
|
4880
6305
|
const examples = Array.isArray(record?.examples) ? record.examples : [];
|
|
4881
6306
|
if (directToolName) {
|
|
6307
|
+
const subtypes = (Array.isArray(record?.subtypes) ? record.subtypes : []).map((item) => asRecord(item)).filter((item) => item !== null && item !== undefined);
|
|
6308
|
+
const subtypeTotal = subtypes.reduce((sum, item) => sum + numberValue(item.count), 0);
|
|
6309
|
+
const top = [...subtypes].sort((left, right) => numberValue(right.count) - numberValue(left.count))[0];
|
|
4882
6310
|
return [{
|
|
4883
6311
|
name: directToolName,
|
|
4884
|
-
count: numberValue(record?.failure_count) || numberValue(record?.total_failures) || examples.length,
|
|
4885
|
-
conversations: numberValue(record?.failed_conversations) || numberValue(record?.conversation_count) || examples.length,
|
|
4886
|
-
subtype: asString(record?.top_error_subtype) ?? asString(record?.error_subtype) ?? undefined,
|
|
6312
|
+
count: numberValue(record?.failure_count) || numberValue(record?.total_failures) || numberValue(asRecord(record?.pagination)?.total) || subtypeTotal || examples.length,
|
|
6313
|
+
conversations: numberValue(record?.failed_conversations) || numberValue(record?.conversation_count) || Math.max(0, ...subtypes.map((item) => numberValue(item.conversation_count))) || examples.length,
|
|
6314
|
+
subtype: asString(record?.top_error_subtype) ?? asString(record?.error_subtype) ?? asString(top?.subtype) ?? undefined,
|
|
4887
6315
|
raw: data
|
|
4888
6316
|
}];
|
|
4889
6317
|
}
|
|
@@ -4961,11 +6389,11 @@ function loadHarnessHints(cwd) {
|
|
|
4961
6389
|
entrypointPaths: []
|
|
4962
6390
|
};
|
|
4963
6391
|
const harnessPath = join5(cwd, ".moda", "harness.json");
|
|
4964
|
-
if (!
|
|
6392
|
+
if (!existsSync5(harnessPath))
|
|
4965
6393
|
return hints;
|
|
4966
6394
|
hints.reportPath = ".moda/harness.json";
|
|
4967
6395
|
try {
|
|
4968
|
-
const parsed = JSON.parse(
|
|
6396
|
+
const parsed = JSON.parse(readFileSync6(harnessPath, "utf8"));
|
|
4969
6397
|
const graph = asRecord(parsed);
|
|
4970
6398
|
const artifacts = Array.isArray(graph?.artifacts) ? graph.artifacts : [];
|
|
4971
6399
|
for (const item of artifacts) {
|
|
@@ -4994,10 +6422,10 @@ function loadHarnessHints(cwd) {
|
|
|
4994
6422
|
}
|
|
4995
6423
|
function localHarnessSummary(cwd) {
|
|
4996
6424
|
const path = join5(cwd, ".moda", "harness.json");
|
|
4997
|
-
if (!
|
|
6425
|
+
if (!existsSync5(path))
|
|
4998
6426
|
return null;
|
|
4999
6427
|
try {
|
|
5000
|
-
const parsed = JSON.parse(
|
|
6428
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
5001
6429
|
return {
|
|
5002
6430
|
harnessId: parsed.harnessId,
|
|
5003
6431
|
graphHash: parsed.graphHash,
|
|
@@ -5009,7 +6437,7 @@ function localHarnessSummary(cwd) {
|
|
|
5009
6437
|
}
|
|
5010
6438
|
function localSyncBrief(cwd, relativePath, label) {
|
|
5011
6439
|
const path = join5(cwd, relativePath);
|
|
5012
|
-
if (!
|
|
6440
|
+
if (!existsSync5(path)) {
|
|
5013
6441
|
return { status: "missing", summary: `${label} state not found`, path: relativePath };
|
|
5014
6442
|
}
|
|
5015
6443
|
return { status: "present", summary: `${label} state exists locally`, path: relativePath };
|
|
@@ -5017,7 +6445,7 @@ function localSyncBrief(cwd, relativePath, label) {
|
|
|
5017
6445
|
function localSkillBrief(cwd) {
|
|
5018
6446
|
const claudeSkill = join5(cwd, ".claude", "skills");
|
|
5019
6447
|
const cursorRules = join5(cwd, ".cursor", "rules");
|
|
5020
|
-
if (
|
|
6448
|
+
if (existsSync5(claudeSkill) || existsSync5(cursorRules)) {
|
|
5021
6449
|
return { status: "present", summary: "Local agent skill/rule directory exists" };
|
|
5022
6450
|
}
|
|
5023
6451
|
return { status: "missing", summary: "No local agent skill/rule directory found" };
|
|
@@ -5035,11 +6463,35 @@ function summarizeInvestigation(findings) {
|
|
|
5035
6463
|
confidence: top.confidence
|
|
5036
6464
|
};
|
|
5037
6465
|
}
|
|
5038
|
-
function nextCommandsForFindings(findings, daysBack) {
|
|
5039
|
-
const
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
6466
|
+
function nextCommandsForFindings(findings, daysBack, includeProblems = false) {
|
|
6467
|
+
const limit = 8;
|
|
6468
|
+
const primary = [...new Set(findings.map((finding) => finding.recommendedActions[0]?.command).filter(Boolean))];
|
|
6469
|
+
const generic = [
|
|
6470
|
+
...includeProblems ? [`moda problems --days-back=${daysBack}`] : [],
|
|
6471
|
+
`moda overview --days-back=${daysBack}`,
|
|
6472
|
+
"moda doctor --online"
|
|
6473
|
+
];
|
|
6474
|
+
const secondary = findings.flatMap((finding) => finding.recommendedActions.slice(1).map((action) => action.command)).filter(Boolean);
|
|
6475
|
+
const keep = new Set(primary);
|
|
6476
|
+
for (const command of [...generic, ...secondary]) {
|
|
6477
|
+
if (keep.size >= Math.max(limit, primary.length))
|
|
6478
|
+
break;
|
|
6479
|
+
keep.add(command);
|
|
6480
|
+
}
|
|
6481
|
+
const ordered = [
|
|
6482
|
+
...findings.flatMap((finding) => finding.recommendedActions.map((action) => action.command)).filter(Boolean),
|
|
6483
|
+
...generic
|
|
6484
|
+
];
|
|
6485
|
+
return [...new Set(ordered)].filter((command) => keep.has(command));
|
|
6486
|
+
}
|
|
6487
|
+
function capFindings(sorted, limit, problemSlots) {
|
|
6488
|
+
const reserved = [
|
|
6489
|
+
...sorted.filter((finding) => finding.kind === "problem").slice(0, problemSlots),
|
|
6490
|
+
...sorted.filter((finding) => finding.kind === "frustration").slice(0, 1)
|
|
6491
|
+
];
|
|
6492
|
+
const others = sorted.filter((finding) => !reserved.includes(finding)).slice(0, limit - reserved.length);
|
|
6493
|
+
const kept = new Set([...reserved, ...others]);
|
|
6494
|
+
return sorted.filter((finding) => kept.has(finding));
|
|
5043
6495
|
}
|
|
5044
6496
|
function compareFindings(a, b) {
|
|
5045
6497
|
const severityDelta = severityWeight(b.severity) - severityWeight(a.severity);
|
|
@@ -5132,12 +6584,15 @@ function numberValue(value) {
|
|
|
5132
6584
|
}
|
|
5133
6585
|
return 0;
|
|
5134
6586
|
}
|
|
5135
|
-
function
|
|
5136
|
-
const raw = flags[
|
|
5137
|
-
|
|
5138
|
-
if (!Number.isFinite(parsed) || parsed < 1)
|
|
6587
|
+
function daysBackFlag(flags, fallback = 7) {
|
|
6588
|
+
const raw = flags["days-back"];
|
|
6589
|
+
if (raw === undefined)
|
|
5139
6590
|
return fallback;
|
|
5140
|
-
|
|
6591
|
+
const parsed = Number(raw);
|
|
6592
|
+
if (raw.trim() === "" || raw === "true" || !Number.isInteger(parsed) || parsed < 1 || parsed > 90) {
|
|
6593
|
+
throw new CliInputError(`--days-back must be a whole number from 1 to 90; got '${raw}'.`, "Nothing was run. Example: --days-back=7 (the default).");
|
|
6594
|
+
}
|
|
6595
|
+
return parsed;
|
|
5141
6596
|
}
|
|
5142
6597
|
function normalizeConfidence(value) {
|
|
5143
6598
|
if (value === "high" || value === "medium" || value === "low" || value === "unknown")
|
|
@@ -5148,7 +6603,7 @@ function capFallbackConfidence(confidence) {
|
|
|
5148
6603
|
return confidence === "high" ? "medium" : confidence;
|
|
5149
6604
|
}
|
|
5150
6605
|
function evidenceKind(value) {
|
|
5151
|
-
if (value === "data_api" || value === "tool_failure" || value === "frustration" || value === "harness" || value === "ask")
|
|
6606
|
+
if (value === "data_api" || value === "tool_failure" || value === "frustration" || value === "problem" || value === "harness" || value === "ask")
|
|
5152
6607
|
return value;
|
|
5153
6608
|
return "ask";
|
|
5154
6609
|
}
|
|
@@ -5164,6 +6619,10 @@ function purposeForCommand2(command) {
|
|
|
5164
6619
|
return "Open the ranked production investigation.";
|
|
5165
6620
|
if (command.startsWith("moda tool-failure-detail"))
|
|
5166
6621
|
return "Inspect tool failure examples and trace anchors.";
|
|
6622
|
+
if (command.startsWith("moda problems"))
|
|
6623
|
+
return "List the ranked Problems.";
|
|
6624
|
+
if (command.startsWith("moda problem "))
|
|
6625
|
+
return "Open the Problem dossier.";
|
|
5167
6626
|
if (command.startsWith("moda context"))
|
|
5168
6627
|
return "Read the relevant trace window.";
|
|
5169
6628
|
if (command.startsWith("moda doctor"))
|
|
@@ -5290,6 +6749,7 @@ function buildManifest(commands) {
|
|
|
5290
6749
|
streaming: command.outputModes.includes("agent-stream") || command.outputModes.includes("agent"),
|
|
5291
6750
|
examples: command.examples,
|
|
5292
6751
|
auth: command.auth,
|
|
6752
|
+
...command.authNote ? { auth_note: command.authNote } : {},
|
|
5293
6753
|
mutability: command.mutability,
|
|
5294
6754
|
exit_codes: exitCodesForCommand(command.name)
|
|
5295
6755
|
})),
|
|
@@ -5338,18 +6798,24 @@ function exitCodesForCommand(command) {
|
|
|
5338
6798
|
|
|
5339
6799
|
// src/cli.ts
|
|
5340
6800
|
import { fileURLToPath } from "url";
|
|
5341
|
-
import { realpathSync as
|
|
6801
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
5342
6802
|
function parseArgs(argv) {
|
|
5343
6803
|
const args = argv.slice(2);
|
|
5344
6804
|
let command = "";
|
|
5345
6805
|
const flags = {};
|
|
5346
6806
|
const positionals = [];
|
|
6807
|
+
const shortFlags = [];
|
|
5347
6808
|
let positional;
|
|
5348
6809
|
let i = 0;
|
|
6810
|
+
let endOfOptions = false;
|
|
5349
6811
|
while (i < args.length) {
|
|
5350
6812
|
const arg = args[i];
|
|
5351
|
-
if (arg
|
|
6813
|
+
if (!endOfOptions && arg === "--" && command !== "") {
|
|
6814
|
+
endOfOptions = true;
|
|
6815
|
+
} else if (!endOfOptions && arg.startsWith("--")) {
|
|
5352
6816
|
i = parseFlag(args, i, flags, command === "");
|
|
6817
|
+
} else if (!endOfOptions && command !== "" && /^-[A-Za-z]/.test(arg)) {
|
|
6818
|
+
shortFlags.push(arg);
|
|
5353
6819
|
} else if (command === "") {
|
|
5354
6820
|
command = arg;
|
|
5355
6821
|
} else if (positional === undefined) {
|
|
@@ -5360,8 +6826,10 @@ function parseArgs(argv) {
|
|
|
5360
6826
|
}
|
|
5361
6827
|
i++;
|
|
5362
6828
|
}
|
|
5363
|
-
return { command, positional, positionals, flags };
|
|
6829
|
+
return { command, positional, positionals, flags, ...shortFlags.length > 0 ? { shortFlags } : {} };
|
|
5364
6830
|
}
|
|
6831
|
+
var DRY_RUN_TRUE = new Set(["true", "yes", "1", "on"]);
|
|
6832
|
+
var DRY_RUN_FALSE = new Set(["false", "no", "0", "off"]);
|
|
5365
6833
|
var GLOBAL_BOOLEAN_FLAGS = new Set([
|
|
5366
6834
|
"agent",
|
|
5367
6835
|
"json",
|
|
@@ -5450,7 +6918,7 @@ Commands:
|
|
|
5450
6918
|
profiles use <name> Switch active Moda profile
|
|
5451
6919
|
profiles create <name> Create or update a local Moda profile
|
|
5452
6920
|
profiles doctor [name] Validate a local Moda profile
|
|
5453
|
-
keys list List local key descriptors
|
|
6921
|
+
keys list List local key descriptors (not server-side API keys)
|
|
5454
6922
|
keys create Create a local key descriptor
|
|
5455
6923
|
config show Show resolved Moda config
|
|
5456
6924
|
harness analyze Produce a cited structured harness report
|
|
@@ -5461,14 +6929,14 @@ Commands:
|
|
|
5461
6929
|
harness candidates Static (no-LLM) harness candidate pre-pass with file:line citations
|
|
5462
6930
|
harness sync Sync the local harness graph to Moda Cloud
|
|
5463
6931
|
harness delete <id-or-key> Permanently delete a synced harness from Moda Cloud
|
|
6932
|
+
harness list|show|versions|diff List synced harnesses, show one, its versions, or a version diff
|
|
5464
6933
|
harness agents List detected runtime agents and families
|
|
5465
6934
|
harness explain Explain the discovered harness graph
|
|
5466
6935
|
agent context Emit local Moda context for coding agents
|
|
5467
|
-
agent investigate Diagnose local Moda readiness for agents
|
|
5468
6936
|
agent next-action Emit prioritized next commands for agents
|
|
5469
6937
|
agent evidence <ref> Expand one evidence reference
|
|
5470
|
-
investigate Rank production issues
|
|
5471
|
-
failures
|
|
6938
|
+
investigate Rank production issues (tool failures, frustrations, top Problems)
|
|
6939
|
+
failures Tool failures only, ranked (no frustrations or Problems)
|
|
5472
6940
|
ask "question" Ask Moda a production/harness question
|
|
5473
6941
|
sync Sync the local harness graph to Moda Cloud
|
|
5474
6942
|
sync harness Sync the local harness graph to Moda Cloud
|
|
@@ -5480,20 +6948,25 @@ Commands:
|
|
|
5480
6948
|
clusters Browse topic cluster hierarchy
|
|
5481
6949
|
cluster-traces <node_id> List traces in a cluster
|
|
5482
6950
|
traces Search and filter traces (agent runs)
|
|
6951
|
+
traces --sort=longest Sort traces: recent (default), longest, slowest
|
|
5483
6952
|
search "<query>" Search trace messages (keyword/semantic/hybrid)
|
|
5484
6953
|
world-state <conversation_id> Get a trace's world state (slots/threads/events)
|
|
5485
6954
|
context <conversation_id> Get windowed trace context
|
|
6955
|
+
context <conversation_id> --all Whole transcript in order (first 500; --from=N --max-messages=N to continue)
|
|
5486
6956
|
audit <conversation_id|trace_id> Raw span audit (spans, hierarchy, orphans, duplicates)
|
|
5487
|
-
frustrations
|
|
5488
|
-
emotions Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)
|
|
6957
|
+
frustrations Frustration detections (same data as emotions --family=frustration)
|
|
6958
|
+
emotions Multi-family emotion detections (frustration, disapproval, sadness, confusion, anxiety, trust, positive)
|
|
5489
6959
|
hallucinations Grounding detections: contradicted/verified outputs with rule breakdown
|
|
5490
6960
|
laziness Laziness detections per trace (repeated calls, premature handoffs, ...)
|
|
5491
6961
|
tool-failures Get tool failure overview
|
|
5492
|
-
tool-failure-detail <tool_name> Get per-tool failure detail
|
|
6962
|
+
tool-failure-detail <tool_name> Get per-tool failure detail (--limit=N examples, 1-20, default 5;
|
|
6963
|
+
long example text is cut to 300 chars)
|
|
5493
6964
|
problems Rank cross-signal Problems by root cause (what to fix first)
|
|
6965
|
+
problems remainder Signals no Problem explains yet (--family, --cursor)
|
|
5494
6966
|
problem-impact Live problem/use-case impact (--node-id, --problem-id, --mode, --days-back)
|
|
5495
6967
|
problem <problem_id> Open one Problem: dossier, or --evidence/--reports/--traces/--feedback
|
|
5496
6968
|
problem-feedback <problem_id> Mark a Problem fixed, dismiss, rename, or flag a bad attribution
|
|
6969
|
+
problem-reopen <problem_id> Reopen a Problem marked fixed (--reason)
|
|
5497
6970
|
false-positive <kind> <target> Mark any detection as a false positive, optional --reason (problem, problem-detection, quote, tool, laziness, hallucination, custom-signal)
|
|
5498
6971
|
false-positives [conv_id] Current false-positive marks: all (--kind, --all, --limit) or one trace
|
|
5499
6972
|
problem-linear <problem_id> Show the Linear issue linked to a Problem (key, state, URL)
|
|
@@ -5502,6 +6975,10 @@ Commands:
|
|
|
5502
6975
|
detection-review <problem_id> Mark a problem detection correct, a false positive, or clear the review
|
|
5503
6976
|
signals List custom signals (name, criteria, status, 7-day stats)
|
|
5504
6977
|
signal <signal_id> One custom signal, or --detections for its recent hits
|
|
6978
|
+
signals create|update|activate|pause|delete ...
|
|
6979
|
+
Manage custom signals (also draft, examples, refine, revise,
|
|
6980
|
+
proposals, accept, reject, search, preview); writes take --dry-run
|
|
6981
|
+
users / user <user_id> End users with health and activity (the dashboard Users page)
|
|
5505
6982
|
trace-signals <conversation_id> Custom signal hits on one trace
|
|
5506
6983
|
signal-label <signal_id> Label a trace fits / does_not_fit (false positive) for a signal
|
|
5507
6984
|
step-scores <conversation_id> Graph-PRM step scores (per-segment curves, first bad step, rollup)
|
|
@@ -5515,9 +6992,11 @@ Commands:
|
|
|
5515
6992
|
Prompt management:
|
|
5516
6993
|
prompts init Create .moda/prompts.yml
|
|
5517
6994
|
prompts status Show local prompt changes
|
|
5518
|
-
prompts diff
|
|
6995
|
+
prompts diff Only changed/new/deleted prompts since last sync
|
|
5519
6996
|
prompts sync Analyze prompt sources and upload versions
|
|
5520
6997
|
prompts sync --watch Upload again when prompt files change
|
|
6998
|
+
prompts list|show|proposals|usage|replay-runs|decide
|
|
6999
|
+
Read the prompt registry; decide merges/rejects a proposal
|
|
5521
7000
|
prompts promote <key> Move a dev/staging/prod label
|
|
5522
7001
|
prompts ab Run a judged prompt A/B replay experiment
|
|
5523
7002
|
prompts propose Revise a prompt from a run's failures (--gate to auto-A/B, --promote-on-win)
|
|
@@ -5530,15 +7009,19 @@ Commands:
|
|
|
5530
7009
|
skills pull --status=proposed Include not-yet-promoted skills (default: approved)
|
|
5531
7010
|
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
5532
7011
|
skills sync --dry-run Show what would be pushed without writing
|
|
7012
|
+
skills list|show|policy List pushed skills, show one, or set --live/--local
|
|
7013
|
+
registry push Push prompts, skills, and tools from one JSON manifest
|
|
5533
7014
|
|
|
5534
7015
|
Fixes (a fix with proof):
|
|
5535
7016
|
fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
|
|
5536
7017
|
fixes draft-batch Campaign mode: batch-draft fixes for the top-ranked problems
|
|
5537
|
-
(--limit=N, 1-25, default
|
|
7018
|
+
(--limit=N, 1-25, default 5 — batches cap at 25: LLM spend guard)
|
|
5538
7019
|
fixes drive Round-robin one advance per active fix per pass until all rest
|
|
5539
7020
|
(--status=S narrows the server-side scan window to one advancing
|
|
5540
|
-
status, --
|
|
5541
|
-
|
|
7021
|
+
status, --max-fixes=N 1-10 default 3 fixes per run (rest skipped;
|
|
7022
|
+
GATING fixes count toward N; least-recently-driven first),
|
|
7023
|
+
--timeout=ms default 2h, --pass-interval=ms default 20s;
|
|
7024
|
+
exit 3 when the timeout lapses, fixes were skipped, or still advancing)
|
|
5542
7025
|
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
5543
7026
|
(tool/skill candidates are also written under .moda/fixes/<ref>/);
|
|
5544
7027
|
--wait drives a still-running pipeline to rest
|
|
@@ -5546,6 +7029,7 @@ Commands:
|
|
|
5546
7029
|
scope -> propose -> gate and prints the packet
|
|
5547
7030
|
fix verify <fix_id> Re-gate on the frozen holdout; --prompt-file=P gates local content
|
|
5548
7031
|
(exit 0 pass / 1 fail with per-case findings / 3 degraded-inconclusive)
|
|
7032
|
+
Refuses a re-gate within 10 min of the last verdict unless --yes
|
|
5549
7033
|
fix checkout <fix_id> Write the candidate at its target source path (never runs git)
|
|
5550
7034
|
fix submit <fix_id> Deliver: --pr opens a draft PR | --local-ref=<branch> records your branch
|
|
5551
7035
|
fix mark-applied <fix_id> Confirm a candidate you applied in your own stack (--note=TEXT);
|
|
@@ -5581,7 +7065,7 @@ Output auto-detection (no flags needed):
|
|
|
5581
7065
|
--no-update-check Skip the daily new-version check (or MODA_CLI_UPDATE_CHECK=0)
|
|
5582
7066
|
|
|
5583
7067
|
Traces flags:
|
|
5584
|
-
--search=TEXT
|
|
7068
|
+
--search=TEXT Ranked trace search (hybrid when embeddings exist, else keyword)
|
|
5585
7069
|
--world-state=KEYWORDS Match world-state content (slots + durable profile); comma = AND
|
|
5586
7070
|
--outcome=any|positive|negative Filter by trace outcome (trajectory + frustration)
|
|
5587
7071
|
--include-world-state Attach each result's world-state summary
|
|
@@ -5604,12 +7088,12 @@ World-state flags:
|
|
|
5604
7088
|
--message-count=N Number of frames to fold (frames mode)
|
|
5605
7089
|
|
|
5606
7090
|
Emotions flags:
|
|
5607
|
-
--family=F frustration|sadness|confusion|anxiety|trust|positive
|
|
7091
|
+
--family=F frustration|disapproval|sadness|confusion|anxiety|trust|positive
|
|
5608
7092
|
--days-back=N --limit=N Window 1-90 (default 7); page size 1-20 (default 10)
|
|
5609
7093
|
--offset=N Skip N detections (0-10000)
|
|
5610
7094
|
|
|
5611
7095
|
Laziness flags:
|
|
5612
|
-
--family=F
|
|
7096
|
+
--family=F One laziness family (e.g. omission_phrase, stub_body, deflection, completion, tool_selection)
|
|
5613
7097
|
--conversation-id=ID One trace's detections
|
|
5614
7098
|
--days-back=N --limit=N Window 1-90 (default 7); page size 1-50 (default 10)
|
|
5615
7099
|
|
|
@@ -5622,7 +7106,7 @@ Problem flags (moda problem <id>):
|
|
|
5622
7106
|
--evidence|--reports|--traces|--feedback
|
|
5623
7107
|
Open one sub-resource page (at most one)
|
|
5624
7108
|
--limit=N --cursor=TOKEN Keyset paging (1-50; pass next_cursor back verbatim)
|
|
5625
|
-
--family=F --door=D Filter --traces (families: tool_failure|emotion|laziness|hallucination|prm_dip)
|
|
7109
|
+
--family=F --door=D Filter --traces (families: tool_failure|emotion|laziness|hallucination|prm_dip|custom_signal|conversation_reader)
|
|
5626
7110
|
|
|
5627
7111
|
Problem-feedback flags:
|
|
5628
7112
|
--action=A mark_fixed|dismiss|flag_attribution|rename (required)
|
|
@@ -5831,6 +7315,9 @@ function printCommandHelp(definition) {
|
|
|
5831
7315
|
if (definition.mutability === "write") {
|
|
5832
7316
|
lines.push("This command can mutate state.");
|
|
5833
7317
|
}
|
|
7318
|
+
for (const note of definition.helpNotes ?? []) {
|
|
7319
|
+
lines.push("", note);
|
|
7320
|
+
}
|
|
5834
7321
|
if (definition.subcommands && definition.subcommands.length > 0) {
|
|
5835
7322
|
lines.push("", "Subcommands:");
|
|
5836
7323
|
const width = Math.max(...definition.subcommands.map((entry) => entry.name.length));
|
|
@@ -5886,7 +7373,8 @@ async function attachWindows(rows, windowSize) {
|
|
|
5886
7373
|
}
|
|
5887
7374
|
const conversation = asRecord(row.conversation);
|
|
5888
7375
|
const embedded = conversation ? embeddedWindowSize(conversation) : null;
|
|
5889
|
-
|
|
7376
|
+
const embeddedCenter = asNumber(asRecord(conversation?.context)?.center_index);
|
|
7377
|
+
if (conversation && embedded !== null && embedded >= windowSize && embeddedCenter === msg_index) {
|
|
5890
7378
|
return { ...row, window: conversation };
|
|
5891
7379
|
}
|
|
5892
7380
|
try {
|
|
@@ -5901,15 +7389,18 @@ async function attachWindows(rows, windowSize) {
|
|
|
5901
7389
|
}
|
|
5902
7390
|
}));
|
|
5903
7391
|
}
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
7392
|
+
var TOOL_FAILURE_DETAIL_DEFAULT_LIMIT = 5;
|
|
7393
|
+
var TOOL_FAILURE_AGENT_SUBTYPES = 20;
|
|
7394
|
+
async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize, output, warnings, finalize) {
|
|
7395
|
+
const allWarnings = [...warnings ?? []];
|
|
7396
|
+
let data = enriched;
|
|
7397
|
+
if (includeWindow) {
|
|
7398
|
+
const list = Array.isArray(enriched[listKey]) ? enriched[listKey] : [];
|
|
7399
|
+
data = { ...enriched, [listKey]: await attachWindows(list, windowSize) };
|
|
5909
7400
|
}
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
output.writeData({
|
|
7401
|
+
if (finalize)
|
|
7402
|
+
data = finalize(data, allWarnings);
|
|
7403
|
+
output.writeData(data, allWarnings.length > 0 ? { warnings: allWarnings } : undefined);
|
|
5913
7404
|
}
|
|
5914
7405
|
function notFoundWarning(entity, id, isMissing) {
|
|
5915
7406
|
if (!isMissing)
|
|
@@ -5918,6 +7409,23 @@ function notFoundWarning(entity, id, isMissing) {
|
|
|
5918
7409
|
`No ${entity} found for id "${id}" — it may not exist in this tenant (the Data API returns an empty result rather than an error for unknown ids).`
|
|
5919
7410
|
];
|
|
5920
7411
|
}
|
|
7412
|
+
var AUDIT_DEFAULT_LIMIT = 200;
|
|
7413
|
+
var AUDIT_MAX_LIMIT = 1000;
|
|
7414
|
+
function auditCapStatus(data, limit) {
|
|
7415
|
+
const record = asRecord(data);
|
|
7416
|
+
if (!record)
|
|
7417
|
+
return null;
|
|
7418
|
+
const summary = asRecord(record.summary) ?? {};
|
|
7419
|
+
const rows = (asNumber(summary.ingest_rows) ?? (Array.isArray(record.ingest) ? record.ingest.length : 0)) + (asNumber(record.parse_errors) ?? 0);
|
|
7420
|
+
const spans = asNumber(summary.total_spans) ?? (Array.isArray(record.spans) ? record.spans.length : 0);
|
|
7421
|
+
const hasMore = record.has_more === true || summary.has_more === true;
|
|
7422
|
+
const spansCapped = summary.truncated === true || record.truncated === true;
|
|
7423
|
+
const serverSaid = typeof record.has_more === "boolean" || typeof summary.has_more === "boolean";
|
|
7424
|
+
const rowsCapped = hasMore || !serverSaid && rows >= limit;
|
|
7425
|
+
if (!rowsCapped && !spansCapped)
|
|
7426
|
+
return null;
|
|
7427
|
+
return { rowsCapped, spansCapped, hasMoreKnown: hasMore, rows, spans };
|
|
7428
|
+
}
|
|
5921
7429
|
var TAIL_SEEN_MAX = 5000;
|
|
5922
7430
|
var EMOTIONS_TAIL_PAGE_SIZE = 20;
|
|
5923
7431
|
var EMOTIONS_TAIL_MAX_PAGES = 25;
|
|
@@ -6099,7 +7607,7 @@ async function runTailCommand(params, context) {
|
|
|
6099
7607
|
break;
|
|
6100
7608
|
if (params.max_events !== undefined && emitted >= params.max_events)
|
|
6101
7609
|
break;
|
|
6102
|
-
await new Promise((
|
|
7610
|
+
await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
|
|
6103
7611
|
}
|
|
6104
7612
|
}
|
|
6105
7613
|
async function attachWorldState(rows) {
|
|
@@ -6197,7 +7705,435 @@ function linearApiError(error) {
|
|
|
6197
7705
|
}
|
|
6198
7706
|
return error;
|
|
6199
7707
|
}
|
|
7708
|
+
var GLOBAL_FLAG_NAMES = new Set([
|
|
7709
|
+
...[...GLOBAL_BOOLEAN_FLAGS].filter((flag2) => flag2 !== "dry-run"),
|
|
7710
|
+
"profile",
|
|
7711
|
+
"progress",
|
|
7712
|
+
"no-stream",
|
|
7713
|
+
"format",
|
|
7714
|
+
"no-telemetry",
|
|
7715
|
+
"no-update-check"
|
|
7716
|
+
]);
|
|
7717
|
+
var OVERVIEW_PRODUCTION_FLAGS = ["raw"];
|
|
7718
|
+
var WINDOW_FLAG_HINTS = {
|
|
7719
|
+
days_back: "This command takes --time-range (e.g. --time-range=7d), not --days-back.",
|
|
7720
|
+
time_range: "This command takes --days-back=N (1-90), not --time-range."
|
|
7721
|
+
};
|
|
7722
|
+
function editDistance(a, b) {
|
|
7723
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
7724
|
+
for (let i = 1;i <= a.length; i++) {
|
|
7725
|
+
let previous = row[0];
|
|
7726
|
+
row[0] = i;
|
|
7727
|
+
for (let j = 1;j <= b.length; j++) {
|
|
7728
|
+
const current = row[j];
|
|
7729
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
7730
|
+
previous = current;
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
return row[b.length];
|
|
7734
|
+
}
|
|
7735
|
+
function schemaFlagKeys(schema) {
|
|
7736
|
+
let current = schema;
|
|
7737
|
+
while (current instanceof z2.ZodEffects)
|
|
7738
|
+
current = current.innerType();
|
|
7739
|
+
return current instanceof z2.ZodObject ? Object.keys(current.shape) : [];
|
|
7740
|
+
}
|
|
7741
|
+
function parseCommandFlags(schema, args, command, options = {}) {
|
|
7742
|
+
const schemaKeys = schemaFlagKeys(schema);
|
|
7743
|
+
const known = new Set([
|
|
7744
|
+
...schemaKeys,
|
|
7745
|
+
...[...GLOBAL_FLAG_NAMES, ...options.extraFlags ?? []].map((flag2) => flag2.replace(/-/g, "_"))
|
|
7746
|
+
]);
|
|
7747
|
+
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
7748
|
+
if (unknown.includes("dry_run")) {
|
|
7749
|
+
throw new CliInputError(`'moda ${command}' has no dry-run mode, so nothing was sent.`, `Remove --dry-run to run it for real. Commands with a real dry-run: sync, registry push, harness sync/delete, prompts sync/decide, skills sync/policy, signals <write subcommands>, problem-reopen.`);
|
|
7750
|
+
}
|
|
7751
|
+
if (unknown.length > 0) {
|
|
7752
|
+
const flagKeys = schemaKeys.filter((key) => key !== options.positionalKey);
|
|
7753
|
+
const valid = flagKeys.map((key) => `--${key.replace(/_/g, "-")}`);
|
|
7754
|
+
const hints = unknown.map((key) => {
|
|
7755
|
+
const windowAlternative = key === "days_back" ? "time_range" : key === "time_range" ? "days_back" : undefined;
|
|
7756
|
+
if (windowAlternative && WINDOW_FLAG_HINTS[key] && flagKeys.includes(windowAlternative))
|
|
7757
|
+
return WINDOW_FLAG_HINTS[key];
|
|
7758
|
+
if (windowAlternative)
|
|
7759
|
+
return `This command has no time-window flag.`;
|
|
7760
|
+
const close = flagKeys.map((candidate) => ({ candidate, distance: editDistance(key, candidate) })).filter(({ distance }) => distance <= 2).sort((left, right) => left.distance - right.distance)[0];
|
|
7761
|
+
return close ? `Did you mean --${close.candidate.replace(/_/g, "-")}?` : "";
|
|
7762
|
+
}).filter(Boolean);
|
|
7763
|
+
const names = unknown.map((key) => `--${key.replace(/_/g, "-")}`).join(", ");
|
|
7764
|
+
throw new CliInputError(`Unknown flag${unknown.length > 1 ? "s" : ""} ${names} for 'moda ${command}'.`, [...hints, `Valid flags: ${valid.join(", ") || "(none)"}. Run 'moda ${command} --help'.`].join(" "));
|
|
7765
|
+
}
|
|
7766
|
+
return schema.parse(args);
|
|
7767
|
+
}
|
|
7768
|
+
function withStringIdFlags(args, flags) {
|
|
7769
|
+
for (const flag2 of ["user-id", "conversation-id"]) {
|
|
7770
|
+
if (flags[flag2] !== undefined)
|
|
7771
|
+
args[flag2.replace(/-/g, "_")] = flags[flag2];
|
|
7772
|
+
}
|
|
7773
|
+
return args;
|
|
7774
|
+
}
|
|
7775
|
+
function shellArg(value) {
|
|
7776
|
+
if (/^[A-Za-z0-9_.,:@%+\/=-]+$/.test(value))
|
|
7777
|
+
return value;
|
|
7778
|
+
if (!/["$`\\!]/.test(value))
|
|
7779
|
+
return `"${value}"`;
|
|
7780
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7781
|
+
}
|
|
7782
|
+
async function searchFallbackToTraces(params) {
|
|
7783
|
+
const limit = Math.min(params.limit ?? 20, 100);
|
|
7784
|
+
const offset = params.offset ?? 0;
|
|
7785
|
+
const query = new URLSearchParams;
|
|
7786
|
+
query.set("search", params.q);
|
|
7787
|
+
if (params.user_id)
|
|
7788
|
+
query.set("user_id", params.user_id);
|
|
7789
|
+
if (params.time_range)
|
|
7790
|
+
query.set("time_range", params.time_range);
|
|
7791
|
+
query.set("limit", String(limit));
|
|
7792
|
+
if (offset > 0)
|
|
7793
|
+
query.set("offset", String(offset));
|
|
7794
|
+
const tracesCommand = [
|
|
7795
|
+
`moda traces --search=${shellArg(params.q)}`,
|
|
7796
|
+
params.user_id ? `--user-id=${shellArg(params.user_id)}` : "",
|
|
7797
|
+
params.time_range ? `--time-range=${shellArg(params.time_range)}` : "",
|
|
7798
|
+
`--limit=${limit}`
|
|
7799
|
+
].filter(Boolean).join(" ");
|
|
7800
|
+
let traces;
|
|
7801
|
+
try {
|
|
7802
|
+
traces = asRecord(await callDataAPI(`/conversations?${query.toString()}`)) ?? {};
|
|
7803
|
+
} catch (error) {
|
|
7804
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
7805
|
+
return {
|
|
7806
|
+
kind: "notice",
|
|
7807
|
+
warning: `Message-level search returned 0 results, and the trace-level fallback failed (${reason}). ` + "Zero results here does NOT mean the topic never came up. Retry, or run the trace search directly.",
|
|
7808
|
+
nextCommands: [{ command: tracesCommand, purpose: "Retry the trace-level search directly.", mutability: "read", requires_approval: false }]
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
const rows = Array.isArray(traces.conversations) ? traces.conversations.map(asRecord).filter(isDefinedRecord) : [];
|
|
7812
|
+
if (rows.length === 0)
|
|
7813
|
+
return;
|
|
7814
|
+
const pagination = asRecord(traces.pagination);
|
|
7815
|
+
const total = asNumber(pagination?.total) ?? rows.length;
|
|
7816
|
+
const nextOffset = offset + rows.length;
|
|
7817
|
+
const hasMore = pagination?.has_more === true || (asNumber(pagination?.total) ?? 0) > nextOffset;
|
|
7818
|
+
const traceMode = asString(traces.search_mode) ?? "keyword";
|
|
7819
|
+
const literal = traceMode === "keyword";
|
|
7820
|
+
if (params.mode === "keyword" && !literal) {
|
|
7821
|
+
return {
|
|
7822
|
+
kind: "notice",
|
|
7823
|
+
warning: `Message-level keyword search returned 0 results. Trace-level search is only available ${traceMode}-ranked here ` + `(results may not contain the exact words), so none are substituted for --mode=keyword. ` + "Zero results here does NOT mean the words never appear; the message index may be incomplete.",
|
|
7824
|
+
nextCommands: [{ command: tracesCommand, purpose: "See ranked trace-level results instead.", mutability: "read", requires_approval: false }]
|
|
7825
|
+
};
|
|
7826
|
+
}
|
|
7827
|
+
const results = rows.map((row) => ({
|
|
7828
|
+
match_grain: "trace",
|
|
7829
|
+
conversation_id: row.conversation_id,
|
|
7830
|
+
summary: row.summary ?? null,
|
|
7831
|
+
message_count: row.message_count ?? null,
|
|
7832
|
+
first_timestamp: row.first_timestamp ?? null,
|
|
7833
|
+
last_timestamp: row.last_timestamp ?? null
|
|
7834
|
+
}));
|
|
7835
|
+
const first = asString(rows[0].conversation_id);
|
|
7836
|
+
const describe = literal ? "trace-level keyword matches (each trace contains the words)" : `trace-level ${traceMode}-ranked related traces (they may not contain the exact words; fallback.total is the candidate pool, not a match count)`;
|
|
7837
|
+
const nextCommands = [];
|
|
7838
|
+
if (first) {
|
|
7839
|
+
nextCommands.push({ command: `moda audit ${first}`, purpose: "Open the top trace to locate the relevant message.", mutability: "read", requires_approval: false });
|
|
7840
|
+
}
|
|
7841
|
+
if (hasMore) {
|
|
7842
|
+
nextCommands.push({ command: `${tracesCommand} --offset=${nextOffset}`, purpose: "Page through further trace-level results.", mutability: "read", requires_approval: false });
|
|
7843
|
+
}
|
|
7844
|
+
return {
|
|
7845
|
+
kind: "results",
|
|
7846
|
+
hasMore,
|
|
7847
|
+
data: {
|
|
7848
|
+
results,
|
|
7849
|
+
fallback: {
|
|
7850
|
+
source: "traces_search",
|
|
7851
|
+
reason: "message_index_empty",
|
|
7852
|
+
grain: "trace",
|
|
7853
|
+
search_mode: traceMode,
|
|
7854
|
+
total,
|
|
7855
|
+
total_kind: literal ? "matches" : "candidate_pool",
|
|
7856
|
+
has_more: hasMore,
|
|
7857
|
+
...offset > 0 ? { offset } : {},
|
|
7858
|
+
...hasMore ? { next_offset: nextOffset } : {}
|
|
7859
|
+
}
|
|
7860
|
+
},
|
|
7861
|
+
warning: `Message-level search returned 0 results, so these are ${describe}, from \`moda traces --search\`. ` + `Each result is a whole trace, not a message, with no score or message index. ` + `Open one with \`moda audit <conversation_id>\` or \`moda context <conversation_id>\` to find the matching turn.` + (hasMore ? " More exist: page with the suggested `moda traces --search ... --offset` command." : ""),
|
|
7862
|
+
nextCommands
|
|
7863
|
+
};
|
|
7864
|
+
}
|
|
7865
|
+
function capProblemsBlocks(data, limit) {
|
|
7866
|
+
const truncated = {};
|
|
7867
|
+
const payload = { ...data };
|
|
7868
|
+
if (Array.isArray(data.emerging) && data.emerging.length > limit) {
|
|
7869
|
+
truncated.emerging = data.emerging.length - limit;
|
|
7870
|
+
payload.emerging = data.emerging.slice(0, limit);
|
|
7871
|
+
}
|
|
7872
|
+
const hierarchy = asRecord(data.hierarchy);
|
|
7873
|
+
if (hierarchy && Array.isArray(hierarchy.macros) && hierarchy.macros.length > limit) {
|
|
7874
|
+
truncated.hierarchy_macros = hierarchy.macros.length - limit;
|
|
7875
|
+
payload.hierarchy = { ...hierarchy, macros: hierarchy.macros.slice(0, limit) };
|
|
7876
|
+
}
|
|
7877
|
+
return Object.keys(truncated).length > 0 ? { payload, truncated } : { payload };
|
|
7878
|
+
}
|
|
7879
|
+
function serverWarnings(data) {
|
|
7880
|
+
const list = data?.warnings;
|
|
7881
|
+
return Array.isArray(list) ? list.filter((item) => typeof item === "string") : [];
|
|
7882
|
+
}
|
|
7883
|
+
function isDefinedRecord(value) {
|
|
7884
|
+
return value !== undefined && value !== null;
|
|
7885
|
+
}
|
|
7886
|
+
var HAND_PARSED_COMMAND_FLAGS = {
|
|
7887
|
+
investigate: ["days-back", "tool", "trace", "conversation"],
|
|
7888
|
+
failures: ["days-back", "tool"],
|
|
7889
|
+
ask: ["days-back", "stream"],
|
|
7890
|
+
"step-scores": [],
|
|
7891
|
+
"trace-signals": [],
|
|
7892
|
+
signals: [],
|
|
7893
|
+
"problem-feedback": ["action", "actor", "reason", "attribution-id", "new-name"],
|
|
7894
|
+
"problem-linear": [],
|
|
7895
|
+
"problem-linear-file": ["issue", "priority", "title", "team-id"],
|
|
7896
|
+
"signal-label": ["label", "note", "conversation-id", "msg-dedup-token", "msg-index"],
|
|
7897
|
+
"detection-reviews": ["attribution-id"],
|
|
7898
|
+
"detection-review": ["note", "verdict", "attribution-id", "conversation-id"],
|
|
7899
|
+
"false-positive": [
|
|
7900
|
+
"family",
|
|
7901
|
+
"quote",
|
|
7902
|
+
"reason",
|
|
7903
|
+
"tool",
|
|
7904
|
+
"turn",
|
|
7905
|
+
"undo",
|
|
7906
|
+
"attribution-id",
|
|
7907
|
+
"conversation-id",
|
|
7908
|
+
"msg-dedup-token",
|
|
7909
|
+
"msg-index",
|
|
7910
|
+
"tool-use-id"
|
|
7911
|
+
],
|
|
7912
|
+
"false-positives": ["all", "kind", "limit"],
|
|
7913
|
+
feedback: ["category", "severity", "cluster-id", "conversation-id", "run-id", "tool-name"],
|
|
7914
|
+
"problem-impact": ["mode", "node-id", "problem-id", "run-id", "to", "cursor", "conversation-ids", "days-back"]
|
|
7915
|
+
};
|
|
7916
|
+
var NO_POSITIONAL_COMMANDS = new Set([
|
|
7917
|
+
"overview",
|
|
7918
|
+
"investigate",
|
|
7919
|
+
"failures",
|
|
7920
|
+
"signals",
|
|
7921
|
+
"emotions",
|
|
7922
|
+
"frustrations",
|
|
7923
|
+
"laziness",
|
|
7924
|
+
"hallucinations",
|
|
7925
|
+
"tool-failures",
|
|
7926
|
+
"traces",
|
|
7927
|
+
"conversations",
|
|
7928
|
+
"clusters",
|
|
7929
|
+
"tail",
|
|
7930
|
+
"problem-impact"
|
|
7931
|
+
]);
|
|
7932
|
+
function assertHandParsedFlags(command, flags, positionals) {
|
|
7933
|
+
if (command === "signals" && positionals[0]) {
|
|
7934
|
+
if (SIGNALS_MANAGEMENT_SUBCOMMANDS.has(positionals[0]))
|
|
7935
|
+
return;
|
|
7936
|
+
throw unknownSignalsSubcommandError(positionals[0], positionals.slice(1));
|
|
7937
|
+
}
|
|
7938
|
+
if (NO_POSITIONAL_COMMANDS.has(command) && positionals.length > 0) {
|
|
7939
|
+
throw new CliInputError(`'moda ${command}' takes no arguments, but got '${positionals.join(" ")}'.`, `Nothing was run. See 'moda ${command} --help' for its flags.`);
|
|
7940
|
+
}
|
|
7941
|
+
const allowed = HAND_PARSED_COMMAND_FLAGS[command];
|
|
7942
|
+
if (!allowed)
|
|
7943
|
+
return;
|
|
7944
|
+
const shape = z2.object(Object.fromEntries(allowed.map((flag2) => [flag2.replace(/-/g, "_"), z2.unknown()])));
|
|
7945
|
+
const args = Object.fromEntries(Object.keys(flags).map((flag2) => [flag2.replace(/-/g, "_"), true]));
|
|
7946
|
+
parseCommandFlags(shape.passthrough(), args, command);
|
|
7947
|
+
}
|
|
7948
|
+
var PROFILE_MANAGING_COMMANDS = new Set(["profiles", "auth", "init", "provision", "status", "config", "keys", "manifest", "agent", "doctor"]);
|
|
7949
|
+
async function runProblemsRemainder(flags, context) {
|
|
7950
|
+
const params = parseCommandFlags(ProblemsRemainderSchema, flagsToArgs(flags), "problems remainder");
|
|
7951
|
+
const query = new URLSearchParams;
|
|
7952
|
+
if (params.days_back)
|
|
7953
|
+
query.set("days_back", params.days_back.toString());
|
|
7954
|
+
if (params.limit)
|
|
7955
|
+
query.set("limit", params.limit.toString());
|
|
7956
|
+
if (params.family)
|
|
7957
|
+
query.set("family", params.family);
|
|
7958
|
+
if (params.cursor)
|
|
7959
|
+
query.set("cursor", params.cursor);
|
|
7960
|
+
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
7961
|
+
context.output.writeData(await callDataAPI(`/problems/remainder${queryString}`));
|
|
7962
|
+
}
|
|
7963
|
+
function processingDisabledWarning(data, family) {
|
|
7964
|
+
const processing = asRecord(asRecord(data)?.processing);
|
|
7965
|
+
const label = family === "emotion" ? "Emotion" : "Frustration";
|
|
7966
|
+
if (processing?.enabled === null) {
|
|
7967
|
+
return [`Could not confirm whether ${family} analysis is enabled for this tenant, so treat zero results with caution.`];
|
|
7968
|
+
}
|
|
7969
|
+
if (processing?.enabled !== false)
|
|
7970
|
+
return [];
|
|
7971
|
+
if (asString(processing.reason) === "dogfood_tenant") {
|
|
7972
|
+
return [`${label} analysis is skipped for this internal (dogfood) tenant, so zero results do NOT mean users are not frustrated.`];
|
|
7973
|
+
}
|
|
7974
|
+
const setting = asString(processing.setting) ?? "signals.frustration";
|
|
7975
|
+
return [
|
|
7976
|
+
`${family === "emotion" ? "Emotion" : "Frustration"} analysis is disabled for this tenant (${setting} = false), ` + "so zero results do NOT mean users are not frustrated. Enable it in Settings → Account → Signal processing; " + "only traces ingested after enabling are analyzed."
|
|
7977
|
+
];
|
|
7978
|
+
}
|
|
7979
|
+
var FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES = 500;
|
|
7980
|
+
var CONTEXT_RANGE_PAGE_SIZE = 200;
|
|
7981
|
+
var LEGACY_WINDOW_MAX_MESSAGES = 100;
|
|
7982
|
+
var CONTEXT_MESSAGE_TRUNCATE_CHARS = 500;
|
|
7983
|
+
var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
|
|
7984
|
+
function stripLoneSurrogates(value) {
|
|
7985
|
+
if (typeof value === "string")
|
|
7986
|
+
return value.replace(LONE_SURROGATE, "");
|
|
7987
|
+
if (Array.isArray(value))
|
|
7988
|
+
return value.map(stripLoneSurrogates);
|
|
7989
|
+
if (value && typeof value === "object") {
|
|
7990
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, stripLoneSurrogates(v)]));
|
|
7991
|
+
}
|
|
7992
|
+
return value;
|
|
7993
|
+
}
|
|
7994
|
+
function isUnknownRangeParamsError(error) {
|
|
7995
|
+
const e = error;
|
|
7996
|
+
if (e?.statusCode !== 400)
|
|
7997
|
+
return false;
|
|
7998
|
+
const text = `${typeof e.responseBody === "string" ? e.responseBody : ""} ${typeof e.message === "string" ? e.message : ""}`;
|
|
7999
|
+
return /property (from|count) should not exist/i.test(text);
|
|
8000
|
+
}
|
|
8001
|
+
function pageMessages(page) {
|
|
8002
|
+
const messages = asRecord(page.context)?.messages ?? page.messages;
|
|
8003
|
+
return (Array.isArray(messages) ? messages : []).map((m) => asRecord(m) ?? {});
|
|
8004
|
+
}
|
|
8005
|
+
async function readFullTranscript(conversationId, opts = {}) {
|
|
8006
|
+
const start = opts.from ?? 0;
|
|
8007
|
+
const requested = opts.maxMessages ?? FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES;
|
|
8008
|
+
const path = `/conversations/${encodeURIComponent(conversationId)}/context`;
|
|
8009
|
+
const warnings = [];
|
|
8010
|
+
const messages = [];
|
|
8011
|
+
let total = 0;
|
|
8012
|
+
let summary = null;
|
|
8013
|
+
let ordering;
|
|
8014
|
+
let nextFrom = null;
|
|
8015
|
+
let legacy = false;
|
|
8016
|
+
let cursor = start;
|
|
8017
|
+
for (;; ) {
|
|
8018
|
+
const count = Math.min(CONTEXT_RANGE_PAGE_SIZE, requested - messages.length);
|
|
8019
|
+
let page;
|
|
8020
|
+
try {
|
|
8021
|
+
page = asRecord(await callDataAPI(`${path}?from=${cursor}&count=${count}`)) ?? {};
|
|
8022
|
+
} catch (error) {
|
|
8023
|
+
if (cursor === start && isUnknownRangeParamsError(error)) {
|
|
8024
|
+
legacy = true;
|
|
8025
|
+
break;
|
|
8026
|
+
}
|
|
8027
|
+
throw error;
|
|
8028
|
+
}
|
|
8029
|
+
if (cursor === start) {
|
|
8030
|
+
total = asNumber(page.total_messages) ?? 0;
|
|
8031
|
+
summary = page.summary ?? null;
|
|
8032
|
+
ordering = asRecord(page.ordering) ?? undefined;
|
|
8033
|
+
}
|
|
8034
|
+
const batch = pageMessages(page);
|
|
8035
|
+
messages.push(...batch);
|
|
8036
|
+
const next = asNumber(page.next_from);
|
|
8037
|
+
nextFrom = page.has_more === true && next !== null && next !== undefined && next > cursor ? next : null;
|
|
8038
|
+
if (nextFrom === null || batch.length === 0 || messages.length >= requested)
|
|
8039
|
+
break;
|
|
8040
|
+
cursor = nextFrom;
|
|
8041
|
+
}
|
|
8042
|
+
if (legacy) {
|
|
8043
|
+
const limit = Math.min(requested, LEGACY_WINDOW_MAX_MESSAGES);
|
|
8044
|
+
const window = 5;
|
|
8045
|
+
const span = window * 2 + 1;
|
|
8046
|
+
const byIndex = new Map;
|
|
8047
|
+
for (let center = start + window;; center += span) {
|
|
8048
|
+
const page = asRecord(await callDataAPI(`${path}?msg_index=${center}&window=${window}`)) ?? {};
|
|
8049
|
+
if (center === start + window) {
|
|
8050
|
+
total = asNumber(page.total_messages) ?? 0;
|
|
8051
|
+
summary = page.summary ?? null;
|
|
8052
|
+
}
|
|
8053
|
+
for (const message of pageMessages(page)) {
|
|
8054
|
+
const index = asNumber(message.index);
|
|
8055
|
+
if (index !== null && index !== undefined && index >= start && index < start + limit)
|
|
8056
|
+
byIndex.set(index, message);
|
|
8057
|
+
}
|
|
8058
|
+
if (center + window + 1 >= Math.min(total, start + limit))
|
|
8059
|
+
break;
|
|
8060
|
+
}
|
|
8061
|
+
messages.push(...[...byIndex.entries()].sort(([a], [b]) => a - b).map(([, message]) => message));
|
|
8062
|
+
const end = start + messages.length;
|
|
8063
|
+
nextFrom = end < total && messages.length > 0 ? end : null;
|
|
8064
|
+
warnings.push(`This Moda server has no range reads for context (older backend), so --all fell back to windowed paging ` + `and stopped at ${LEGACY_WINDOW_MAX_MESSAGES} messages to keep it cheap.`);
|
|
8065
|
+
}
|
|
8066
|
+
const pageSize = legacy ? LEGACY_WINDOW_MAX_MESSAGES : requested;
|
|
8067
|
+
const tied = asNumber(ordering?.tied_timestamp_messages) ?? 0;
|
|
8068
|
+
const synthetic = asNumber(ordering?.synthetic_timestamp_messages) ?? 0;
|
|
8069
|
+
const atCap = (value) => {
|
|
8070
|
+
const text = typeof value === "string" ? value : value === undefined || value === null ? "" : JSON.stringify(value);
|
|
8071
|
+
return text.length >= CONTEXT_MESSAGE_TRUNCATE_CHARS;
|
|
8072
|
+
};
|
|
8073
|
+
const isTruncated = (record) => {
|
|
8074
|
+
const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
|
|
8075
|
+
const toolResults = Array.isArray(record.tool_results) ? record.tool_results : [];
|
|
8076
|
+
return atCap(record.content) || toolCalls.some((call) => atCap(asRecord(call)?.input)) || toolResults.some((result) => atCap(asRecord(result)?.content));
|
|
8077
|
+
};
|
|
8078
|
+
const build = (kept) => {
|
|
8079
|
+
const returned = messages.slice(0, kept);
|
|
8080
|
+
const out = [...notFoundWarning("trace", conversationId, total === 0), ...warnings];
|
|
8081
|
+
if (total > 0 && start >= total) {
|
|
8082
|
+
out.push(`--from=${start} is past the end of this trace (${total} messages, indexes 0-${total - 1}).`);
|
|
8083
|
+
}
|
|
8084
|
+
const nextCommands = [];
|
|
8085
|
+
const firstMissing = kept < messages.length ? asNumber(messages[kept]?.index) ?? start + kept : null;
|
|
8086
|
+
const next = firstMissing ?? nextFrom;
|
|
8087
|
+
if (next !== null) {
|
|
8088
|
+
const last = asNumber(returned[returned.length - 1]?.index) ?? next - 1;
|
|
8089
|
+
const nextMax = firstMissing !== null ? Math.min(pageSize, Math.max(kept * 2, 50)) : pageSize;
|
|
8090
|
+
const continueWith = `moda context ${shellArg(conversationId)} --all --from=${next} --max-messages=${nextMax}`;
|
|
8091
|
+
out.push(`Trace has ${total} messages; returned messages ${start}-${last}. ` + (firstMissing !== null ? `Stopped at ${kept} of the ${messages.length} messages read to stay under the agent output byte limit (--json has no limit). ` : "") + `Continue with: ${continueWith}`);
|
|
8092
|
+
nextCommands.push({
|
|
8093
|
+
command: continueWith,
|
|
8094
|
+
purpose: `Read the next messages of this trace, starting at message ${next}.`,
|
|
8095
|
+
mutability: "read",
|
|
8096
|
+
requires_approval: false
|
|
8097
|
+
});
|
|
8098
|
+
}
|
|
8099
|
+
if (tied > 0 || synthetic > 0) {
|
|
8100
|
+
out.push(`Message order may not be chronological: ${tied} of ${total} messages share a timestamp with another message ` + `(ordered by id, not time)${synthetic > 0 ? ` and ${synthetic} carry a synthetic pre-2022 timestamp` : ""}.`);
|
|
8101
|
+
}
|
|
8102
|
+
if (returned.some(isTruncated)) {
|
|
8103
|
+
out.push(`Message text, tool-call inputs and tool results longer than ${CONTEXT_MESSAGE_TRUNCATE_CHARS} characters are truncated by the context endpoint. ` + "For full raw content (prompts, tool arguments/results) use `moda audit <conversation_id> --include-raw`.");
|
|
8104
|
+
}
|
|
8105
|
+
return {
|
|
8106
|
+
data: stripLoneSurrogates({
|
|
8107
|
+
conversation_id: conversationId,
|
|
8108
|
+
total_messages: total,
|
|
8109
|
+
from: start,
|
|
8110
|
+
returned: returned.length,
|
|
8111
|
+
has_more: next !== null,
|
|
8112
|
+
next_from: next,
|
|
8113
|
+
summary,
|
|
8114
|
+
...ordering ? { ordering } : {},
|
|
8115
|
+
messages: returned
|
|
8116
|
+
}),
|
|
8117
|
+
warnings: out,
|
|
8118
|
+
nextCommands
|
|
8119
|
+
};
|
|
8120
|
+
};
|
|
8121
|
+
const full = build(messages.length);
|
|
8122
|
+
if (!opts.fits || messages.length <= 1 || opts.fits(full))
|
|
8123
|
+
return full;
|
|
8124
|
+
let low = 1;
|
|
8125
|
+
let high = messages.length - 1;
|
|
8126
|
+
while (low < high) {
|
|
8127
|
+
const mid = Math.ceil((low + high) / 2);
|
|
8128
|
+
if (opts.fits(build(mid)))
|
|
8129
|
+
low = mid;
|
|
8130
|
+
else
|
|
8131
|
+
high = mid - 1;
|
|
8132
|
+
}
|
|
8133
|
+
return build(low);
|
|
8134
|
+
}
|
|
6200
8135
|
async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
|
|
8136
|
+
assertHandParsedFlags(command, flags, positionals);
|
|
6201
8137
|
switch (command) {
|
|
6202
8138
|
case "prompts": {
|
|
6203
8139
|
await runPromptsCommand(context);
|
|
@@ -6205,7 +8141,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6205
8141
|
}
|
|
6206
8142
|
case "overview": {
|
|
6207
8143
|
const args = flagsToArgs(flags);
|
|
6208
|
-
OverviewSchema
|
|
8144
|
+
parseCommandFlags(OverviewSchema, args, command, { extraFlags: OVERVIEW_PRODUCTION_FLAGS });
|
|
6209
8145
|
return (await runProductionCommand(context)).exitCode;
|
|
6210
8146
|
}
|
|
6211
8147
|
case "investigate":
|
|
@@ -6215,7 +8151,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6215
8151
|
}
|
|
6216
8152
|
case "clusters": {
|
|
6217
8153
|
const args = flagsToArgs(flags);
|
|
6218
|
-
const params = ClustersSchema
|
|
8154
|
+
const params = parseCommandFlags(ClustersSchema, args, command);
|
|
6219
8155
|
if (params.search !== undefined || params.node_id !== undefined) {
|
|
6220
8156
|
const query2 = new URLSearchParams;
|
|
6221
8157
|
if (params.search !== undefined)
|
|
@@ -6249,7 +8185,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6249
8185
|
throw new CliInputError("<node_id> is required", "Usage: moda cluster-traces <node_id> [--limit=N] [--offset=N]");
|
|
6250
8186
|
}
|
|
6251
8187
|
const args = flagsToArgs(flags, "node_id", positional);
|
|
6252
|
-
const params = ClusterConversationsSchema
|
|
8188
|
+
const params = parseCommandFlags(ClusterConversationsSchema, args, command, { positionalKey: "node_id" });
|
|
6253
8189
|
const query = new URLSearchParams;
|
|
6254
8190
|
if (params.limit)
|
|
6255
8191
|
query.set("limit", params.limit.toString());
|
|
@@ -6264,8 +8200,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6264
8200
|
}
|
|
6265
8201
|
case "traces":
|
|
6266
8202
|
case "conversations": {
|
|
6267
|
-
const args = flagsToArgs(flags);
|
|
6268
|
-
const params = ConversationsSchema
|
|
8203
|
+
const args = withStringIdFlags(flagsToArgs(flags), flags);
|
|
8204
|
+
const params = parseCommandFlags(ConversationsSchema, args, command);
|
|
6269
8205
|
const query = new URLSearchParams;
|
|
6270
8206
|
if (params.search)
|
|
6271
8207
|
query.set("search", params.search);
|
|
@@ -6281,20 +8217,40 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6281
8217
|
query.set("world_state", params.world_state);
|
|
6282
8218
|
if (params.outcome && params.outcome !== "any")
|
|
6283
8219
|
query.set("outcome", params.outcome);
|
|
8220
|
+
if (params.sort)
|
|
8221
|
+
query.set("sort_by", params.sort);
|
|
6284
8222
|
if (params.limit)
|
|
6285
8223
|
query.set("limit", params.limit.toString());
|
|
6286
8224
|
if (params.offset !== undefined)
|
|
6287
8225
|
query.set("offset", params.offset.toString());
|
|
6288
8226
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6289
|
-
const
|
|
6290
|
-
recordConversationsTelemetry(context,
|
|
8227
|
+
const raw = await callDataAPI(`/conversations${queryString}`);
|
|
8228
|
+
recordConversationsTelemetry(context, raw);
|
|
8229
|
+
const searchWarnings = [];
|
|
8230
|
+
let data = raw;
|
|
8231
|
+
const rawRecord = asRecord(raw);
|
|
8232
|
+
if (params.search && rawRecord) {
|
|
8233
|
+
const traceMode = asString(rawRecord.search_mode) ?? "keyword";
|
|
8234
|
+
const literal = traceMode === "keyword";
|
|
8235
|
+
data = { ...rawRecord, total_kind: literal ? "matches" : "candidate_pool" };
|
|
8236
|
+
if (!literal) {
|
|
8237
|
+
searchWarnings.push(`--search ran ${traceMode}-ranked: results are related traces that may not contain "${params.search}", and pagination.total is the candidate pool, not a match count.`);
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
if (params.sort === "slowest") {
|
|
8241
|
+
const rows = asRecord(data)?.conversations;
|
|
8242
|
+
if (Array.isArray(rows) && rows.length > 0 && rows.every((row) => asRecord(row)?.duration_ms == null)) {
|
|
8243
|
+
searchWarnings.push("--sort=slowest: no trace in this page has a known duration (synthetic timestamps), so the order is not by duration. Use --sort=longest (message count) instead.");
|
|
8244
|
+
}
|
|
8245
|
+
}
|
|
8246
|
+
const writeOptions = searchWarnings.length > 0 ? { warnings: searchWarnings } : undefined;
|
|
6291
8247
|
if (params.include_world_state === true) {
|
|
6292
8248
|
const enriched = asRecord(data) ?? {};
|
|
6293
8249
|
const list = Array.isArray(enriched.conversations) ? enriched.conversations : [];
|
|
6294
8250
|
const withState = await attachWorldState(list);
|
|
6295
|
-
context.output.writeData({ ...enriched, conversations: withState });
|
|
8251
|
+
context.output.writeData({ ...enriched, conversations: withState }, writeOptions);
|
|
6296
8252
|
} else {
|
|
6297
|
-
context.output.writeData(data);
|
|
8253
|
+
context.output.writeData(data, writeOptions);
|
|
6298
8254
|
}
|
|
6299
8255
|
break;
|
|
6300
8256
|
}
|
|
@@ -6302,8 +8258,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6302
8258
|
if (!positional || positional.trim() === "") {
|
|
6303
8259
|
throw new CliInputError("<query> is required", 'Usage: moda search "<query>" [--mode=keyword|semantic|hybrid] [--user-id=ID] [--time-range=RANGE] [--include-tool-io] [--limit=N]');
|
|
6304
8260
|
}
|
|
6305
|
-
const args = flagsToArgs(flags, "q", positional);
|
|
6306
|
-
const params = SearchSchema
|
|
8261
|
+
const args = withStringIdFlags(flagsToArgs(flags, "q", positional), flags);
|
|
8262
|
+
const params = parseCommandFlags(SearchSchema, args, command, { positionalKey: "q" });
|
|
6307
8263
|
const query = new URLSearchParams;
|
|
6308
8264
|
query.set("q", params.q);
|
|
6309
8265
|
if (params.mode)
|
|
@@ -6327,6 +8283,30 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6327
8283
|
} else if (params.mode && actualMode && actualMode !== params.mode && params.mode !== "hybrid") {
|
|
6328
8284
|
warnings.push(`Search ran in ${actualMode} mode instead of the requested ${params.mode} mode.`);
|
|
6329
8285
|
}
|
|
8286
|
+
const results = Array.isArray(searchRecord.results) ? searchRecord.results : [];
|
|
8287
|
+
if (results.length > 0 && (params.offset ?? 0) > 0) {
|
|
8288
|
+
warnings.push(`--offset=${params.offset} was not applied: message-level search is not pageable (it returns the top --limit hits). ` + "--offset only pages the trace-level fallback. Narrow with --time-range/--user-id or raise --limit instead.");
|
|
8289
|
+
}
|
|
8290
|
+
if (results.length === 0) {
|
|
8291
|
+
const fallback = await searchFallbackToTraces({ ...params, offset: params.offset });
|
|
8292
|
+
if (fallback?.kind === "notice") {
|
|
8293
|
+
warnings.push(fallback.warning);
|
|
8294
|
+
context.output.writeData(data, { warnings, nextCommands: fallback.nextCommands });
|
|
8295
|
+
break;
|
|
8296
|
+
}
|
|
8297
|
+
if (fallback?.kind === "results") {
|
|
8298
|
+
warnings.push(fallback.warning);
|
|
8299
|
+
const fallbackResults = fallback.data.results;
|
|
8300
|
+
const merged = {
|
|
8301
|
+
...searchRecord,
|
|
8302
|
+
...fallback.data,
|
|
8303
|
+
pagination: { ...asRecord(searchRecord.pagination) ?? {}, returned: fallbackResults.length, has_more: fallback.hasMore }
|
|
8304
|
+
};
|
|
8305
|
+
recordSearchTelemetry(context, merged);
|
|
8306
|
+
context.output.writeData(merged, { warnings, nextCommands: fallback.nextCommands });
|
|
8307
|
+
break;
|
|
8308
|
+
}
|
|
8309
|
+
}
|
|
6330
8310
|
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
6331
8311
|
break;
|
|
6332
8312
|
}
|
|
@@ -6335,7 +8315,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6335
8315
|
throw new CliInputError("<conversation_id> is required", "Usage: moda world-state <conversation_id> [--summary-only] [--event-limit=N]");
|
|
6336
8316
|
}
|
|
6337
8317
|
const args = flagsToArgs(flags, "conversation_id", positional);
|
|
6338
|
-
const params = WorldStateSchema
|
|
8318
|
+
const params = parseCommandFlags(WorldStateSchema, args, command, { positionalKey: "conversation_id" });
|
|
6339
8319
|
if (params.snapshot === true && params.replay === true) {
|
|
6340
8320
|
throw new CliInputError("Choose either --snapshot or --replay, not both.", "Usage: moda world-state <conversation_id> [--snapshot --msg-index=N [--block-index=M] [--segment-index=K]] | [--replay --message-count=N [--format=frames|fold-inputs]]");
|
|
6341
8321
|
}
|
|
@@ -6384,7 +8364,26 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6384
8364
|
throw new CliInputError("<conversation_id> is required", "Usage: moda context <conversation_id> [--msg-index=N] [--window=N]");
|
|
6385
8365
|
}
|
|
6386
8366
|
const args = flagsToArgs(flags, "conversation_id", positional);
|
|
6387
|
-
const params = ContextSchema
|
|
8367
|
+
const params = parseCommandFlags(ContextSchema, args, command, { positionalKey: "conversation_id" });
|
|
8368
|
+
if (params.all === true) {
|
|
8369
|
+
if (params.msg_index !== undefined || flags.window !== undefined) {
|
|
8370
|
+
throw new CliInputError("--all reads the whole trace; drop --msg-index/--window.", "Usage: moda context <conversation_id> --all [--from=N] [--max-messages=N]");
|
|
8371
|
+
}
|
|
8372
|
+
const agentMode = context.output.mode === "agent" || context.output.mode === "agent-stream";
|
|
8373
|
+
const full = await readFullTranscript(params.conversation_id, {
|
|
8374
|
+
from: params.from,
|
|
8375
|
+
maxMessages: params.max_messages,
|
|
8376
|
+
fits: agentMode ? (page) => agentEnvelopeFits(command, page.data, { warnings: page.warnings, nextCommands: page.nextCommands }) : undefined
|
|
8377
|
+
});
|
|
8378
|
+
context.output.writeData(full.data, {
|
|
8379
|
+
...full.warnings.length > 0 ? { warnings: full.warnings } : {},
|
|
8380
|
+
...full.nextCommands.length > 0 ? { nextCommands: full.nextCommands } : {}
|
|
8381
|
+
});
|
|
8382
|
+
break;
|
|
8383
|
+
}
|
|
8384
|
+
if (params.from !== undefined || params.max_messages !== undefined) {
|
|
8385
|
+
throw new CliInputError("--from and --max-messages page the whole trace and need --all.", "Usage: moda context <conversation_id> --all [--from=N] [--max-messages=N]");
|
|
8386
|
+
}
|
|
6388
8387
|
const query = new URLSearchParams;
|
|
6389
8388
|
if (params.msg_index !== undefined)
|
|
6390
8389
|
query.set("msg_index", params.msg_index.toString());
|
|
@@ -6403,7 +8402,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6403
8402
|
throw new CliInputError("<conversation_id|trace_id> is required", "Usage: moda audit <conversation_id|trace_id> [--kind=auto|trace|conversation] [--include-raw] [--limit=N]");
|
|
6404
8403
|
}
|
|
6405
8404
|
const args = flagsToArgs(flags, "id", positional);
|
|
6406
|
-
const params = AuditSchema
|
|
8405
|
+
const params = parseCommandFlags(AuditSchema, args, command, { positionalKey: "id" });
|
|
6407
8406
|
const query = new URLSearchParams;
|
|
6408
8407
|
if (params.kind && params.kind !== "auto")
|
|
6409
8408
|
query.set("kind", params.kind);
|
|
@@ -6413,12 +8412,37 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6413
8412
|
query.set("limit", params.limit.toString());
|
|
6414
8413
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6415
8414
|
const data = await callDataAPI(`/audit/${encodeURIComponent(params.id)}${queryString}`);
|
|
6416
|
-
|
|
8415
|
+
const capped = auditCapStatus(data, params.limit ?? AUDIT_DEFAULT_LIMIT);
|
|
8416
|
+
const auditRecord = asRecord(data);
|
|
8417
|
+
if (!capped || !auditRecord) {
|
|
8418
|
+
context.output.writeData(auditRecord ? { ...auditRecord, truncated: false, has_more: false } : data);
|
|
8419
|
+
break;
|
|
8420
|
+
}
|
|
8421
|
+
const limit = params.limit ?? AUDIT_DEFAULT_LIMIT;
|
|
8422
|
+
const perTrace = "audit one OTLP trace at a time with `moda audit <trace_id> --kind=trace` (ids in summary.trace_ids)";
|
|
8423
|
+
const warnings = [];
|
|
8424
|
+
const nextCommands = [];
|
|
8425
|
+
if (capped.rowsCapped) {
|
|
8426
|
+
const bigger = Math.min(AUDIT_MAX_LIMIT, limit * 5);
|
|
8427
|
+
warnings.push(`Spans were capped: read ${capped.rows} ingest row(s), the --limit=${limit} maximum, so this trace ${capped.hasMoreKnown ? "has" : "may have"} more spans than the ${capped.spans} shown. ` + (limit < AUDIT_MAX_LIMIT ? `Rerun with --limit=${bigger} (max ${AUDIT_MAX_LIMIT}), or ${perTrace}.` : `${perTrace[0].toUpperCase()}${perTrace.slice(1)}.`));
|
|
8428
|
+
if (limit < AUDIT_MAX_LIMIT) {
|
|
8429
|
+
const rerun = [
|
|
8430
|
+
`moda ${command} ${shellArg(params.id)}`,
|
|
8431
|
+
params.kind && params.kind !== "auto" ? `--kind=${params.kind}` : "",
|
|
8432
|
+
params.include_raw === true ? "--include-raw" : "",
|
|
8433
|
+
`--limit=${bigger}`
|
|
8434
|
+
].filter(Boolean).join(" ");
|
|
8435
|
+
nextCommands.push({ command: rerun, purpose: "Read more ingest rows of this trace.", mutability: "read", requires_approval: false });
|
|
8436
|
+
}
|
|
8437
|
+
} else {
|
|
8438
|
+
warnings.push(`Spans were capped: the server returned ${capped.spans} span(s) and marked the response truncated. ${perTrace[0].toUpperCase()}${perTrace.slice(1)}.`);
|
|
8439
|
+
}
|
|
8440
|
+
context.output.writeData({ ...auditRecord, truncated: true, has_more: capped.rowsCapped }, { warnings, ...nextCommands.length > 0 ? { nextCommands } : {} });
|
|
6417
8441
|
break;
|
|
6418
8442
|
}
|
|
6419
8443
|
case "frustrations": {
|
|
6420
|
-
const args = flagsToArgs(flags);
|
|
6421
|
-
const params = FrustrationsSchema
|
|
8444
|
+
const args = withStringIdFlags(flagsToArgs(flags), flags);
|
|
8445
|
+
const params = parseCommandFlags(FrustrationsSchema, args, command);
|
|
6422
8446
|
const query = new URLSearchParams;
|
|
6423
8447
|
if (params.days_back)
|
|
6424
8448
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6426,14 +8450,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6426
8450
|
query.set("limit", params.limit.toString());
|
|
6427
8451
|
if (params.offset !== undefined)
|
|
6428
8452
|
query.set("offset", params.offset.toString());
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
8453
|
+
if (params.user_id)
|
|
8454
|
+
query.set("user_id", params.user_id);
|
|
8455
|
+
if (params.conversation_id)
|
|
8456
|
+
query.set("conversation_id", params.conversation_id);
|
|
8457
|
+
query.set("family", "frustration");
|
|
8458
|
+
const data = frustrationsFromEmotions(await callDataAPI(`/emotions?${query.toString()}`));
|
|
8459
|
+
await printWithOptionalWindows(withFrustrationAnchors(data), "frustrations", params.include_window === true, params.window ?? 1, context.output, processingDisabledWarning(data, "frustration"));
|
|
6432
8460
|
break;
|
|
6433
8461
|
}
|
|
6434
8462
|
case "tool-failures": {
|
|
6435
8463
|
const args = flagsToArgs(flags);
|
|
6436
|
-
const params = ToolFailuresSchema
|
|
8464
|
+
const params = parseCommandFlags(ToolFailuresSchema, args, command);
|
|
6437
8465
|
const queryString = params.days_back ? `?days_back=${params.days_back}` : "";
|
|
6438
8466
|
const data = await callDataAPI(`/tool-failures${queryString}`);
|
|
6439
8467
|
context.output.writeData(data);
|
|
@@ -6441,17 +8469,27 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6441
8469
|
}
|
|
6442
8470
|
case "tool-failure-detail": {
|
|
6443
8471
|
if (!positional) {
|
|
6444
|
-
throw new CliInputError("<tool_name> is required", "Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N] [--offset=N]");
|
|
8472
|
+
throw new CliInputError("<tool_name> is required", "Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N (1-20, default 5)] [--offset=N] [--compact | --include-window [--window=N]]");
|
|
8473
|
+
}
|
|
8474
|
+
const { "no-include-window": noIncludeWindow, ...toolFlags } = flags;
|
|
8475
|
+
if (noIncludeWindow !== undefined && noIncludeWindow !== "false") {
|
|
8476
|
+
if (toolFlags["include-window"] !== undefined && toolFlags["include-window"] !== "false") {
|
|
8477
|
+
throw new CliInputError("Choose either --include-window or --no-include-window, not both.");
|
|
8478
|
+
}
|
|
8479
|
+
toolFlags["include-window"] = "false";
|
|
8480
|
+
}
|
|
8481
|
+
const args = flagsToArgs(toolFlags, "tool_name", positional);
|
|
8482
|
+
const params = parseCommandFlags(ToolFailureDetailSchema, args, command, { positionalKey: "tool_name" });
|
|
8483
|
+
const dropConversation = params.compact === true || params.include_window === false;
|
|
8484
|
+
if (params.compact === true && params.include_window === true) {
|
|
8485
|
+
throw new CliInputError("--compact drops the conversation window; drop --include-window, or drop --compact.");
|
|
6445
8486
|
}
|
|
6446
|
-
const args = flagsToArgs(flags, "tool_name", positional);
|
|
6447
|
-
const params = ToolFailureDetailSchema.parse(args);
|
|
6448
8487
|
const query = new URLSearchParams;
|
|
6449
8488
|
if (params.subtype)
|
|
6450
8489
|
query.set("subtype", params.subtype);
|
|
6451
8490
|
if (params.days_back)
|
|
6452
8491
|
query.set("days_back", params.days_back.toString());
|
|
6453
|
-
|
|
6454
|
-
query.set("limit", params.limit.toString());
|
|
8492
|
+
query.set("limit", (params.limit ?? TOOL_FAILURE_DETAIL_DEFAULT_LIMIT).toString());
|
|
6455
8493
|
if (params.offset !== undefined)
|
|
6456
8494
|
query.set("offset", params.offset.toString());
|
|
6457
8495
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
@@ -6462,7 +8500,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6462
8500
|
const warnings = subtypeCount === 0 && exampleCount === 0 ? [
|
|
6463
8501
|
`No failures found for tool "${params.tool_name}" in the last ${params.days_back ?? 7} day(s) — the tool may have no failures in this window, or the name may not exist in this tenant. Run \`moda tool-failures\` to list tools with recorded failures, or widen --days-back.`
|
|
6464
8502
|
] : [];
|
|
6465
|
-
|
|
8503
|
+
const knownSubtypes = Array.isArray(tfRecord.subtypes) ? tfRecord.subtypes.map((row) => asString(asRecord(row)?.subtype)).filter((value) => Boolean(value)) : [];
|
|
8504
|
+
if (params.subtype && knownSubtypes.length > 0 && !knownSubtypes.includes(params.subtype)) {
|
|
8505
|
+
warnings.push(`--subtype=${params.subtype} is not a failure subtype of "${params.tool_name}" in this window, so no examples matched. ` + `Valid subtypes: ${knownSubtypes.slice(0, 10).join(", ")}${knownSubtypes.length > 10 ? `, … (${knownSubtypes.length - 10} more in \`subtypes\`)` : ""}.`);
|
|
8506
|
+
}
|
|
8507
|
+
await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings, (payload, out) => {
|
|
8508
|
+
let trimmed = trimToolFailureConversations(payload, {
|
|
8509
|
+
drop: dropConversation || params.include_window === true,
|
|
8510
|
+
windowRadius: params.window ?? 1
|
|
8511
|
+
});
|
|
8512
|
+
const agentMode = context.output.mode === "agent" || context.output.mode === "agent-stream";
|
|
8513
|
+
const subtypes = Array.isArray(trimmed.subtypes) ? trimmed.subtypes : [];
|
|
8514
|
+
if (agentMode && subtypes.length > TOOL_FAILURE_AGENT_SUBTYPES) {
|
|
8515
|
+
const ranked = [...subtypes].sort((a, b) => (asNumber(asRecord(b)?.count) ?? 0) - (asNumber(asRecord(a)?.count) ?? 0));
|
|
8516
|
+
trimmed = { ...trimmed, subtypes: ranked.slice(0, TOOL_FAILURE_AGENT_SUBTYPES), subtypes_total: subtypes.length };
|
|
8517
|
+
out.push(`Showing the ${TOOL_FAILURE_AGENT_SUBTYPES} most frequent of ${subtypes.length} failure subtypes; --json lists all of them.`);
|
|
8518
|
+
}
|
|
8519
|
+
const { data: compacted, truncated } = compactToolFailureExamples(trimmed);
|
|
8520
|
+
if (truncated > 0) {
|
|
8521
|
+
out.push(`${truncated} long text field(s) in examples were cut to ${TOOL_FAILURE_TEXT_LIMIT} characters to keep this output small. For the full text, run each example's \`context_command\` (\`moda context <conversation_id> --msg-index=<msg_index>\`).`);
|
|
8522
|
+
}
|
|
8523
|
+
return compacted;
|
|
8524
|
+
});
|
|
6466
8525
|
break;
|
|
6467
8526
|
}
|
|
6468
8527
|
case "problem-impact": {
|
|
@@ -6491,12 +8550,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6491
8550
|
link.set("useCase", params.node_id);
|
|
6492
8551
|
const problem = asString(data.problemId) || params.problem_id;
|
|
6493
8552
|
const path = problem ? `/dashboard/problems/${encodeURIComponent(problem)}` : params.node_id ? `/dashboard/use-cases/${encodeURIComponent(params.node_id)}` : "/dashboard/use-cases";
|
|
6494
|
-
|
|
8553
|
+
const impactWarnings = serverWarnings(data);
|
|
8554
|
+
context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` }, impactWarnings.length > 0 ? { warnings: impactWarnings } : undefined);
|
|
6495
8555
|
break;
|
|
6496
8556
|
}
|
|
6497
8557
|
case "problems": {
|
|
8558
|
+
if (positional === "remainder") {
|
|
8559
|
+
await runProblemsRemainder(flags, context);
|
|
8560
|
+
break;
|
|
8561
|
+
}
|
|
8562
|
+
if (positional) {
|
|
8563
|
+
throw new CliInputError(`Unknown problems subcommand '${positional}'.`, "Usage: moda problems [--days-back=N] [--limit=N] | moda problems remainder [--family=F] [--days-back=N] [--limit=N] [--cursor=TOKEN]. For one Problem use 'moda problem <id>'.");
|
|
8564
|
+
}
|
|
6498
8565
|
const args = flagsToArgs(flags);
|
|
6499
|
-
const params = ProblemsSchema
|
|
8566
|
+
const params = parseCommandFlags(ProblemsSchema, args, command);
|
|
6500
8567
|
const query = new URLSearchParams;
|
|
6501
8568
|
if (params.days_back)
|
|
6502
8569
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6504,19 +8571,26 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6504
8571
|
query.set("limit", params.limit.toString());
|
|
6505
8572
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6506
8573
|
const data = await callDataAPI(`/problems${queryString}`);
|
|
8574
|
+
const { payload, truncated } = capProblemsBlocks(asRecord(data) ?? {}, params.limit ?? 25);
|
|
6507
8575
|
const withLink = {
|
|
6508
|
-
...
|
|
8576
|
+
...payload,
|
|
8577
|
+
...truncated ? { truncated } : {},
|
|
6509
8578
|
dashboard_url: `${MODA_BASE_URL}/dashboard/problems`
|
|
6510
8579
|
};
|
|
6511
|
-
|
|
8580
|
+
const warnings = [...serverWarnings(payload), ...truncated ? [`--limit=${params.limit ?? 25} also capped ${Object.entries(truncated).map(([block, count]) => `${block} (${count} more)`).join(", ")}; raise --limit (max 25) or open dashboard_url for the rest.`] : []];
|
|
8581
|
+
context.output.writeData(withLink, warnings.length > 0 ? { warnings } : undefined);
|
|
6512
8582
|
break;
|
|
6513
8583
|
}
|
|
6514
8584
|
case "problem": {
|
|
8585
|
+
if (positional === "remainder") {
|
|
8586
|
+
await runProblemsRemainder(flags, context);
|
|
8587
|
+
break;
|
|
8588
|
+
}
|
|
6515
8589
|
if (!positional) {
|
|
6516
8590
|
throw new CliInputError("<problem_id> is required", "Usage: moda problem <problem_id> [--evidence|--reports|--traces|--feedback] [--limit=N] [--cursor=TOKEN] [--family=F] [--door=D]");
|
|
6517
8591
|
}
|
|
6518
8592
|
const args = flagsToArgs(flags, "id", positional);
|
|
6519
|
-
const params = ProblemSchema
|
|
8593
|
+
const params = parseCommandFlags(ProblemSchema, args, command, { positionalKey: "id" });
|
|
6520
8594
|
const traceView = params.traces === true || params.conversations === true;
|
|
6521
8595
|
const views = ["evidence", "reports", "conversations", "feedback"].filter((view2) => view2 === "conversations" ? traceView : params[view2] === true);
|
|
6522
8596
|
if (views.length > 1) {
|
|
@@ -6664,6 +8738,9 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6664
8738
|
break;
|
|
6665
8739
|
}
|
|
6666
8740
|
case "signals": {
|
|
8741
|
+
if (positional && SIGNALS_MANAGEMENT_SUBCOMMANDS.has(positional)) {
|
|
8742
|
+
return (await runSignalsManagementCommand(positional, context)).exitCode;
|
|
8743
|
+
}
|
|
6667
8744
|
const data = await callDataAPI("/custom-signals");
|
|
6668
8745
|
context.output.writeData(data);
|
|
6669
8746
|
break;
|
|
@@ -6674,7 +8751,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6674
8751
|
}
|
|
6675
8752
|
const args = flagsToArgs(flags, "id", positional);
|
|
6676
8753
|
args.id = positional;
|
|
6677
|
-
const params = SignalSchema
|
|
8754
|
+
const params = parseCommandFlags(SignalSchema, args, command, { positionalKey: "id" });
|
|
6678
8755
|
const id = encodeURIComponent(params.id);
|
|
6679
8756
|
if (params.detections !== true) {
|
|
6680
8757
|
const data2 = await callDataAPI(`/custom-signals/${id}`);
|
|
@@ -6863,8 +8940,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6863
8940
|
break;
|
|
6864
8941
|
}
|
|
6865
8942
|
case "emotions": {
|
|
6866
|
-
const args = flagsToArgs(flags);
|
|
6867
|
-
const params = EmotionsSchema
|
|
8943
|
+
const args = withStringIdFlags(flagsToArgs(flags), flags);
|
|
8944
|
+
const params = parseCommandFlags(EmotionsSchema, args, command);
|
|
6868
8945
|
const query = new URLSearchParams;
|
|
6869
8946
|
if (params.days_back)
|
|
6870
8947
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6874,9 +8951,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6874
8951
|
query.set("offset", params.offset.toString());
|
|
6875
8952
|
if (params.family)
|
|
6876
8953
|
query.set("family", params.family);
|
|
8954
|
+
if (params.user_id)
|
|
8955
|
+
query.set("user_id", params.user_id);
|
|
8956
|
+
if (params.conversation_id)
|
|
8957
|
+
query.set("conversation_id", params.conversation_id);
|
|
6877
8958
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6878
8959
|
const data = await callDataAPI(`/emotions${queryString}`);
|
|
6879
|
-
|
|
8960
|
+
const processingWarnings = processingDisabledWarning(data, "emotion");
|
|
8961
|
+
context.output.writeData(data, processingWarnings.length > 0 ? { warnings: processingWarnings } : undefined);
|
|
6880
8962
|
break;
|
|
6881
8963
|
}
|
|
6882
8964
|
case "laziness": {
|
|
@@ -6884,7 +8966,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6884
8966
|
if (flags["conversation-id"] !== undefined) {
|
|
6885
8967
|
args.conversation_id = flags["conversation-id"];
|
|
6886
8968
|
}
|
|
6887
|
-
const params = LazinessSchema
|
|
8969
|
+
const params = parseCommandFlags(LazinessSchema, args, command);
|
|
6888
8970
|
const query = new URLSearchParams;
|
|
6889
8971
|
if (params.days_back)
|
|
6890
8972
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6906,7 +8988,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6906
8988
|
if (flags["conversation-id"] !== undefined) {
|
|
6907
8989
|
args.conversation_id = flags["conversation-id"];
|
|
6908
8990
|
}
|
|
6909
|
-
const params = HallucinationsSchema
|
|
8991
|
+
const params = parseCommandFlags(HallucinationsSchema, args, command);
|
|
6910
8992
|
const query = new URLSearchParams;
|
|
6911
8993
|
if (params.days_back)
|
|
6912
8994
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6940,7 +9022,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6940
9022
|
}
|
|
6941
9023
|
case "tail": {
|
|
6942
9024
|
const args = flagsToArgs(flags);
|
|
6943
|
-
const params = TailSchema
|
|
9025
|
+
const params = parseCommandFlags(TailSchema, args, command);
|
|
6944
9026
|
await runTailCommand(params, context);
|
|
6945
9027
|
break;
|
|
6946
9028
|
}
|
|
@@ -7086,7 +9168,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7086
9168
|
resetApiRequestCountBeforeRun: true,
|
|
7087
9169
|
telemetry: "result",
|
|
7088
9170
|
handler: async (context) => {
|
|
7089
|
-
const { runInit } = await import("./index-
|
|
9171
|
+
const { runInit } = await import("./index-qgc37efj.js");
|
|
7090
9172
|
if (context.outputMode === "agent-stream") {
|
|
7091
9173
|
context.output.writeEvent({
|
|
7092
9174
|
event: "started",
|
|
@@ -7127,7 +9209,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7127
9209
|
resetApiRequestCountBeforeRun: true,
|
|
7128
9210
|
telemetry: "result-and-error",
|
|
7129
9211
|
handler: async (context) => {
|
|
7130
|
-
const { runProvision } = await import("./provision-
|
|
9212
|
+
const { runProvision } = await import("./provision-gabtks29.js");
|
|
7131
9213
|
return runProvision(context, {
|
|
7132
9214
|
resumeCommand: buildResumeCommand("provision", context.rawFlags)
|
|
7133
9215
|
});
|
|
@@ -7269,6 +9351,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7269
9351
|
description: "Diagnose local Moda setup and data flow",
|
|
7270
9352
|
examples: ["moda doctor", "moda doctor --json", "moda doctor --offline"],
|
|
7271
9353
|
auth: "none",
|
|
9354
|
+
authNote: "Offline by default; --online checks the API key and data flow.",
|
|
7272
9355
|
outputModes: ["human", "json", "agent"],
|
|
7273
9356
|
mutability: "read",
|
|
7274
9357
|
validateConfigBeforeRun: false,
|
|
@@ -7302,7 +9385,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7302
9385
|
},
|
|
7303
9386
|
{
|
|
7304
9387
|
name: "keys",
|
|
7305
|
-
description: "
|
|
9388
|
+
description: "Local key descriptors in ~/.moda only (not your server-side API keys; manage those in the dashboard)",
|
|
7306
9389
|
examples: ["moda keys list", "moda keys create --name=local-dev"],
|
|
7307
9390
|
auth: "none",
|
|
7308
9391
|
outputModes: ["human", "json", "agent"],
|
|
@@ -7324,7 +9407,9 @@ var commandRegistry = createCommandRegistry([
|
|
|
7324
9407
|
"moda harness delete my-harness-key --yes",
|
|
7325
9408
|
"moda harness explain --agent=agent_support"
|
|
7326
9409
|
],
|
|
7327
|
-
|
|
9410
|
+
subcommands: HARNESS_SUBCOMMAND_HELP,
|
|
9411
|
+
auth: "api-key",
|
|
9412
|
+
authNote: "sync, list, show, versions, diff, delete, pull, and analyze need the API key; scan, explain, agents, graph, approve, and validate-report are local.",
|
|
7328
9413
|
outputModes: ["human", "json", "agent"],
|
|
7329
9414
|
mutability: "write",
|
|
7330
9415
|
validateConfigBeforeRun: false,
|
|
@@ -7334,10 +9419,9 @@ var commandRegistry = createCommandRegistry([
|
|
|
7334
9419
|
},
|
|
7335
9420
|
{
|
|
7336
9421
|
name: "agent",
|
|
7337
|
-
description: "Agent-native Moda context,
|
|
9422
|
+
description: "Agent-native local Moda context, next actions, and evidence (production investigation: moda investigate)",
|
|
7338
9423
|
examples: [
|
|
7339
9424
|
"moda agent context",
|
|
7340
|
-
"moda agent investigate",
|
|
7341
9425
|
"moda agent next-action",
|
|
7342
9426
|
"moda agent evidence doctor:api_key_resolves"
|
|
7343
9427
|
],
|
|
@@ -7357,7 +9441,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7357
9441
|
"moda sync harness --from-report .moda/harness-report.json",
|
|
7358
9442
|
"moda sync prompts"
|
|
7359
9443
|
],
|
|
7360
|
-
auth: "
|
|
9444
|
+
auth: "api-key",
|
|
7361
9445
|
outputModes: ["human", "json", "agent"],
|
|
7362
9446
|
mutability: "write",
|
|
7363
9447
|
validateConfigBeforeRun: false,
|
|
@@ -7365,6 +9449,92 @@ var commandRegistry = createCommandRegistry([
|
|
|
7365
9449
|
telemetry: "none",
|
|
7366
9450
|
handler: runSyncCommand
|
|
7367
9451
|
},
|
|
9452
|
+
{
|
|
9453
|
+
name: "users",
|
|
9454
|
+
description: "List end users with health, frustration and activity (the dashboard Users page)",
|
|
9455
|
+
examples: [
|
|
9456
|
+
"moda users --window=30d --sort=health_score --order=asc",
|
|
9457
|
+
"moda users --cohort=frustrated --min-conversations=3"
|
|
9458
|
+
],
|
|
9459
|
+
auth: "api-key",
|
|
9460
|
+
outputModes: ["human", "json", "agent"],
|
|
9461
|
+
mutability: "read",
|
|
9462
|
+
validateConfigBeforeRun: false,
|
|
9463
|
+
resetApiRequestCountBeforeRun: false,
|
|
9464
|
+
telemetry: "result-and-error",
|
|
9465
|
+
handler: runUsersCommand
|
|
9466
|
+
},
|
|
9467
|
+
{
|
|
9468
|
+
name: "user",
|
|
9469
|
+
description: "One end user's profile: health, traits, frustration trajectory, activity",
|
|
9470
|
+
examples: ["moda user u-123 --window=30d"],
|
|
9471
|
+
auth: "api-key",
|
|
9472
|
+
outputModes: ["human", "json", "agent"],
|
|
9473
|
+
mutability: "read",
|
|
9474
|
+
validateConfigBeforeRun: false,
|
|
9475
|
+
resetApiRequestCountBeforeRun: false,
|
|
9476
|
+
telemetry: "result-and-error",
|
|
9477
|
+
handler: runUserCommand
|
|
9478
|
+
},
|
|
9479
|
+
{
|
|
9480
|
+
name: "problem-reopen",
|
|
9481
|
+
description: "Reopen a problem that was marked fixed (resolved problems return only via re-detection)",
|
|
9482
|
+
examples: ['moda problem-reopen <problem_id> --reason="still happening"', "moda problem-reopen <problem_id> --dry-run"],
|
|
9483
|
+
auth: "api-key",
|
|
9484
|
+
outputModes: ["human", "json", "agent"],
|
|
9485
|
+
mutability: "write",
|
|
9486
|
+
validateConfigBeforeRun: false,
|
|
9487
|
+
resetApiRequestCountBeforeRun: false,
|
|
9488
|
+
telemetry: "result-and-error",
|
|
9489
|
+
handler: runProblemReopenCommand
|
|
9490
|
+
},
|
|
9491
|
+
{
|
|
9492
|
+
name: "registry",
|
|
9493
|
+
description: "Push prompts, skills, and tools to Moda from one agent-written JSON manifest (no repo scan)",
|
|
9494
|
+
examples: [
|
|
9495
|
+
"moda registry template --out=.moda/registry.json",
|
|
9496
|
+
"moda registry validate",
|
|
9497
|
+
"moda registry push --dry-run",
|
|
9498
|
+
"moda registry push --file=.moda/registry.json"
|
|
9499
|
+
],
|
|
9500
|
+
subcommands: [
|
|
9501
|
+
{
|
|
9502
|
+
name: "template",
|
|
9503
|
+
description: "Print (or write with --out) an example moda_registry.v1 manifest",
|
|
9504
|
+
usage: "moda registry template [--out=<path>] [--yes]",
|
|
9505
|
+
flags: [
|
|
9506
|
+
{ name: "--out=<path>", description: "Write the template to a file instead of stdout (refuses to overwrite without --yes)." }
|
|
9507
|
+
],
|
|
9508
|
+
examples: ["moda registry template --out=.moda/registry.json"]
|
|
9509
|
+
},
|
|
9510
|
+
{
|
|
9511
|
+
name: "validate",
|
|
9512
|
+
description: "Validate a manifest locally without contacting Moda",
|
|
9513
|
+
usage: "moda registry validate [--file=<path>]",
|
|
9514
|
+
flags: [{ name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." }],
|
|
9515
|
+
examples: ["moda registry validate --file=.moda/registry.json"]
|
|
9516
|
+
},
|
|
9517
|
+
{
|
|
9518
|
+
name: "push",
|
|
9519
|
+
description: "Upload the manifest's prompts, skills, and tools to the API key's tenant",
|
|
9520
|
+
usage: "moda registry push [--file=<path>] [--dry-run] [--live]",
|
|
9521
|
+
flags: [
|
|
9522
|
+
{ name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." },
|
|
9523
|
+
{ name: "--dry-run", description: "Server-side preview; nothing is written. Without an API key, validates locally only." },
|
|
9524
|
+
{ name: "--live", description: "Mark every pushed skill live (included in the served skill surface)." }
|
|
9525
|
+
],
|
|
9526
|
+
examples: ["moda registry push --dry-run", "moda registry push --live"]
|
|
9527
|
+
}
|
|
9528
|
+
],
|
|
9529
|
+
auth: "api-key",
|
|
9530
|
+
authNote: "template and validate are local; push needs the API key (push --dry-run without a key validates locally only).",
|
|
9531
|
+
outputModes: ["human", "json", "agent"],
|
|
9532
|
+
mutability: "write",
|
|
9533
|
+
validateConfigBeforeRun: false,
|
|
9534
|
+
resetApiRequestCountBeforeRun: false,
|
|
9535
|
+
telemetry: "none",
|
|
9536
|
+
handler: runRegistryCommand
|
|
9537
|
+
},
|
|
7368
9538
|
legacyCommand({
|
|
7369
9539
|
name: "overview",
|
|
7370
9540
|
description: "Harness health briefing from production data",
|
|
@@ -7373,25 +9543,32 @@ var commandRegistry = createCommandRegistry([
|
|
|
7373
9543
|
}),
|
|
7374
9544
|
legacyCommand({
|
|
7375
9545
|
name: "investigate",
|
|
7376
|
-
description: "Rank production issues with evidence and next commands",
|
|
9546
|
+
description: "Rank production issues (tool failures, frustrations, top Problems) with evidence and next commands. --trace scopes only the frustration evidence.",
|
|
7377
9547
|
examples: ["moda investigate", "moda investigate --tool lookupCustomer"],
|
|
7378
9548
|
...dataApiDefaults
|
|
7379
9549
|
}),
|
|
7380
9550
|
legacyCommand({
|
|
7381
9551
|
name: "failures",
|
|
7382
|
-
description: "
|
|
9552
|
+
description: "Tool failures only, ranked with evidence (no frustrations or Problems; use investigate or problems for those)",
|
|
7383
9553
|
examples: ["moda failures", "moda failures --tool lookupCustomer"],
|
|
7384
9554
|
...dataApiDefaults
|
|
7385
9555
|
}),
|
|
7386
9556
|
legacyCommand({
|
|
7387
9557
|
name: "ask",
|
|
7388
|
-
description: "Ask Moda a production intelligence question",
|
|
9558
|
+
description: "Ask Moda a production intelligence question (one paid agent run per question)",
|
|
7389
9559
|
examples: [
|
|
7390
9560
|
'moda ask "what should I fix first?"',
|
|
7391
9561
|
'moda ask "what should I fix first?" --no-stream',
|
|
9562
|
+
'moda ask "what changed this week?" --days-back=7',
|
|
7392
9563
|
'moda --agent ask "why did this run fail?"'
|
|
7393
9564
|
],
|
|
7394
|
-
|
|
9565
|
+
helpNotes: [
|
|
9566
|
+
"Cost: each question starts one agent run on Moda (LLM calls, up to about 200k tokens). Re-asking the same question pays again.",
|
|
9567
|
+
"Streaming: in a terminal, ask streams live progress. If the stream fails before the server accepts the run, ask retries once on the buffered endpoint. If it drops after the server accepted the run, ask does NOT re-run the question (that would pay for a second run): it fails, and the run may still finish server-side. Retry later with --no-stream.",
|
|
9568
|
+
"Flags: --days-back=N (window for the evidence), --no-stream (buffered request, no live progress; --json and --agent never stream)."
|
|
9569
|
+
],
|
|
9570
|
+
...dataApiDefaults,
|
|
9571
|
+
mutability: "write"
|
|
7395
9572
|
}),
|
|
7396
9573
|
legacyCommand({
|
|
7397
9574
|
name: "clusters",
|
|
@@ -7411,7 +9588,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7411
9588
|
name: "traces",
|
|
7412
9589
|
aliases: ["conversations"],
|
|
7413
9590
|
telemetryCommand: "conversations",
|
|
7414
|
-
description: "Search and filter traces (agent runs)",
|
|
9591
|
+
description: "Search and filter traces (agent runs); --search is ranked (hybrid when embeddings exist, else keyword), see total_kind",
|
|
7415
9592
|
examples: [
|
|
7416
9593
|
'moda traces --search="error" --limit=5',
|
|
7417
9594
|
'moda traces --world-state="enterprise" --outcome=positive',
|
|
@@ -7439,8 +9616,12 @@ var commandRegistry = createCommandRegistry([
|
|
|
7439
9616
|
}),
|
|
7440
9617
|
legacyCommand({
|
|
7441
9618
|
name: "context",
|
|
7442
|
-
description: "Get windowed trace context (messages around one turn)",
|
|
7443
|
-
examples: [
|
|
9619
|
+
description: "Get windowed trace context (messages around one turn), or the whole trace with --all",
|
|
9620
|
+
examples: [
|
|
9621
|
+
"moda context <conversation_id> --window=3",
|
|
9622
|
+
"moda context <conversation_id> --all",
|
|
9623
|
+
"moda context <conversation_id> --all --from=500 --max-messages=500"
|
|
9624
|
+
],
|
|
7444
9625
|
...dataApiDefaults
|
|
7445
9626
|
}),
|
|
7446
9627
|
legacyCommand({
|
|
@@ -7450,13 +9631,14 @@ var commandRegistry = createCommandRegistry([
|
|
|
7450
9631
|
examples: [
|
|
7451
9632
|
"moda audit <conversation_id|trace_id>",
|
|
7452
9633
|
"moda audit <trace_id> --kind=trace --json",
|
|
7453
|
-
"moda audit <conversation_id> --include-raw"
|
|
9634
|
+
"moda audit <conversation_id> --include-raw",
|
|
9635
|
+
"moda audit <conversation_id> --limit=1000 # ingest rows to read (default 200, max 1000); warns when capped"
|
|
7454
9636
|
],
|
|
7455
9637
|
...dataApiDefaults
|
|
7456
9638
|
}),
|
|
7457
9639
|
legacyCommand({
|
|
7458
9640
|
name: "frustrations",
|
|
7459
|
-
description: "
|
|
9641
|
+
description: "User frustration detections: the frustration family of `emotions` (same data as the dashboard), legacy field names",
|
|
7460
9642
|
examples: ["moda frustrations --days-back=14 --include-window"],
|
|
7461
9643
|
...dataApiDefaults
|
|
7462
9644
|
}),
|
|
@@ -7468,14 +9650,19 @@ var commandRegistry = createCommandRegistry([
|
|
|
7468
9650
|
}),
|
|
7469
9651
|
legacyCommand({
|
|
7470
9652
|
name: "tool-failure-detail",
|
|
7471
|
-
description: "Get per-tool failure detail",
|
|
7472
|
-
examples: [
|
|
9653
|
+
description: "Get per-tool failure detail (each example embeds its anchor ±2 messages, long text cut to 300 chars)",
|
|
9654
|
+
examples: [
|
|
9655
|
+
"moda tool-failure-detail search",
|
|
9656
|
+
"moda tool-failure-detail search --compact # no embedded conversation (same as --include-window=false)",
|
|
9657
|
+
"moda tool-failure-detail search --include-window --window=3"
|
|
9658
|
+
],
|
|
7473
9659
|
...dataApiDefaults
|
|
7474
9660
|
}),
|
|
7475
9661
|
legacyCommand({
|
|
7476
9662
|
name: "problems",
|
|
7477
9663
|
description: "Rank cross-signal Problems by root cause (what to fix first)",
|
|
7478
|
-
examples: ["moda problems", "moda problems --days-back=7 --limit=10"],
|
|
9664
|
+
examples: ["moda problems", "moda problems --days-back=7 --limit=10", "moda problems remainder --family=emotion"],
|
|
9665
|
+
subcommands: PROBLEMS_SUBCOMMAND_HELP,
|
|
7479
9666
|
...dataApiDefaults
|
|
7480
9667
|
}),
|
|
7481
9668
|
legacyCommand({
|
|
@@ -7582,9 +9769,16 @@ var commandRegistry = createCommandRegistry([
|
|
|
7582
9769
|
}),
|
|
7583
9770
|
legacyCommand({
|
|
7584
9771
|
name: "signals",
|
|
7585
|
-
description: "List custom signals
|
|
7586
|
-
examples: [
|
|
7587
|
-
|
|
9772
|
+
description: "List and manage custom signals (create, update, activate/pause/delete, examples, refine, proposals, search)",
|
|
9773
|
+
examples: [
|
|
9774
|
+
"moda signals",
|
|
9775
|
+
'moda signals draft --note="agent promises a refund it cannot issue"',
|
|
9776
|
+
'moda signals create --name="Refund promise" --description=… --pass-criteria=… --fail-criteria=… --dry-run',
|
|
9777
|
+
"moda signals activate <signal_id>"
|
|
9778
|
+
],
|
|
9779
|
+
subcommands: SIGNALS_SUBCOMMAND_HELP,
|
|
9780
|
+
...dataApiDefaults,
|
|
9781
|
+
mutability: "write"
|
|
7588
9782
|
}),
|
|
7589
9783
|
legacyCommand({
|
|
7590
9784
|
name: "signal",
|
|
@@ -7613,7 +9807,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7613
9807
|
}),
|
|
7614
9808
|
legacyCommand({
|
|
7615
9809
|
name: "emotions",
|
|
7616
|
-
description: "Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)",
|
|
9810
|
+
description: "Multi-family emotion detections (frustration, disapproval, sadness, confusion, anxiety, trust, positive)",
|
|
7617
9811
|
examples: [
|
|
7618
9812
|
"moda emotions --days-back=7",
|
|
7619
9813
|
"moda emotions --family=confusion --limit=20"
|
|
@@ -7686,7 +9880,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7686
9880
|
},
|
|
7687
9881
|
{
|
|
7688
9882
|
name: "diff",
|
|
7689
|
-
description: "
|
|
9883
|
+
description: "Only the prompts that changed, are new, or were deleted since the last sync (hash-level; status lists all)",
|
|
7690
9884
|
usage: "moda prompts diff",
|
|
7691
9885
|
examples: ["moda prompts diff"]
|
|
7692
9886
|
},
|
|
@@ -7718,11 +9912,15 @@ var commandRegistry = createCommandRegistry([
|
|
|
7718
9912
|
{
|
|
7719
9913
|
name: "ab",
|
|
7720
9914
|
description: "Run a judged prompt A/B replay experiment",
|
|
7721
|
-
usage: "moda prompts ab --baseline=<path> --candidate=<path> --traces=<ids
|
|
9915
|
+
usage: "moda prompts ab --baseline=<path> --candidate=<path> [--traces=<ids>|--set-id=<id>|--auto-generate] [--yes]",
|
|
7722
9916
|
flags: [
|
|
7723
9917
|
{ name: "--baseline=<path>", description: "Prompt file to treat as the control." },
|
|
7724
9918
|
{ name: "--candidate=<path>", description: "Prompt file to treat as the variant." },
|
|
7725
|
-
{ name: "--traces=<ids>", description: "Comma-separated trace ids (conversation_id values) to replay. Legacy alias: --conversations=<ids>." }
|
|
9919
|
+
{ name: "--traces=<ids>", description: "Comma-separated trace ids (conversation_id values) to replay, at most 100. Legacy alias: --conversations=<ids>." },
|
|
9920
|
+
{ name: "--cases=<n>", description: "Cases to auto-generate (default 5, max 100)." },
|
|
9921
|
+
{ name: "--seeds=<n>", description: "Repeats per case per arm (default 3, max 10)." },
|
|
9922
|
+
{ name: "--model=<slug>", description: "Assistant model for both arms; must be in the replay allowlist unless --allow-any-model." },
|
|
9923
|
+
{ name: "--yes", description: "Confirm a run above 200 planned playouts (cases x seeds x 2 arms), or one whose case count is unknown." }
|
|
7726
9924
|
],
|
|
7727
9925
|
examples: [
|
|
7728
9926
|
"moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --traces=conv_1,conv_2"
|
|
@@ -7730,14 +9928,23 @@ var commandRegistry = createCommandRegistry([
|
|
|
7730
9928
|
},
|
|
7731
9929
|
{
|
|
7732
9930
|
name: "propose",
|
|
7733
|
-
description: "Revise a prompt from a run's failures",
|
|
7734
|
-
usage: "moda prompts propose [--gate] [--promote-on-win]",
|
|
9931
|
+
description: "Revise a prompt from a run's failures (one LLM call; --gate adds paid replays)",
|
|
9932
|
+
usage: "moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=N] [--model=<slug>] [--out=<path>] [--gate [--seeds=N] [--yes]] [--promote-on-win]",
|
|
7735
9933
|
flags: [
|
|
7736
|
-
{ name: "--
|
|
9934
|
+
{ name: "--prompt-key=<key>", description: "Required. Registered prompt to revise." },
|
|
9935
|
+
{ name: "--from-run=<run_id>", description: "Required. The `prompts ab` run whose failures drive the revision." },
|
|
9936
|
+
{ name: "--set-id=<replay_set_id>", description: "Required. The replay set that run used; --gate replays all of it. A set that does not exist fails before anything is sent." },
|
|
9937
|
+
{ name: "--max-dossiers=<n>", description: "Failure dossiers to revise from (default 8, max 16)." },
|
|
9938
|
+
{ name: "--model=<slug>", description: "Revision model (replay allowlist unless --allow-any-model)." },
|
|
9939
|
+
{ name: "--out=<path>", description: "Write the candidate prompt to this file." },
|
|
9940
|
+
{ name: "--seeds=<n>", description: "Gate: repeats per case per arm (default 3, max 10)." },
|
|
9941
|
+
{ name: "--gate", description: "Automatically A/B the proposed revision over the full --set-id (planned playouts = cases x seeds x 2; above 200 needs --yes)." },
|
|
9942
|
+
{ name: "--yes", description: "Confirm a gate above 200 planned playouts, or one whose case count is unknown." },
|
|
7737
9943
|
{ name: "--promote-on-win", description: "Promote the candidate if it wins the gate." }
|
|
7738
9944
|
],
|
|
7739
|
-
examples: ["moda prompts propose --gate --promote-on-win"]
|
|
7740
|
-
}
|
|
9945
|
+
examples: ["moda prompts propose --prompt-key=support.triage --from-run=<run_id> --set-id=<set_id> --gate --promote-on-win"]
|
|
9946
|
+
},
|
|
9947
|
+
...PROMPTS_REGISTRY_SUBCOMMAND_HELP
|
|
7741
9948
|
],
|
|
7742
9949
|
auth: "api-key",
|
|
7743
9950
|
outputModes: ["human", "json", "agent"],
|
|
@@ -7757,7 +9964,9 @@ var commandRegistry = createCommandRegistry([
|
|
|
7757
9964
|
"moda skills install --list",
|
|
7758
9965
|
"moda skills install integration-node-openai"
|
|
7759
9966
|
],
|
|
7760
|
-
|
|
9967
|
+
subcommands: SKILLS_SUBCOMMAND_HELP,
|
|
9968
|
+
auth: "api-key",
|
|
9969
|
+
authNote: "install is local; gen, status, pull, sync, list, show, policy, and proposals need the API key.",
|
|
7761
9970
|
outputModes: ["human", "json", "agent"],
|
|
7762
9971
|
mutability: "write",
|
|
7763
9972
|
validateConfigBeforeRun: false,
|
|
@@ -7773,13 +9982,14 @@ var commandRegistry = createCommandRegistry([
|
|
|
7773
9982
|
examples: [
|
|
7774
9983
|
"moda fixes",
|
|
7775
9984
|
"moda fixes --status=VERIFIED",
|
|
7776
|
-
"moda fixes draft-batch --limit=
|
|
9985
|
+
"moda fixes draft-batch --limit=5",
|
|
7777
9986
|
"moda fixes drive",
|
|
7778
9987
|
"moda fixes drive --status=SCOPING"
|
|
7779
9988
|
],
|
|
7780
|
-
auth: "
|
|
9989
|
+
auth: "api-key",
|
|
7781
9990
|
outputModes: ["human", "json", "agent"],
|
|
7782
9991
|
mutability: "write",
|
|
9992
|
+
subcommands: FIXES_SUBCOMMAND_HELP,
|
|
7783
9993
|
validateConfigBeforeRun: false,
|
|
7784
9994
|
resetApiRequestCountBeforeRun: false,
|
|
7785
9995
|
telemetry: "result",
|
|
@@ -7790,6 +10000,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7790
10000
|
{
|
|
7791
10001
|
name: "fix",
|
|
7792
10002
|
description: 'Draft, verify, check out, submit, and dismiss Fixes ("a fix with proof")',
|
|
10003
|
+
subcommands: FIX_SUBCOMMAND_HELP,
|
|
7793
10004
|
examples: [
|
|
7794
10005
|
"moda fix start <problem_id> --wait",
|
|
7795
10006
|
"moda fix <fix_id> --packet",
|
|
@@ -7799,7 +10010,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7799
10010
|
'moda fix mark-applied <fix_id> --note="tool description updated in our agent config"',
|
|
7800
10011
|
'moda fix dismiss <fix_id> --reason="not actionable"'
|
|
7801
10012
|
],
|
|
7802
|
-
auth: "
|
|
10013
|
+
auth: "api-key",
|
|
7803
10014
|
outputModes: ["human", "json", "agent"],
|
|
7804
10015
|
mutability: "write",
|
|
7805
10016
|
validateConfigBeforeRun: false,
|
|
@@ -7878,6 +10089,17 @@ function printAgentCliError(command, error, mode = "agent") {
|
|
|
7878
10089
|
output.writeError(error);
|
|
7879
10090
|
}
|
|
7880
10091
|
async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
10092
|
+
const previousProfileEnv = process.env.MODA_PROFILE;
|
|
10093
|
+
try {
|
|
10094
|
+
return await dispatchParsedCommandInner(parsed, startedAt);
|
|
10095
|
+
} finally {
|
|
10096
|
+
if (previousProfileEnv === undefined)
|
|
10097
|
+
delete process.env.MODA_PROFILE;
|
|
10098
|
+
else
|
|
10099
|
+
process.env.MODA_PROFILE = previousProfileEnv;
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
10102
|
+
async function dispatchParsedCommandInner(parsed, startedAt) {
|
|
7881
10103
|
const { command, flags } = parsed;
|
|
7882
10104
|
const commandFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
|
|
7883
10105
|
const telemetryDisabled = isTelemetryDisabled(flags);
|
|
@@ -7909,19 +10131,50 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
7909
10131
|
printHelp();
|
|
7910
10132
|
return 1;
|
|
7911
10133
|
}
|
|
7912
|
-
const
|
|
7913
|
-
if (!definition) {
|
|
7914
|
-
const error = new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
|
|
10134
|
+
const failEarly = (error) => {
|
|
7915
10135
|
const mode = resolveOutputMode(commandFlags, {
|
|
7916
10136
|
isTTY: process.stdout.isTTY,
|
|
7917
10137
|
env: process.env
|
|
7918
10138
|
});
|
|
7919
|
-
if (mode === "agent" || mode === "agent-stream") {
|
|
10139
|
+
if (mode === "agent" || mode === "agent-stream" || mode === "json") {
|
|
7920
10140
|
printAgentCliError(command, error, mode);
|
|
7921
10141
|
} else {
|
|
7922
10142
|
printCliError(error);
|
|
7923
10143
|
}
|
|
7924
10144
|
return 1;
|
|
10145
|
+
};
|
|
10146
|
+
const definition = commandRegistry.find(command);
|
|
10147
|
+
if (!definition) {
|
|
10148
|
+
return failEarly(new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.'));
|
|
10149
|
+
}
|
|
10150
|
+
if (parsed.shortFlags?.length) {
|
|
10151
|
+
return failEarly(new CliInputError(`Short options are not supported (${parsed.shortFlags.join(", ")}), so nothing was run.`, `Use the long form, e.g. --dry-run, --json, --quiet. See 'moda ${command} --help'.`));
|
|
10152
|
+
}
|
|
10153
|
+
for (const target of [flags, commandFlags]) {
|
|
10154
|
+
const raw = target["dry-run"];
|
|
10155
|
+
if (raw === undefined)
|
|
10156
|
+
continue;
|
|
10157
|
+
const value = raw.trim().toLowerCase();
|
|
10158
|
+
if (DRY_RUN_TRUE.has(value))
|
|
10159
|
+
target["dry-run"] = "true";
|
|
10160
|
+
else if (DRY_RUN_FALSE.has(value))
|
|
10161
|
+
delete target["dry-run"];
|
|
10162
|
+
else {
|
|
10163
|
+
return failEarly(new CliInputError(`--dry-run=${raw} is not a yes/no value, so nothing was run.`, "Pass --dry-run (or --dry-run=true) to preview, or omit it to run for real."));
|
|
10164
|
+
}
|
|
10165
|
+
}
|
|
10166
|
+
if (commandFlags.profile === "true" || commandFlags.profile === "") {
|
|
10167
|
+
return failEarly(new CliInputError("--profile needs a value, e.g. --profile=prod.", "List profiles with 'moda profiles list'."));
|
|
10168
|
+
}
|
|
10169
|
+
const requestedProfile = commandFlags.profile ?? process.env.MODA_PROFILE?.trim();
|
|
10170
|
+
if (requestedProfile && !PROFILE_MANAGING_COMMANDS.has(definition.name)) {
|
|
10171
|
+
const known = Object.keys(loadProfileConfig({ env: process.env }).profiles);
|
|
10172
|
+
if (!known.includes(requestedProfile)) {
|
|
10173
|
+
const source = commandFlags.profile ? `--profile=${requestedProfile}` : `MODA_PROFILE=${requestedProfile}`;
|
|
10174
|
+
return failEarly(new CliInputError(`Moda profile '${requestedProfile}' does not exist (from ${source}), so nothing was run.`, `Known profiles: ${known.join(", ")}. Create it with 'moda profiles create ${requestedProfile}' or fix the name.`));
|
|
10175
|
+
}
|
|
10176
|
+
if (commandFlags.profile)
|
|
10177
|
+
process.env.MODA_PROFILE = commandFlags.profile;
|
|
7925
10178
|
}
|
|
7926
10179
|
if (definition.name === "agent" && commandFlags.json !== "true") {
|
|
7927
10180
|
commandFlags.agent = "true";
|
|
@@ -8063,7 +10316,7 @@ function installSignalHandlers() {
|
|
|
8063
10316
|
async function main() {
|
|
8064
10317
|
process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
|
|
8065
10318
|
}
|
|
8066
|
-
var isMain = process.argv[1] && fileURLToPath(import.meta.url) ===
|
|
10319
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync3(process.argv[1]);
|
|
8067
10320
|
if (isMain) {
|
|
8068
10321
|
installSignalHandlers();
|
|
8069
10322
|
main().catch((error) => {
|
|
@@ -8073,9 +10326,11 @@ if (isMain) {
|
|
|
8073
10326
|
}
|
|
8074
10327
|
export {
|
|
8075
10328
|
telemetryCommandName,
|
|
10329
|
+
stripLoneSurrogates,
|
|
8076
10330
|
runCommand,
|
|
8077
10331
|
resolveTailSignal,
|
|
8078
10332
|
printCliError,
|
|
10333
|
+
parseCommandFlags,
|
|
8079
10334
|
parseArgs,
|
|
8080
10335
|
initFlagsForContext,
|
|
8081
10336
|
flagsToArgs,
|
|
@@ -8084,7 +10339,11 @@ export {
|
|
|
8084
10339
|
describeLinearLink,
|
|
8085
10340
|
commandRegistry,
|
|
8086
10341
|
classifyCliError,
|
|
10342
|
+
capProblemsBlocks,
|
|
8087
10343
|
buildResumeCommand,
|
|
10344
|
+
auditCapStatus,
|
|
8088
10345
|
TAIL_LIMITS,
|
|
10346
|
+
LEGACY_WINDOW_MAX_MESSAGES,
|
|
10347
|
+
FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES,
|
|
8089
10348
|
CliInputError
|
|
8090
10349
|
};
|