@moda-ai/cli 1.37.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-ert1sbbt.js → cli-a7ypptsf.js} +515 -59
- package/dist/{cli-strngwn3.js → cli-dyeywy9d.js} +67 -2
- package/dist/{cli-q79sq80a.js → cli-rp912ypf.js} +163 -21
- package/dist/{cli-07tv75te.js → cli-xsmsjny3.js} +696 -139
- package/dist/{cli-qgf2cz2v.js → cli-yy8fwg1a.js} +2 -2
- package/dist/cli.js +2524 -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-vmmfya6b.js → index-qgc37efj.js} +41 -8
- package/dist/prompt-source-mcp.js +2 -2
- package/dist/{provision-spwn5t41.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)
|
|
@@ -5721,6 +7205,8 @@ Init flags:
|
|
|
5721
7205
|
--no-sdk-integrate Skip SDK integration (alias: --no-sdk-guide)
|
|
5722
7206
|
--verification-id=ID Tag the SDK integration's test trace with this id
|
|
5723
7207
|
(the onb_ id from the Moda onboarding page)
|
|
7208
|
+
--connect-code=CODE Sign in with the one-time mcc_ code from the Moda
|
|
7209
|
+
onboarding page instead of the browser (10 min, single use)
|
|
5724
7210
|
--ci-cd Create GitHub Actions Moda validation workflow
|
|
5725
7211
|
--no-ci-cd Skip CI/CD workflow creation
|
|
5726
7212
|
--autosync Create GitHub Actions workflow to sync prompts + skills to Moda on push to main
|
|
@@ -5829,6 +7315,9 @@ function printCommandHelp(definition) {
|
|
|
5829
7315
|
if (definition.mutability === "write") {
|
|
5830
7316
|
lines.push("This command can mutate state.");
|
|
5831
7317
|
}
|
|
7318
|
+
for (const note of definition.helpNotes ?? []) {
|
|
7319
|
+
lines.push("", note);
|
|
7320
|
+
}
|
|
5832
7321
|
if (definition.subcommands && definition.subcommands.length > 0) {
|
|
5833
7322
|
lines.push("", "Subcommands:");
|
|
5834
7323
|
const width = Math.max(...definition.subcommands.map((entry) => entry.name.length));
|
|
@@ -5884,7 +7373,8 @@ async function attachWindows(rows, windowSize) {
|
|
|
5884
7373
|
}
|
|
5885
7374
|
const conversation = asRecord(row.conversation);
|
|
5886
7375
|
const embedded = conversation ? embeddedWindowSize(conversation) : null;
|
|
5887
|
-
|
|
7376
|
+
const embeddedCenter = asNumber(asRecord(conversation?.context)?.center_index);
|
|
7377
|
+
if (conversation && embedded !== null && embedded >= windowSize && embeddedCenter === msg_index) {
|
|
5888
7378
|
return { ...row, window: conversation };
|
|
5889
7379
|
}
|
|
5890
7380
|
try {
|
|
@@ -5899,15 +7389,18 @@ async function attachWindows(rows, windowSize) {
|
|
|
5899
7389
|
}
|
|
5900
7390
|
}));
|
|
5901
7391
|
}
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
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) };
|
|
5907
7400
|
}
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
output.writeData({
|
|
7401
|
+
if (finalize)
|
|
7402
|
+
data = finalize(data, allWarnings);
|
|
7403
|
+
output.writeData(data, allWarnings.length > 0 ? { warnings: allWarnings } : undefined);
|
|
5911
7404
|
}
|
|
5912
7405
|
function notFoundWarning(entity, id, isMissing) {
|
|
5913
7406
|
if (!isMissing)
|
|
@@ -5916,6 +7409,23 @@ function notFoundWarning(entity, id, isMissing) {
|
|
|
5916
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).`
|
|
5917
7410
|
];
|
|
5918
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
|
+
}
|
|
5919
7429
|
var TAIL_SEEN_MAX = 5000;
|
|
5920
7430
|
var EMOTIONS_TAIL_PAGE_SIZE = 20;
|
|
5921
7431
|
var EMOTIONS_TAIL_MAX_PAGES = 25;
|
|
@@ -6097,7 +7607,7 @@ async function runTailCommand(params, context) {
|
|
|
6097
7607
|
break;
|
|
6098
7608
|
if (params.max_events !== undefined && emitted >= params.max_events)
|
|
6099
7609
|
break;
|
|
6100
|
-
await new Promise((
|
|
7610
|
+
await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
|
|
6101
7611
|
}
|
|
6102
7612
|
}
|
|
6103
7613
|
async function attachWorldState(rows) {
|
|
@@ -6195,7 +7705,435 @@ function linearApiError(error) {
|
|
|
6195
7705
|
}
|
|
6196
7706
|
return error;
|
|
6197
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
|
+
}
|
|
6198
8135
|
async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
|
|
8136
|
+
assertHandParsedFlags(command, flags, positionals);
|
|
6199
8137
|
switch (command) {
|
|
6200
8138
|
case "prompts": {
|
|
6201
8139
|
await runPromptsCommand(context);
|
|
@@ -6203,7 +8141,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6203
8141
|
}
|
|
6204
8142
|
case "overview": {
|
|
6205
8143
|
const args = flagsToArgs(flags);
|
|
6206
|
-
OverviewSchema
|
|
8144
|
+
parseCommandFlags(OverviewSchema, args, command, { extraFlags: OVERVIEW_PRODUCTION_FLAGS });
|
|
6207
8145
|
return (await runProductionCommand(context)).exitCode;
|
|
6208
8146
|
}
|
|
6209
8147
|
case "investigate":
|
|
@@ -6213,7 +8151,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6213
8151
|
}
|
|
6214
8152
|
case "clusters": {
|
|
6215
8153
|
const args = flagsToArgs(flags);
|
|
6216
|
-
const params = ClustersSchema
|
|
8154
|
+
const params = parseCommandFlags(ClustersSchema, args, command);
|
|
6217
8155
|
if (params.search !== undefined || params.node_id !== undefined) {
|
|
6218
8156
|
const query2 = new URLSearchParams;
|
|
6219
8157
|
if (params.search !== undefined)
|
|
@@ -6247,7 +8185,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6247
8185
|
throw new CliInputError("<node_id> is required", "Usage: moda cluster-traces <node_id> [--limit=N] [--offset=N]");
|
|
6248
8186
|
}
|
|
6249
8187
|
const args = flagsToArgs(flags, "node_id", positional);
|
|
6250
|
-
const params = ClusterConversationsSchema
|
|
8188
|
+
const params = parseCommandFlags(ClusterConversationsSchema, args, command, { positionalKey: "node_id" });
|
|
6251
8189
|
const query = new URLSearchParams;
|
|
6252
8190
|
if (params.limit)
|
|
6253
8191
|
query.set("limit", params.limit.toString());
|
|
@@ -6262,8 +8200,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6262
8200
|
}
|
|
6263
8201
|
case "traces":
|
|
6264
8202
|
case "conversations": {
|
|
6265
|
-
const args = flagsToArgs(flags);
|
|
6266
|
-
const params = ConversationsSchema
|
|
8203
|
+
const args = withStringIdFlags(flagsToArgs(flags), flags);
|
|
8204
|
+
const params = parseCommandFlags(ConversationsSchema, args, command);
|
|
6267
8205
|
const query = new URLSearchParams;
|
|
6268
8206
|
if (params.search)
|
|
6269
8207
|
query.set("search", params.search);
|
|
@@ -6279,20 +8217,40 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6279
8217
|
query.set("world_state", params.world_state);
|
|
6280
8218
|
if (params.outcome && params.outcome !== "any")
|
|
6281
8219
|
query.set("outcome", params.outcome);
|
|
8220
|
+
if (params.sort)
|
|
8221
|
+
query.set("sort_by", params.sort);
|
|
6282
8222
|
if (params.limit)
|
|
6283
8223
|
query.set("limit", params.limit.toString());
|
|
6284
8224
|
if (params.offset !== undefined)
|
|
6285
8225
|
query.set("offset", params.offset.toString());
|
|
6286
8226
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6287
|
-
const
|
|
6288
|
-
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;
|
|
6289
8247
|
if (params.include_world_state === true) {
|
|
6290
8248
|
const enriched = asRecord(data) ?? {};
|
|
6291
8249
|
const list = Array.isArray(enriched.conversations) ? enriched.conversations : [];
|
|
6292
8250
|
const withState = await attachWorldState(list);
|
|
6293
|
-
context.output.writeData({ ...enriched, conversations: withState });
|
|
8251
|
+
context.output.writeData({ ...enriched, conversations: withState }, writeOptions);
|
|
6294
8252
|
} else {
|
|
6295
|
-
context.output.writeData(data);
|
|
8253
|
+
context.output.writeData(data, writeOptions);
|
|
6296
8254
|
}
|
|
6297
8255
|
break;
|
|
6298
8256
|
}
|
|
@@ -6300,8 +8258,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6300
8258
|
if (!positional || positional.trim() === "") {
|
|
6301
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]');
|
|
6302
8260
|
}
|
|
6303
|
-
const args = flagsToArgs(flags, "q", positional);
|
|
6304
|
-
const params = SearchSchema
|
|
8261
|
+
const args = withStringIdFlags(flagsToArgs(flags, "q", positional), flags);
|
|
8262
|
+
const params = parseCommandFlags(SearchSchema, args, command, { positionalKey: "q" });
|
|
6305
8263
|
const query = new URLSearchParams;
|
|
6306
8264
|
query.set("q", params.q);
|
|
6307
8265
|
if (params.mode)
|
|
@@ -6325,6 +8283,30 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6325
8283
|
} else if (params.mode && actualMode && actualMode !== params.mode && params.mode !== "hybrid") {
|
|
6326
8284
|
warnings.push(`Search ran in ${actualMode} mode instead of the requested ${params.mode} mode.`);
|
|
6327
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
|
+
}
|
|
6328
8310
|
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
6329
8311
|
break;
|
|
6330
8312
|
}
|
|
@@ -6333,7 +8315,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6333
8315
|
throw new CliInputError("<conversation_id> is required", "Usage: moda world-state <conversation_id> [--summary-only] [--event-limit=N]");
|
|
6334
8316
|
}
|
|
6335
8317
|
const args = flagsToArgs(flags, "conversation_id", positional);
|
|
6336
|
-
const params = WorldStateSchema
|
|
8318
|
+
const params = parseCommandFlags(WorldStateSchema, args, command, { positionalKey: "conversation_id" });
|
|
6337
8319
|
if (params.snapshot === true && params.replay === true) {
|
|
6338
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]]");
|
|
6339
8321
|
}
|
|
@@ -6382,7 +8364,26 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6382
8364
|
throw new CliInputError("<conversation_id> is required", "Usage: moda context <conversation_id> [--msg-index=N] [--window=N]");
|
|
6383
8365
|
}
|
|
6384
8366
|
const args = flagsToArgs(flags, "conversation_id", positional);
|
|
6385
|
-
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
|
+
}
|
|
6386
8387
|
const query = new URLSearchParams;
|
|
6387
8388
|
if (params.msg_index !== undefined)
|
|
6388
8389
|
query.set("msg_index", params.msg_index.toString());
|
|
@@ -6401,7 +8402,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6401
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]");
|
|
6402
8403
|
}
|
|
6403
8404
|
const args = flagsToArgs(flags, "id", positional);
|
|
6404
|
-
const params = AuditSchema
|
|
8405
|
+
const params = parseCommandFlags(AuditSchema, args, command, { positionalKey: "id" });
|
|
6405
8406
|
const query = new URLSearchParams;
|
|
6406
8407
|
if (params.kind && params.kind !== "auto")
|
|
6407
8408
|
query.set("kind", params.kind);
|
|
@@ -6411,12 +8412,37 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6411
8412
|
query.set("limit", params.limit.toString());
|
|
6412
8413
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6413
8414
|
const data = await callDataAPI(`/audit/${encodeURIComponent(params.id)}${queryString}`);
|
|
6414
|
-
|
|
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 } : {} });
|
|
6415
8441
|
break;
|
|
6416
8442
|
}
|
|
6417
8443
|
case "frustrations": {
|
|
6418
|
-
const args = flagsToArgs(flags);
|
|
6419
|
-
const params = FrustrationsSchema
|
|
8444
|
+
const args = withStringIdFlags(flagsToArgs(flags), flags);
|
|
8445
|
+
const params = parseCommandFlags(FrustrationsSchema, args, command);
|
|
6420
8446
|
const query = new URLSearchParams;
|
|
6421
8447
|
if (params.days_back)
|
|
6422
8448
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6424,14 +8450,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6424
8450
|
query.set("limit", params.limit.toString());
|
|
6425
8451
|
if (params.offset !== undefined)
|
|
6426
8452
|
query.set("offset", params.offset.toString());
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
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"));
|
|
6430
8460
|
break;
|
|
6431
8461
|
}
|
|
6432
8462
|
case "tool-failures": {
|
|
6433
8463
|
const args = flagsToArgs(flags);
|
|
6434
|
-
const params = ToolFailuresSchema
|
|
8464
|
+
const params = parseCommandFlags(ToolFailuresSchema, args, command);
|
|
6435
8465
|
const queryString = params.days_back ? `?days_back=${params.days_back}` : "";
|
|
6436
8466
|
const data = await callDataAPI(`/tool-failures${queryString}`);
|
|
6437
8467
|
context.output.writeData(data);
|
|
@@ -6439,17 +8469,27 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6439
8469
|
}
|
|
6440
8470
|
case "tool-failure-detail": {
|
|
6441
8471
|
if (!positional) {
|
|
6442
|
-
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.");
|
|
6443
8486
|
}
|
|
6444
|
-
const args = flagsToArgs(flags, "tool_name", positional);
|
|
6445
|
-
const params = ToolFailureDetailSchema.parse(args);
|
|
6446
8487
|
const query = new URLSearchParams;
|
|
6447
8488
|
if (params.subtype)
|
|
6448
8489
|
query.set("subtype", params.subtype);
|
|
6449
8490
|
if (params.days_back)
|
|
6450
8491
|
query.set("days_back", params.days_back.toString());
|
|
6451
|
-
|
|
6452
|
-
query.set("limit", params.limit.toString());
|
|
8492
|
+
query.set("limit", (params.limit ?? TOOL_FAILURE_DETAIL_DEFAULT_LIMIT).toString());
|
|
6453
8493
|
if (params.offset !== undefined)
|
|
6454
8494
|
query.set("offset", params.offset.toString());
|
|
6455
8495
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
@@ -6460,7 +8500,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6460
8500
|
const warnings = subtypeCount === 0 && exampleCount === 0 ? [
|
|
6461
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.`
|
|
6462
8502
|
] : [];
|
|
6463
|
-
|
|
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
|
+
});
|
|
6464
8525
|
break;
|
|
6465
8526
|
}
|
|
6466
8527
|
case "problem-impact": {
|
|
@@ -6489,12 +8550,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6489
8550
|
link.set("useCase", params.node_id);
|
|
6490
8551
|
const problem = asString(data.problemId) || params.problem_id;
|
|
6491
8552
|
const path = problem ? `/dashboard/problems/${encodeURIComponent(problem)}` : params.node_id ? `/dashboard/use-cases/${encodeURIComponent(params.node_id)}` : "/dashboard/use-cases";
|
|
6492
|
-
|
|
8553
|
+
const impactWarnings = serverWarnings(data);
|
|
8554
|
+
context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` }, impactWarnings.length > 0 ? { warnings: impactWarnings } : undefined);
|
|
6493
8555
|
break;
|
|
6494
8556
|
}
|
|
6495
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
|
+
}
|
|
6496
8565
|
const args = flagsToArgs(flags);
|
|
6497
|
-
const params = ProblemsSchema
|
|
8566
|
+
const params = parseCommandFlags(ProblemsSchema, args, command);
|
|
6498
8567
|
const query = new URLSearchParams;
|
|
6499
8568
|
if (params.days_back)
|
|
6500
8569
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6502,19 +8571,26 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6502
8571
|
query.set("limit", params.limit.toString());
|
|
6503
8572
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6504
8573
|
const data = await callDataAPI(`/problems${queryString}`);
|
|
8574
|
+
const { payload, truncated } = capProblemsBlocks(asRecord(data) ?? {}, params.limit ?? 25);
|
|
6505
8575
|
const withLink = {
|
|
6506
|
-
...
|
|
8576
|
+
...payload,
|
|
8577
|
+
...truncated ? { truncated } : {},
|
|
6507
8578
|
dashboard_url: `${MODA_BASE_URL}/dashboard/problems`
|
|
6508
8579
|
};
|
|
6509
|
-
|
|
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);
|
|
6510
8582
|
break;
|
|
6511
8583
|
}
|
|
6512
8584
|
case "problem": {
|
|
8585
|
+
if (positional === "remainder") {
|
|
8586
|
+
await runProblemsRemainder(flags, context);
|
|
8587
|
+
break;
|
|
8588
|
+
}
|
|
6513
8589
|
if (!positional) {
|
|
6514
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]");
|
|
6515
8591
|
}
|
|
6516
8592
|
const args = flagsToArgs(flags, "id", positional);
|
|
6517
|
-
const params = ProblemSchema
|
|
8593
|
+
const params = parseCommandFlags(ProblemSchema, args, command, { positionalKey: "id" });
|
|
6518
8594
|
const traceView = params.traces === true || params.conversations === true;
|
|
6519
8595
|
const views = ["evidence", "reports", "conversations", "feedback"].filter((view2) => view2 === "conversations" ? traceView : params[view2] === true);
|
|
6520
8596
|
if (views.length > 1) {
|
|
@@ -6662,6 +8738,9 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6662
8738
|
break;
|
|
6663
8739
|
}
|
|
6664
8740
|
case "signals": {
|
|
8741
|
+
if (positional && SIGNALS_MANAGEMENT_SUBCOMMANDS.has(positional)) {
|
|
8742
|
+
return (await runSignalsManagementCommand(positional, context)).exitCode;
|
|
8743
|
+
}
|
|
6665
8744
|
const data = await callDataAPI("/custom-signals");
|
|
6666
8745
|
context.output.writeData(data);
|
|
6667
8746
|
break;
|
|
@@ -6672,7 +8751,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6672
8751
|
}
|
|
6673
8752
|
const args = flagsToArgs(flags, "id", positional);
|
|
6674
8753
|
args.id = positional;
|
|
6675
|
-
const params = SignalSchema
|
|
8754
|
+
const params = parseCommandFlags(SignalSchema, args, command, { positionalKey: "id" });
|
|
6676
8755
|
const id = encodeURIComponent(params.id);
|
|
6677
8756
|
if (params.detections !== true) {
|
|
6678
8757
|
const data2 = await callDataAPI(`/custom-signals/${id}`);
|
|
@@ -6861,8 +8940,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6861
8940
|
break;
|
|
6862
8941
|
}
|
|
6863
8942
|
case "emotions": {
|
|
6864
|
-
const args = flagsToArgs(flags);
|
|
6865
|
-
const params = EmotionsSchema
|
|
8943
|
+
const args = withStringIdFlags(flagsToArgs(flags), flags);
|
|
8944
|
+
const params = parseCommandFlags(EmotionsSchema, args, command);
|
|
6866
8945
|
const query = new URLSearchParams;
|
|
6867
8946
|
if (params.days_back)
|
|
6868
8947
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6872,9 +8951,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6872
8951
|
query.set("offset", params.offset.toString());
|
|
6873
8952
|
if (params.family)
|
|
6874
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);
|
|
6875
8958
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
6876
8959
|
const data = await callDataAPI(`/emotions${queryString}`);
|
|
6877
|
-
|
|
8960
|
+
const processingWarnings = processingDisabledWarning(data, "emotion");
|
|
8961
|
+
context.output.writeData(data, processingWarnings.length > 0 ? { warnings: processingWarnings } : undefined);
|
|
6878
8962
|
break;
|
|
6879
8963
|
}
|
|
6880
8964
|
case "laziness": {
|
|
@@ -6882,7 +8966,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6882
8966
|
if (flags["conversation-id"] !== undefined) {
|
|
6883
8967
|
args.conversation_id = flags["conversation-id"];
|
|
6884
8968
|
}
|
|
6885
|
-
const params = LazinessSchema
|
|
8969
|
+
const params = parseCommandFlags(LazinessSchema, args, command);
|
|
6886
8970
|
const query = new URLSearchParams;
|
|
6887
8971
|
if (params.days_back)
|
|
6888
8972
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6904,7 +8988,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6904
8988
|
if (flags["conversation-id"] !== undefined) {
|
|
6905
8989
|
args.conversation_id = flags["conversation-id"];
|
|
6906
8990
|
}
|
|
6907
|
-
const params = HallucinationsSchema
|
|
8991
|
+
const params = parseCommandFlags(HallucinationsSchema, args, command);
|
|
6908
8992
|
const query = new URLSearchParams;
|
|
6909
8993
|
if (params.days_back)
|
|
6910
8994
|
query.set("days_back", params.days_back.toString());
|
|
@@ -6938,7 +9022,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6938
9022
|
}
|
|
6939
9023
|
case "tail": {
|
|
6940
9024
|
const args = flagsToArgs(flags);
|
|
6941
|
-
const params = TailSchema
|
|
9025
|
+
const params = parseCommandFlags(TailSchema, args, command);
|
|
6942
9026
|
await runTailCommand(params, context);
|
|
6943
9027
|
break;
|
|
6944
9028
|
}
|
|
@@ -7084,7 +9168,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7084
9168
|
resetApiRequestCountBeforeRun: true,
|
|
7085
9169
|
telemetry: "result",
|
|
7086
9170
|
handler: async (context) => {
|
|
7087
|
-
const { runInit } = await import("./index-
|
|
9171
|
+
const { runInit } = await import("./index-qgc37efj.js");
|
|
7088
9172
|
if (context.outputMode === "agent-stream") {
|
|
7089
9173
|
context.output.writeEvent({
|
|
7090
9174
|
event: "started",
|
|
@@ -7125,7 +9209,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7125
9209
|
resetApiRequestCountBeforeRun: true,
|
|
7126
9210
|
telemetry: "result-and-error",
|
|
7127
9211
|
handler: async (context) => {
|
|
7128
|
-
const { runProvision } = await import("./provision-
|
|
9212
|
+
const { runProvision } = await import("./provision-gabtks29.js");
|
|
7129
9213
|
return runProvision(context, {
|
|
7130
9214
|
resumeCommand: buildResumeCommand("provision", context.rawFlags)
|
|
7131
9215
|
});
|
|
@@ -7267,6 +9351,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7267
9351
|
description: "Diagnose local Moda setup and data flow",
|
|
7268
9352
|
examples: ["moda doctor", "moda doctor --json", "moda doctor --offline"],
|
|
7269
9353
|
auth: "none",
|
|
9354
|
+
authNote: "Offline by default; --online checks the API key and data flow.",
|
|
7270
9355
|
outputModes: ["human", "json", "agent"],
|
|
7271
9356
|
mutability: "read",
|
|
7272
9357
|
validateConfigBeforeRun: false,
|
|
@@ -7300,7 +9385,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7300
9385
|
},
|
|
7301
9386
|
{
|
|
7302
9387
|
name: "keys",
|
|
7303
|
-
description: "
|
|
9388
|
+
description: "Local key descriptors in ~/.moda only (not your server-side API keys; manage those in the dashboard)",
|
|
7304
9389
|
examples: ["moda keys list", "moda keys create --name=local-dev"],
|
|
7305
9390
|
auth: "none",
|
|
7306
9391
|
outputModes: ["human", "json", "agent"],
|
|
@@ -7322,7 +9407,9 @@ var commandRegistry = createCommandRegistry([
|
|
|
7322
9407
|
"moda harness delete my-harness-key --yes",
|
|
7323
9408
|
"moda harness explain --agent=agent_support"
|
|
7324
9409
|
],
|
|
7325
|
-
|
|
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.",
|
|
7326
9413
|
outputModes: ["human", "json", "agent"],
|
|
7327
9414
|
mutability: "write",
|
|
7328
9415
|
validateConfigBeforeRun: false,
|
|
@@ -7332,10 +9419,9 @@ var commandRegistry = createCommandRegistry([
|
|
|
7332
9419
|
},
|
|
7333
9420
|
{
|
|
7334
9421
|
name: "agent",
|
|
7335
|
-
description: "Agent-native Moda context,
|
|
9422
|
+
description: "Agent-native local Moda context, next actions, and evidence (production investigation: moda investigate)",
|
|
7336
9423
|
examples: [
|
|
7337
9424
|
"moda agent context",
|
|
7338
|
-
"moda agent investigate",
|
|
7339
9425
|
"moda agent next-action",
|
|
7340
9426
|
"moda agent evidence doctor:api_key_resolves"
|
|
7341
9427
|
],
|
|
@@ -7355,7 +9441,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7355
9441
|
"moda sync harness --from-report .moda/harness-report.json",
|
|
7356
9442
|
"moda sync prompts"
|
|
7357
9443
|
],
|
|
7358
|
-
auth: "
|
|
9444
|
+
auth: "api-key",
|
|
7359
9445
|
outputModes: ["human", "json", "agent"],
|
|
7360
9446
|
mutability: "write",
|
|
7361
9447
|
validateConfigBeforeRun: false,
|
|
@@ -7363,6 +9449,92 @@ var commandRegistry = createCommandRegistry([
|
|
|
7363
9449
|
telemetry: "none",
|
|
7364
9450
|
handler: runSyncCommand
|
|
7365
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
|
+
},
|
|
7366
9538
|
legacyCommand({
|
|
7367
9539
|
name: "overview",
|
|
7368
9540
|
description: "Harness health briefing from production data",
|
|
@@ -7371,25 +9543,32 @@ var commandRegistry = createCommandRegistry([
|
|
|
7371
9543
|
}),
|
|
7372
9544
|
legacyCommand({
|
|
7373
9545
|
name: "investigate",
|
|
7374
|
-
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.",
|
|
7375
9547
|
examples: ["moda investigate", "moda investigate --tool lookupCustomer"],
|
|
7376
9548
|
...dataApiDefaults
|
|
7377
9549
|
}),
|
|
7378
9550
|
legacyCommand({
|
|
7379
9551
|
name: "failures",
|
|
7380
|
-
description: "
|
|
9552
|
+
description: "Tool failures only, ranked with evidence (no frustrations or Problems; use investigate or problems for those)",
|
|
7381
9553
|
examples: ["moda failures", "moda failures --tool lookupCustomer"],
|
|
7382
9554
|
...dataApiDefaults
|
|
7383
9555
|
}),
|
|
7384
9556
|
legacyCommand({
|
|
7385
9557
|
name: "ask",
|
|
7386
|
-
description: "Ask Moda a production intelligence question",
|
|
9558
|
+
description: "Ask Moda a production intelligence question (one paid agent run per question)",
|
|
7387
9559
|
examples: [
|
|
7388
9560
|
'moda ask "what should I fix first?"',
|
|
7389
9561
|
'moda ask "what should I fix first?" --no-stream',
|
|
9562
|
+
'moda ask "what changed this week?" --days-back=7',
|
|
7390
9563
|
'moda --agent ask "why did this run fail?"'
|
|
7391
9564
|
],
|
|
7392
|
-
|
|
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"
|
|
7393
9572
|
}),
|
|
7394
9573
|
legacyCommand({
|
|
7395
9574
|
name: "clusters",
|
|
@@ -7409,7 +9588,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7409
9588
|
name: "traces",
|
|
7410
9589
|
aliases: ["conversations"],
|
|
7411
9590
|
telemetryCommand: "conversations",
|
|
7412
|
-
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",
|
|
7413
9592
|
examples: [
|
|
7414
9593
|
'moda traces --search="error" --limit=5',
|
|
7415
9594
|
'moda traces --world-state="enterprise" --outcome=positive',
|
|
@@ -7437,8 +9616,12 @@ var commandRegistry = createCommandRegistry([
|
|
|
7437
9616
|
}),
|
|
7438
9617
|
legacyCommand({
|
|
7439
9618
|
name: "context",
|
|
7440
|
-
description: "Get windowed trace context (messages around one turn)",
|
|
7441
|
-
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
|
+
],
|
|
7442
9625
|
...dataApiDefaults
|
|
7443
9626
|
}),
|
|
7444
9627
|
legacyCommand({
|
|
@@ -7448,13 +9631,14 @@ var commandRegistry = createCommandRegistry([
|
|
|
7448
9631
|
examples: [
|
|
7449
9632
|
"moda audit <conversation_id|trace_id>",
|
|
7450
9633
|
"moda audit <trace_id> --kind=trace --json",
|
|
7451
|
-
"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"
|
|
7452
9636
|
],
|
|
7453
9637
|
...dataApiDefaults
|
|
7454
9638
|
}),
|
|
7455
9639
|
legacyCommand({
|
|
7456
9640
|
name: "frustrations",
|
|
7457
|
-
description: "
|
|
9641
|
+
description: "User frustration detections: the frustration family of `emotions` (same data as the dashboard), legacy field names",
|
|
7458
9642
|
examples: ["moda frustrations --days-back=14 --include-window"],
|
|
7459
9643
|
...dataApiDefaults
|
|
7460
9644
|
}),
|
|
@@ -7466,14 +9650,19 @@ var commandRegistry = createCommandRegistry([
|
|
|
7466
9650
|
}),
|
|
7467
9651
|
legacyCommand({
|
|
7468
9652
|
name: "tool-failure-detail",
|
|
7469
|
-
description: "Get per-tool failure detail",
|
|
7470
|
-
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
|
+
],
|
|
7471
9659
|
...dataApiDefaults
|
|
7472
9660
|
}),
|
|
7473
9661
|
legacyCommand({
|
|
7474
9662
|
name: "problems",
|
|
7475
9663
|
description: "Rank cross-signal Problems by root cause (what to fix first)",
|
|
7476
|
-
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,
|
|
7477
9666
|
...dataApiDefaults
|
|
7478
9667
|
}),
|
|
7479
9668
|
legacyCommand({
|
|
@@ -7580,9 +9769,16 @@ var commandRegistry = createCommandRegistry([
|
|
|
7580
9769
|
}),
|
|
7581
9770
|
legacyCommand({
|
|
7582
9771
|
name: "signals",
|
|
7583
|
-
description: "List custom signals
|
|
7584
|
-
examples: [
|
|
7585
|
-
|
|
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"
|
|
7586
9782
|
}),
|
|
7587
9783
|
legacyCommand({
|
|
7588
9784
|
name: "signal",
|
|
@@ -7611,7 +9807,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7611
9807
|
}),
|
|
7612
9808
|
legacyCommand({
|
|
7613
9809
|
name: "emotions",
|
|
7614
|
-
description: "Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)",
|
|
9810
|
+
description: "Multi-family emotion detections (frustration, disapproval, sadness, confusion, anxiety, trust, positive)",
|
|
7615
9811
|
examples: [
|
|
7616
9812
|
"moda emotions --days-back=7",
|
|
7617
9813
|
"moda emotions --family=confusion --limit=20"
|
|
@@ -7684,7 +9880,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7684
9880
|
},
|
|
7685
9881
|
{
|
|
7686
9882
|
name: "diff",
|
|
7687
|
-
description: "
|
|
9883
|
+
description: "Only the prompts that changed, are new, or were deleted since the last sync (hash-level; status lists all)",
|
|
7688
9884
|
usage: "moda prompts diff",
|
|
7689
9885
|
examples: ["moda prompts diff"]
|
|
7690
9886
|
},
|
|
@@ -7716,11 +9912,15 @@ var commandRegistry = createCommandRegistry([
|
|
|
7716
9912
|
{
|
|
7717
9913
|
name: "ab",
|
|
7718
9914
|
description: "Run a judged prompt A/B replay experiment",
|
|
7719
|
-
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]",
|
|
7720
9916
|
flags: [
|
|
7721
9917
|
{ name: "--baseline=<path>", description: "Prompt file to treat as the control." },
|
|
7722
9918
|
{ name: "--candidate=<path>", description: "Prompt file to treat as the variant." },
|
|
7723
|
-
{ 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." }
|
|
7724
9924
|
],
|
|
7725
9925
|
examples: [
|
|
7726
9926
|
"moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --traces=conv_1,conv_2"
|
|
@@ -7728,14 +9928,23 @@ var commandRegistry = createCommandRegistry([
|
|
|
7728
9928
|
},
|
|
7729
9929
|
{
|
|
7730
9930
|
name: "propose",
|
|
7731
|
-
description: "Revise a prompt from a run's failures",
|
|
7732
|
-
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]",
|
|
7733
9933
|
flags: [
|
|
7734
|
-
{ 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." },
|
|
7735
9943
|
{ name: "--promote-on-win", description: "Promote the candidate if it wins the gate." }
|
|
7736
9944
|
],
|
|
7737
|
-
examples: ["moda prompts propose --gate --promote-on-win"]
|
|
7738
|
-
}
|
|
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
|
|
7739
9948
|
],
|
|
7740
9949
|
auth: "api-key",
|
|
7741
9950
|
outputModes: ["human", "json", "agent"],
|
|
@@ -7755,7 +9964,9 @@ var commandRegistry = createCommandRegistry([
|
|
|
7755
9964
|
"moda skills install --list",
|
|
7756
9965
|
"moda skills install integration-node-openai"
|
|
7757
9966
|
],
|
|
7758
|
-
|
|
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.",
|
|
7759
9970
|
outputModes: ["human", "json", "agent"],
|
|
7760
9971
|
mutability: "write",
|
|
7761
9972
|
validateConfigBeforeRun: false,
|
|
@@ -7771,13 +9982,14 @@ var commandRegistry = createCommandRegistry([
|
|
|
7771
9982
|
examples: [
|
|
7772
9983
|
"moda fixes",
|
|
7773
9984
|
"moda fixes --status=VERIFIED",
|
|
7774
|
-
"moda fixes draft-batch --limit=
|
|
9985
|
+
"moda fixes draft-batch --limit=5",
|
|
7775
9986
|
"moda fixes drive",
|
|
7776
9987
|
"moda fixes drive --status=SCOPING"
|
|
7777
9988
|
],
|
|
7778
|
-
auth: "
|
|
9989
|
+
auth: "api-key",
|
|
7779
9990
|
outputModes: ["human", "json", "agent"],
|
|
7780
9991
|
mutability: "write",
|
|
9992
|
+
subcommands: FIXES_SUBCOMMAND_HELP,
|
|
7781
9993
|
validateConfigBeforeRun: false,
|
|
7782
9994
|
resetApiRequestCountBeforeRun: false,
|
|
7783
9995
|
telemetry: "result",
|
|
@@ -7788,6 +10000,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7788
10000
|
{
|
|
7789
10001
|
name: "fix",
|
|
7790
10002
|
description: 'Draft, verify, check out, submit, and dismiss Fixes ("a fix with proof")',
|
|
10003
|
+
subcommands: FIX_SUBCOMMAND_HELP,
|
|
7791
10004
|
examples: [
|
|
7792
10005
|
"moda fix start <problem_id> --wait",
|
|
7793
10006
|
"moda fix <fix_id> --packet",
|
|
@@ -7797,7 +10010,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
7797
10010
|
'moda fix mark-applied <fix_id> --note="tool description updated in our agent config"',
|
|
7798
10011
|
'moda fix dismiss <fix_id> --reason="not actionable"'
|
|
7799
10012
|
],
|
|
7800
|
-
auth: "
|
|
10013
|
+
auth: "api-key",
|
|
7801
10014
|
outputModes: ["human", "json", "agent"],
|
|
7802
10015
|
mutability: "write",
|
|
7803
10016
|
validateConfigBeforeRun: false,
|
|
@@ -7876,6 +10089,17 @@ function printAgentCliError(command, error, mode = "agent") {
|
|
|
7876
10089
|
output.writeError(error);
|
|
7877
10090
|
}
|
|
7878
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) {
|
|
7879
10103
|
const { command, flags } = parsed;
|
|
7880
10104
|
const commandFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
|
|
7881
10105
|
const telemetryDisabled = isTelemetryDisabled(flags);
|
|
@@ -7907,19 +10131,50 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
7907
10131
|
printHelp();
|
|
7908
10132
|
return 1;
|
|
7909
10133
|
}
|
|
7910
|
-
const
|
|
7911
|
-
if (!definition) {
|
|
7912
|
-
const error = new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
|
|
10134
|
+
const failEarly = (error) => {
|
|
7913
10135
|
const mode = resolveOutputMode(commandFlags, {
|
|
7914
10136
|
isTTY: process.stdout.isTTY,
|
|
7915
10137
|
env: process.env
|
|
7916
10138
|
});
|
|
7917
|
-
if (mode === "agent" || mode === "agent-stream") {
|
|
10139
|
+
if (mode === "agent" || mode === "agent-stream" || mode === "json") {
|
|
7918
10140
|
printAgentCliError(command, error, mode);
|
|
7919
10141
|
} else {
|
|
7920
10142
|
printCliError(error);
|
|
7921
10143
|
}
|
|
7922
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;
|
|
7923
10178
|
}
|
|
7924
10179
|
if (definition.name === "agent" && commandFlags.json !== "true") {
|
|
7925
10180
|
commandFlags.agent = "true";
|
|
@@ -8061,7 +10316,7 @@ function installSignalHandlers() {
|
|
|
8061
10316
|
async function main() {
|
|
8062
10317
|
process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
|
|
8063
10318
|
}
|
|
8064
|
-
var isMain = process.argv[1] && fileURLToPath(import.meta.url) ===
|
|
10319
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync3(process.argv[1]);
|
|
8065
10320
|
if (isMain) {
|
|
8066
10321
|
installSignalHandlers();
|
|
8067
10322
|
main().catch((error) => {
|
|
@@ -8071,9 +10326,11 @@ if (isMain) {
|
|
|
8071
10326
|
}
|
|
8072
10327
|
export {
|
|
8073
10328
|
telemetryCommandName,
|
|
10329
|
+
stripLoneSurrogates,
|
|
8074
10330
|
runCommand,
|
|
8075
10331
|
resolveTailSignal,
|
|
8076
10332
|
printCliError,
|
|
10333
|
+
parseCommandFlags,
|
|
8077
10334
|
parseArgs,
|
|
8078
10335
|
initFlagsForContext,
|
|
8079
10336
|
flagsToArgs,
|
|
@@ -8082,7 +10339,11 @@ export {
|
|
|
8082
10339
|
describeLinearLink,
|
|
8083
10340
|
commandRegistry,
|
|
8084
10341
|
classifyCliError,
|
|
10342
|
+
capProblemsBlocks,
|
|
8085
10343
|
buildResumeCommand,
|
|
10344
|
+
auditCapStatus,
|
|
8086
10345
|
TAIL_LIMITS,
|
|
10346
|
+
LEGACY_WINDOW_MAX_MESSAGES,
|
|
10347
|
+
FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES,
|
|
8087
10348
|
CliInputError
|
|
8088
10349
|
};
|