@moda-ai/cli 1.31.1 → 1.32.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.
@@ -1,2972 +0,0 @@
1
- import {
2
- HARNESS_REPORT_APPROVAL_PATH,
3
- HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
4
- HARNESS_REPORT_PATH,
5
- callControlAPI,
6
- callDataAPI,
7
- hashHarnessReport,
8
- readHarnessReport,
9
- readHarnessSyncState,
10
- renderDoctorHuman,
11
- renderStatusHuman,
12
- scanHarness,
13
- validateHarnessReport
14
- } from "./cli-p3271yp2.js";
15
- import {
16
- isAuthSessionValid,
17
- loadAuthSession
18
- } from "./cli-9xw5bq1j.js";
19
- import {
20
- CliInputError,
21
- createCommandContext,
22
- ensureSecretFilePermissions,
23
- getActiveProfile,
24
- getModaPaths,
25
- isLoopbackBaseUrl,
26
- loadProfileConfig,
27
- resolveApiKey,
28
- resolveIngestUrl,
29
- resolveModaBaseUrl,
30
- resolveSecretReference,
31
- resolveTenantId,
32
- secretFilePathFromRef,
33
- validateConfig
34
- } from "./cli-59yacef3.js";
35
-
36
- // src/prompts.ts
37
- import { execFileSync } from "node:child_process";
38
- import { createHash } from "node:crypto";
39
- import {
40
- existsSync as existsSync2,
41
- lstatSync,
42
- mkdirSync,
43
- readFileSync,
44
- readdirSync,
45
- writeFileSync as writeFileSync2
46
- } from "node:fs";
47
- import { dirname, isAbsolute as isAbsolute3, join, relative, resolve as resolve3 } from "node:path";
48
-
49
- // src/prompts-ab.ts
50
- import { existsSync } from "node:fs";
51
- import { isAbsolute, resolve } from "node:path";
52
- var DEFAULT_SEEDS_PER_CASE = 3;
53
- var DEFAULT_POLL_INTERVAL_MS = 15000;
54
- var DEFAULT_WAIT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
55
- var TERMINAL_RUN_STATUSES = new Set(["completed", "skipped", "error"]);
56
- async function runPromptAb(flags, profileOptions, context) {
57
- validateConfig();
58
- const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
59
- if (!tenantId) {
60
- throw new Error("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
61
- }
62
- const baselineSource = flags.baseline || flags["baseline-file"] || flags["baseline-key"];
63
- const candidateSource = flags.candidate || flags["candidate-file"] || flags["candidate-key"];
64
- if (!baselineSource || !candidateSource) {
65
- throw new Error("Usage: moda prompts ab --baseline=<file|key> --candidate=<file|key> [--auto-generate|--set-id=ID|--traces=id1,id2] (legacy alias: --conversations=)");
66
- }
67
- if (flags.sync === "true") {
68
- await runPromptSync({ ...flags, watch: "false" }, profileOptions);
69
- }
70
- const promptArms = {
71
- prod: resolvePromptArmSpec(baselineSource, "baseline"),
72
- proposed: resolvePromptArmSpec(candidateSource, "candidate")
73
- };
74
- const replaySetId = await ensureReplaySet(flags, tenantId, profileOptions);
75
- const seedsPerCase = parsePositiveInt(flags.seeds ?? flags["seeds-per-case"], DEFAULT_SEEDS_PER_CASE, 10);
76
- const assistantModel = (flags.model ?? flags["assistant-model"] ?? "").trim();
77
- const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/run`, {
78
- method: "POST",
79
- body: JSON.stringify({
80
- promptArms,
81
- seedsPerCase,
82
- assistantModel: assistantModel || undefined,
83
- promotePrimary: flags["no-promote"] !== "true"
84
- })
85
- }, profileOptions, { timeoutMs: 120000, retries: 1 });
86
- const noWait = flags.wait === "false" || flags["no-wait"] === "true";
87
- if (noWait) {
88
- context.output.writeData({
89
- replaySetId,
90
- runId: enqueue.runId,
91
- status: enqueue.status,
92
- message: enqueue.message ?? "Replay comparison queued"
93
- });
94
- return;
95
- }
96
- const timeoutMs = parsePositiveInt(flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, 24 * 60 * 60 * 1000);
97
- const pollIntervalMs = parsePositiveInt(flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, 120000);
98
- const deadline = Date.now() + timeoutMs;
99
- let latest = null;
100
- while (Date.now() < deadline) {
101
- latest = await fetchLatestRun(tenantId, replaySetId, enqueue.runId, profileOptions);
102
- const status = latest.run?.status ?? "";
103
- const runId = latest.run?.runId ?? "";
104
- if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
105
- break;
106
- }
107
- if (context.outputMode === "human") {
108
- process.stderr.write(`Waiting for replay run (${status || "pending"})...
109
- `);
110
- }
111
- await sleep(pollIntervalMs);
112
- }
113
- if (!latest?.run || latest.run.runId !== enqueue.runId || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
114
- throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
115
- }
116
- const result = {
117
- replaySetId,
118
- runId: latest.run.runId,
119
- status: latest.run.status,
120
- verdict: formatVerdict(latest),
121
- run: { ...latest.run },
122
- cases: latest.cases.map((row) => ({ ...row }))
123
- };
124
- if (context.outputMode === "human") {
125
- printHumanVerdict(result);
126
- }
127
- context.output.writeData(result);
128
- }
129
- function resolvePromptArmSpec(source, label, opts = {}) {
130
- const trimmed = source.trim();
131
- if (!trimmed) {
132
- throw new Error(`Empty prompt source for ${label}`);
133
- }
134
- const fromDiscovery = () => {
135
- const discovered = discoverPrompts();
136
- const match = discovered.find((prompt) => prompt.key === trimmed) ?? discovered.find((prompt) => prompt.sourcePath === trimmed);
137
- return match ? { key: match.key, content: match.content } : null;
138
- };
139
- const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
140
- let key;
141
- let content;
142
- const preferred = opts.preferKey ? fromDiscovery() : null;
143
- if (preferred) {
144
- key = preferred.key;
145
- content = preferred.content;
146
- } else if (opts.preferKey) {
147
- throw new Error(`Could not resolve prompt key '${trimmed}' — no discovered prompt matched this key`);
148
- } else if (existsSync(absPath)) {
149
- const parsed = parsePromptFileForAb(absPath);
150
- key = parsed.key;
151
- content = parsed.content;
152
- } else {
153
- const match = fromDiscovery();
154
- if (!match) {
155
- throw new Error(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
156
- }
157
- key = match.key;
158
- content = match.content;
159
- }
160
- const locked = readPromptLock()?.prompts[key];
161
- return {
162
- content,
163
- prompt_version_id: locked?.versionId ?? "",
164
- prompt_id: locked?.promptId ?? "",
165
- prompt_label: label
166
- };
167
- }
168
- async function ensureReplaySet(flags, tenantId, profileOptions) {
169
- const existingSetId = (flags["set-id"] || flags["replay-set-id"] || "").trim();
170
- if (existingSetId) {
171
- return existingSetId;
172
- }
173
- const conversations = parseCsv(flags.traces ?? flags.conversations ?? flags["conversation-ids"]);
174
- if (conversations.length) {
175
- return createSetFromConversations(conversations, flags, tenantId, profileOptions);
176
- }
177
- if (flags["auto-generate"] === "false" && !existingSetId) {
178
- throw new Error("Provide --set-id=, --traces= (legacy alias: --conversations=), or allow --auto-generate (default)");
179
- }
180
- const caseCount = parsePositiveInt(flags.cases ?? flags["case-count"], 5, 500);
181
- const lookbackDays = parsePositiveInt(flags["lookback-days"], 30, 365);
182
- const name = (flags.name ?? flags["set-name"] ?? `Prompt A/B ${new Date().toISOString().slice(0, 10)}`).trim();
183
- const generated = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/auto-generate`, {
184
- method: "POST",
185
- body: JSON.stringify({ name, caseCount, lookbackDays })
186
- }, profileOptions, { timeoutMs: 300000, retries: 0 });
187
- if (!generated?.id) {
188
- throw new Error("Auto-generate did not return a replay set id");
189
- }
190
- return generated.id;
191
- }
192
- async function createSetFromConversations(conversationIds, flags, tenantId, profileOptions) {
193
- const name = (flags.name ?? flags["set-name"] ?? `Prompt A/B ${conversationIds.length} trace(s)`).trim();
194
- const created = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets`, {
195
- method: "POST",
196
- body: JSON.stringify({ name, description: "Created by moda prompts ab" })
197
- }, profileOptions);
198
- let position = 0;
199
- for (const conversationId of conversationIds) {
200
- const scenario = await loadScenarioFromConversation(conversationId);
201
- await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(created.id)}/cases`, {
202
- method: "POST",
203
- body: JSON.stringify({
204
- title: conversationId,
205
- scenario,
206
- sourceConversationId: conversationId,
207
- successCriteria: [
208
- "The agent understands the user request",
209
- "The agent uses tools appropriately when needed",
210
- "The agent provides a helpful final answer"
211
- ],
212
- position: position++
213
- })
214
- }, profileOptions);
215
- }
216
- return created.id;
217
- }
218
- async function loadScenarioFromConversation(conversationId) {
219
- try {
220
- const data = await callDataAPI(`/conversations/${encodeURIComponent(conversationId)}/context?msg_index=0&window=0`);
221
- const firstUser = (data.messages ?? []).find((message) => message.role === "user" && String(message.content ?? "").trim());
222
- const opening = String(firstUser?.content ?? "").trim();
223
- if (opening) {
224
- return `The user says: "${opening}"
225
-
226
- Replay the conversation faithfully and help them accomplish their goal.`;
227
- }
228
- } catch {}
229
- return `Replay conversation ${conversationId}: help the user accomplish their original goal using the available tools.`;
230
- }
231
- async function fetchLatestRun(tenantId, replaySetId, runId, profileOptions) {
232
- return callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/runs/latest?runId=${encodeURIComponent(runId)}`, { method: "GET" }, profileOptions, { timeoutMs: 60000, retries: 2 });
233
- }
234
- function candidateOutcome(payload) {
235
- const run = payload.run;
236
- if (!run || run.status !== "completed")
237
- return "no-run";
238
- const delta = run.proposedPassCount - run.prodPassCount;
239
- if (delta > 0)
240
- return "candidate";
241
- if (delta < 0)
242
- return "baseline";
243
- return "tie";
244
- }
245
- function formatVerdict(payload) {
246
- const run = payload.run;
247
- if (!run)
248
- return "no run";
249
- if (run.status === "skipped")
250
- return `skipped: ${run.notes || "no reason"}`;
251
- if (run.status === "error")
252
- return `error: ${run.notes || "unknown error"}`;
253
- if (run.status !== "completed")
254
- return `status=${run.status}`;
255
- const delta = run.proposedPassCount - run.prodPassCount;
256
- const winner = delta > 0 ? "candidate wins" : delta < 0 ? "baseline wins" : "tie on case passes";
257
- const rates = `baseline ${run.prodPassCount}/${run.caseCount} (${pct(run.prodPassRate)}), candidate ${run.proposedPassCount}/${run.caseCount} (${pct(run.proposedPassRate)})`;
258
- const notes = run.notes?.trim();
259
- return `${winner}; ${rates}${notes ? `; ${notes}` : ""}`;
260
- }
261
- function printHumanVerdict(result) {
262
- process.stderr.write(`
263
- Prompt A/B replay verdict
264
- `);
265
- process.stderr.write(`-------------------------
266
- `);
267
- process.stderr.write(`Replay set: ${result.replaySetId}
268
- `);
269
- process.stderr.write(`Run id: ${result.runId}
270
- `);
271
- process.stderr.write(`Status: ${result.status}
272
- `);
273
- process.stderr.write(`Verdict: ${result.verdict}
274
- `);
275
- if (result.cases.length) {
276
- process.stderr.write(`
277
- Per-case:
278
- `);
279
- for (const row of result.cases) {
280
- const caseId = String(row.caseId ?? "");
281
- const prod = formatPass(row.prodPass);
282
- const proposed = formatPass(row.proposedPass);
283
- process.stderr.write(` ${caseId}: baseline=${prod} candidate=${proposed}
284
- `);
285
- }
286
- }
287
- process.stderr.write(`
288
- `);
289
- }
290
- function formatPass(value) {
291
- if (value === null || value === undefined)
292
- return "abstained";
293
- return value ? "pass" : "fail";
294
- }
295
- function pct(rate) {
296
- return `${Math.round(Math.max(0, Math.min(1, rate)) * 100)}%`;
297
- }
298
- async function enqueueAndPollComparison(args, profileOptions) {
299
- const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(args.tenantId)}/replay-sets/${encodeURIComponent(args.replaySetId)}/run`, {
300
- method: "POST",
301
- body: JSON.stringify({
302
- promptArms: args.promptArms,
303
- seedsPerCase: args.seedsPerCase ?? DEFAULT_SEEDS_PER_CASE,
304
- assistantModel: args.assistantModel || undefined,
305
- promotePrimary: args.promotePrimary ?? false
306
- })
307
- }, profileOptions, { timeoutMs: 120000, retries: 1 });
308
- const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
309
- const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
310
- const deadline = Date.now() + timeoutMs;
311
- let latest = null;
312
- while (Date.now() < deadline) {
313
- latest = await fetchLatestRun(args.tenantId, args.replaySetId, enqueue.runId, profileOptions);
314
- const status = latest.run?.status ?? "";
315
- const runId = latest.run?.runId ?? "";
316
- if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
317
- return latest;
318
- }
319
- args.onWaiting?.(status || "pending");
320
- await sleep(pollIntervalMs);
321
- }
322
- if (!latest?.run || latest.run.runId !== enqueue.runId || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
323
- throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
324
- }
325
- return latest;
326
- }
327
- function parsePositiveInt(raw, fallback, max) {
328
- if (!raw)
329
- return fallback;
330
- const parsed = Number.parseInt(raw, 10);
331
- if (!Number.isFinite(parsed) || parsed < 1)
332
- return fallback;
333
- return Math.min(parsed, max);
334
- }
335
- function parseCsv(raw) {
336
- if (!raw?.trim())
337
- return [];
338
- return raw.split(",").map((part) => part.trim()).filter(Boolean);
339
- }
340
- function sleep(ms) {
341
- return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
342
- }
343
-
344
- // src/prompts-propose.ts
345
- import { writeFileSync } from "node:fs";
346
- import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
347
- var PROPOSE_TIMEOUT_MS = 300000;
348
- async function runPromptPropose(positionals, flags, profileOptions, context) {
349
- validateConfig();
350
- const promptKey = flags["prompt-key"] || positionals[1];
351
- const fromRunId = flags["from-run"] || flags["from-run-id"] || flags["run-id"];
352
- const replaySetId = flags["set-id"] || flags["replay-set-id"];
353
- if (!promptKey || !fromRunId || !replaySetId) {
354
- throw new Error("Usage: moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=8] [--model=<slug>] [--out=path.prompt.md] [--gate] [--promote-on-win]");
355
- }
356
- const maxDossiers = parsePositiveInt2(flags["max-dossiers"], 8, 16);
357
- const model = (flags.model ?? flags["revise-model"] ?? "").trim();
358
- const result = await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, {
359
- method: "POST",
360
- body: JSON.stringify({ fromRunId, replaySetId, maxDossiers, model: model || undefined })
361
- }, profileOptions, { timeoutMs: PROPOSE_TIMEOUT_MS, retries: 0 });
362
- let outPath;
363
- if (result.status === "proposed" && result.content && flags.out) {
364
- outPath = isAbsolute2(flags.out) ? flags.out : resolve2(process.cwd(), flags.out);
365
- writeFileSync(outPath, result.content, "utf8");
366
- }
367
- const wantGate = flags.gate === "true";
368
- const promoteOnWin = flags["promote-on-win"] === "true";
369
- let gate;
370
- if (wantGate) {
371
- if (result.status !== "proposed" || !result.content) {
372
- gate = {
373
- ran: false,
374
- scope: "full-set",
375
- scopeNote: `no candidate to gate (${result.status})`,
376
- verdict: "skipped",
377
- outcome: "no-run",
378
- runId: null,
379
- promoted: false
380
- };
381
- } else {
382
- gate = await runGate({
383
- result,
384
- promptKey,
385
- replaySetId,
386
- flags,
387
- promoteOnWin,
388
- profileOptions,
389
- context
390
- });
391
- }
392
- }
393
- if (context.outputMode === "human") {
394
- printHuman(result, outPath, gate);
395
- }
396
- context.output.writeData({
397
- ...result,
398
- ...outPath ? { outPath } : {},
399
- ...gate ? { gate } : {}
400
- });
401
- }
402
- async function runGate(args) {
403
- const { result, promptKey, replaySetId, flags, promoteOnWin, profileOptions, context } = args;
404
- const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
405
- if (!tenantId) {
406
- throw new Error("Missing tenant id for --gate. Run `moda init` or pass --tenant-id=<id>.");
407
- }
408
- const prodArm = resolvePromptArmSpec(promptKey, "baseline", { preferKey: true });
409
- const proposedArm = {
410
- content: result.content ?? "",
411
- prompt_version_id: result.versionId ?? "",
412
- prompt_id: "",
413
- prompt_label: "candidate"
414
- };
415
- const seedsPerCase = parsePositiveInt2(flags.seeds ?? flags["seeds-per-case"], 3, 10);
416
- const assistantModel = (flags["assistant-model"] ?? "").trim();
417
- const timeoutMs = parsePositiveInt2(flags["gate-timeout"] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000);
418
- const pollIntervalMs = parsePositiveInt2(flags["poll-interval"], 15000, 120000);
419
- const scopeNote = result.holdoutCaseIds.length ? `gated over the FULL replay set (${result.holdoutCaseIds.length} holdout case(s) could not be isolated — the run endpoint scores every case)` : "gated over the FULL replay set";
420
- if (context.outputMode === "human") {
421
- process.stderr.write(`
422
- Gating candidate for '${promptKey}' — ${scopeNote}...
423
- `);
424
- }
425
- const payload = await enqueueAndPollComparison({
426
- tenantId,
427
- replaySetId,
428
- promptArms: { prod: prodArm, proposed: proposedArm },
429
- seedsPerCase,
430
- assistantModel: assistantModel || undefined,
431
- promotePrimary: false,
432
- timeoutMs,
433
- pollIntervalMs,
434
- onWaiting: context.outputMode === "human" ? (status) => process.stderr.write(`Waiting for gate run (${status})...
435
- `) : undefined
436
- }, profileOptions);
437
- const outcome = candidateOutcome(payload);
438
- const gate = {
439
- ran: true,
440
- scope: "full-set",
441
- scopeNote,
442
- verdict: formatVerdict(payload),
443
- outcome,
444
- runId: payload.run?.runId ?? null,
445
- promoted: false
446
- };
447
- if (promoteOnWin) {
448
- if (outcome === "candidate" && result.versionId) {
449
- await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/labels`, { method: "POST", body: JSON.stringify({ label: "prod", versionId: result.versionId }) }, profileOptions);
450
- gate.promoted = true;
451
- } else {
452
- gate.promoteSkippedReason = outcome === "candidate" ? "no candidate versionId to promote" : `candidate did not strictly win (${outcome})`;
453
- }
454
- }
455
- return gate;
456
- }
457
- function printHuman(result, outPath, gate) {
458
- const w = (s) => process.stderr.write(s + `
459
- `);
460
- if (result.status !== "proposed") {
461
- w(`No candidate produced (${result.status}): ${result.note}`);
462
- return;
463
- }
464
- w(`Proposed candidate for '${result.promptKey}'`);
465
- w(` new version: ${result.versionId} (unlabeled — not promoted)`);
466
- w(` base version: ${result.baseVersionId ?? "(none)"}`);
467
- w(` from run: built from ${result.dossierCount} failure dossier(s)`);
468
- if (result.changelog)
469
- w(` changelog: ${result.changelog}`);
470
- if (result.risks.length)
471
- w(` risks: ${result.risks.map((r) => `- ${r}`).join(`
472
- `)}`);
473
- if (outPath)
474
- w(` written to: ${outPath}`);
475
- if (result.lowN) {
476
- w("");
477
- w(` ⚠ low sample: ${result.lowN}`);
478
- }
479
- w("");
480
- if (gate?.ran) {
481
- w("Gate (automatic A/B of candidate vs baseline):");
482
- w(` scope: ${gate.scopeNote}`);
483
- w(` verdict: ${gate.verdict}`);
484
- if (gate.runId)
485
- w(` run id: ${gate.runId}`);
486
- if (gate.promoted) {
487
- w(` promoted: candidate won — promoted version ${result.versionId} to label 'prod'`);
488
- } else if (gate.promoteSkippedReason) {
489
- w(` promote: skipped (${gate.promoteSkippedReason})`);
490
- }
491
- w("");
492
- if (!gate.promoted) {
493
- w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
494
- }
495
- return;
496
- }
497
- if (gate && !gate.ran) {
498
- w(`Gate not run: ${gate.scopeNote}`);
499
- w("");
500
- }
501
- w("Next — gate on holdout cases the revision never saw, then promote if it wins:");
502
- if (result.holdoutCaseIds.length) {
503
- w(` holdout cases (${result.holdoutCaseIds.length}): ${result.holdoutCaseIds.join(", ")}`);
504
- } else {
505
- w(" (too few failures to reserve a holdout — gate on a fresh --auto-generate set)");
506
- }
507
- if (outPath) {
508
- w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=${outPath} --auto-generate`);
509
- } else {
510
- w(` # re-run with --out=<path.prompt.md> to materialize the candidate, then:`);
511
- w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=<path.prompt.md> --auto-generate`);
512
- }
513
- w(` # or: moda prompts propose ... --gate --promote-on-win (auto-A/B, promote on a strict win)`);
514
- w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
515
- }
516
- function parsePositiveInt2(raw, fallback, max) {
517
- const n = Number.parseInt(raw ?? "", 10);
518
- if (!Number.isFinite(n) || n <= 0)
519
- return fallback;
520
- return Math.min(n, max);
521
- }
522
-
523
- // src/prompts.ts
524
- var MODA_DIR = ".moda";
525
- var MANIFEST_PATH = join(MODA_DIR, "prompts.yml");
526
- var LOCK_PATH = join(MODA_DIR, "prompts.lock.json");
527
- var DEFAULT_PROMPT_PATHS = [
528
- "prompts/**/*.prompt.md",
529
- "prompts/**/*.prompt.json",
530
- "prompts/**/*.prompt.yaml",
531
- "prompts/**/*.prompt.yml"
532
- ];
533
- async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
534
- const context = Array.isArray(input) ? createCommandContext({
535
- command: "prompts",
536
- positional: input[0],
537
- positionals: input,
538
- flags: legacyFlags ?? {}
539
- }, {
540
- commandFlags: legacyFlags ?? {},
541
- outputMode: legacyFlags?.agent === "true" ? "agent" : legacyFlags?.json === "true" ? "json" : "human",
542
- env: legacyProfileOptions?.env
543
- }) : input;
544
- const positionals = context.positionals;
545
- const flags = context.flags;
546
- const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
547
- const subcommand = positionals[0] || "status";
548
- switch (subcommand) {
549
- case "init": {
550
- const init = await initPrompts({ silent: true });
551
- context.output.writeData({
552
- manifest: MANIFEST_PATH,
553
- manifestPath: init.manifestPath,
554
- lockfile: LOCK_PATH,
555
- lockPath: init.lockPath,
556
- cwd: init.cwd,
557
- created: init.created,
558
- promptPaths: init.promptPaths
559
- }, {
560
- artifacts: [{
561
- kind: "prompt_manifest",
562
- path: init.manifestPath,
563
- description: init.created ? "Prompt discovery manifest created by this run." : "Prompt discovery manifest already existed; left unchanged."
564
- }]
565
- });
566
- break;
567
- }
568
- case "status":
569
- context.output.writeData(await getPromptStatus());
570
- break;
571
- case "diff":
572
- context.output.writeData(await getPromptStatus());
573
- break;
574
- case "sync":
575
- await syncPrompts(flags, profileOptions, context);
576
- break;
577
- case "promote":
578
- await promotePrompt(positionals, flags, profileOptions, context);
579
- break;
580
- case "ab":
581
- await runPromptAb(flags, profileOptions, context);
582
- break;
583
- case "propose":
584
- await runPromptPropose(positionals, flags, profileOptions, context);
585
- break;
586
- default:
587
- throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, or propose.`);
588
- }
589
- }
590
- async function initPrompts(options = {}) {
591
- const cwd = process.cwd();
592
- const manifestPath = resolve3(cwd, MANIFEST_PATH);
593
- const lockPath = resolve3(cwd, LOCK_PATH);
594
- mkdirSync(MODA_DIR, { recursive: true });
595
- const existed = existsSync2(MANIFEST_PATH);
596
- if (!existed) {
597
- writeFileSync2(MANIFEST_PATH, [
598
- "version: 1",
599
- "prompt_paths:",
600
- ...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`),
601
- ""
602
- ].join(`
603
- `));
604
- }
605
- if (!existsSync2(manifestPath)) {
606
- throw new Error(`Could not write ${manifestPath}: the file is missing after init. Check directory permissions and available disk space.`);
607
- }
608
- const result = {
609
- manifestPath,
610
- lockPath,
611
- cwd,
612
- created: !existed,
613
- promptPaths: readManifest().promptPaths
614
- };
615
- if (!options.silent) {
616
- printJson({
617
- manifest: MANIFEST_PATH,
618
- manifestPath,
619
- lockfile: LOCK_PATH,
620
- lockPath,
621
- cwd,
622
- created: result.created,
623
- promptPaths: result.promptPaths
624
- });
625
- }
626
- return result;
627
- }
628
- async function syncPrompts(flags, profileOptions, context) {
629
- if (flags.watch === "true") {
630
- await watchPromptSync(flags, profileOptions);
631
- return;
632
- }
633
- await syncPromptsOnce(flags, profileOptions, context);
634
- }
635
- async function runPromptSync(flags, profileOptions) {
636
- await initPrompts({ silent: true });
637
- const prompts = discoverPrompts();
638
- const sourceCommit = getGitCommit();
639
- const manifestHash = hashFile(MANIFEST_PATH);
640
- const dryRun = flags["dry-run"] === "true";
641
- const response = await callControlAPI("/prompts/sync", {
642
- method: "POST",
643
- body: JSON.stringify({
644
- prompts: prompts.map((prompt) => ({
645
- ...prompt,
646
- sourceCommit,
647
- manifestHash
648
- })),
649
- sourceCommit,
650
- manifestHash,
651
- dryRun
652
- })
653
- }, profileOptions);
654
- if (!dryRun) {
655
- writeLock(prompts, response.synced ?? []);
656
- }
657
- return { synced: response.synced ?? [], dryRun };
658
- }
659
- async function syncPromptsOnce(flags, profileOptions, context) {
660
- const result = await runPromptSync(flags, profileOptions);
661
- const payload = {
662
- synced: result.synced,
663
- lockfile: result.dryRun ? undefined : LOCK_PATH
664
- };
665
- if (context)
666
- context.output.writeData(payload);
667
- else
668
- printJson(payload);
669
- }
670
- async function watchPromptSync(flags, profileOptions) {
671
- console.error("Watching prompt files. Press Ctrl+C to stop.");
672
- let lastSignature = "";
673
- const intervalMs = Math.max(1000, Number(flags.interval ?? 2500));
674
- while (true) {
675
- const status = await getPromptStatus();
676
- const signature = stableJson(status.prompts);
677
- if (signature !== lastSignature) {
678
- lastSignature = signature;
679
- if (status.changed > 0 || status.new > 0 || status.deleted > 0) {
680
- await syncPromptsOnce({ ...flags, watch: "false" }, profileOptions);
681
- }
682
- }
683
- await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
684
- }
685
- }
686
- async function promotePrompt(positionals, flags, profileOptions, context) {
687
- const key = positionals[1];
688
- const label = flags.label;
689
- const versionId = flags.version || flags["version-id"];
690
- if (!key)
691
- throw new Error("Usage: moda prompts promote <prompt_key> --label=prod --version=<version_id>");
692
- if (!label)
693
- throw new Error("Missing required flag: --label=dev|staging|prod");
694
- if (!versionId)
695
- throw new Error("Missing required flag: --version=<version_id>");
696
- const response = await callControlAPI(`/prompts/${encodeURIComponent(key)}/labels`, {
697
- method: "POST",
698
- body: JSON.stringify({ label, versionId })
699
- }, profileOptions);
700
- context.output.writeData(response);
701
- }
702
- async function getPromptStatus() {
703
- await initPrompts({ silent: true });
704
- const prompts = discoverPrompts();
705
- const lock = readLock();
706
- const rows = prompts.map((prompt) => {
707
- const locked = lock?.prompts[prompt.key];
708
- const state = !locked ? "new" : locked.contentHash === prompt.contentHash ? "unchanged" : "changed";
709
- return {
710
- key: prompt.key,
711
- sourcePath: prompt.sourcePath,
712
- contentHash: prompt.contentHash,
713
- lockedHash: locked?.contentHash ?? null,
714
- versionId: locked?.versionId ?? null,
715
- state
716
- };
717
- });
718
- const discoveredKeys = new Set(prompts.map((prompt) => prompt.key));
719
- for (const [key, locked] of Object.entries(lock?.prompts ?? {})) {
720
- if (!discoveredKeys.has(key)) {
721
- rows.push({
722
- key,
723
- sourcePath: locked.sourcePath,
724
- contentHash: null,
725
- lockedHash: locked.contentHash,
726
- versionId: locked.versionId ?? null,
727
- state: "deleted"
728
- });
729
- }
730
- }
731
- return {
732
- manifest: MANIFEST_PATH,
733
- lockfile: LOCK_PATH,
734
- total: rows.length,
735
- changed: rows.filter((row) => row.state === "changed").length,
736
- new: rows.filter((row) => row.state === "new").length,
737
- deleted: rows.filter((row) => row.state === "deleted").length,
738
- prompts: rows
739
- };
740
- }
741
- function discoverPrompts() {
742
- const manifest = readManifest();
743
- const rules = manifest.promptPaths.map((pattern) => {
744
- const suffixMatch = pattern.match(/\.prompt\.(md|json|ya?ml)$/i);
745
- if (!suffixMatch)
746
- return null;
747
- const suffix = `.prompt.${suffixMatch[1]}`;
748
- const globIndex = pattern.search(/[*?[]/);
749
- const baseRaw = globIndex === -1 ? dirname(pattern) : pattern.slice(0, globIndex);
750
- const base = baseRaw.replace(/[\\/]+$/, "") || ".";
751
- const baseAbs = isAbsolute3(base) ? base : resolve3(process.cwd(), base);
752
- return { baseAbs, suffix };
753
- }).filter((rule) => Boolean(rule));
754
- const effectiveRules = rules.length ? rules : [
755
- { baseAbs: process.cwd(), suffix: ".prompt.md" },
756
- { baseAbs: process.cwd(), suffix: ".prompt.json" }
757
- ];
758
- const discovered = new Set;
759
- for (const { baseAbs, suffix } of effectiveRules) {
760
- if (!existsSync2(baseAbs))
761
- continue;
762
- for (const file of walk(baseAbs)) {
763
- if (file.endsWith(suffix))
764
- discovered.add(file);
765
- }
766
- }
767
- return [...discovered].sort().map(parsePromptFile);
768
- }
769
- function parsePromptFile(absPath) {
770
- const sourcePath = relative(process.cwd(), absPath);
771
- const raw = readFileSync(absPath, "utf8");
772
- if (absPath.endsWith(".json")) {
773
- const parsed = JSON.parse(raw);
774
- const key2 = parsed.key || keyFromPath(sourcePath);
775
- const hasContent = typeof parsed.content === "string" && parsed.content.trim().length > 0;
776
- const hasSystemPrompt = typeof parsed.systemPrompt === "string" && parsed.systemPrompt.trim().length > 0;
777
- const hasTemplate = typeof parsed.template === "string" && parsed.template.trim().length > 0;
778
- const hasMessages = Array.isArray(parsed.messages) && parsed.messages.some((message) => {
779
- if (!message || typeof message !== "object" || !("role" in message) || !["system", "developer", "user", "assistant", "tool"].includes(String(message.role)) || !("content" in message))
780
- return false;
781
- const content2 = message.content;
782
- return typeof content2 === "string" ? content2.trim().length > 0 : Array.isArray(content2) && content2.some((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
783
- });
784
- if (!hasContent && !hasSystemPrompt && !hasMessages && !hasTemplate) {
785
- throw new Error(`${sourcePath}: prompt JSON requires non-empty content, systemPrompt, messages, or a text template.`);
786
- }
787
- const content = hasContent ? parsed.content : hasSystemPrompt ? parsed.systemPrompt : hasMessages ? JSON.stringify(parsed.messages, null, 2) : parsed.template;
788
- return normalizePrompt({ ...parsed, key: key2, name: parsed.name || key2, sourcePath, content });
789
- }
790
- const { frontmatter, body } = parseFrontmatter(raw);
791
- const key = frontmatter.key || keyFromPath(sourcePath);
792
- return normalizePrompt({
793
- key,
794
- name: frontmatter.name || key,
795
- description: frontmatter.description,
796
- category: frontmatter.category,
797
- systemPrompt: frontmatter.system_prompt || frontmatter.systemPrompt,
798
- modelConfig: frontmatter.model ? { model: frontmatter.model } : undefined,
799
- sourcePath,
800
- content: body.trim()
801
- });
802
- }
803
- function normalizePrompt(prompt) {
804
- const normalized = stableJson({
805
- key: prompt.key,
806
- content: prompt.content,
807
- systemPrompt: prompt.systemPrompt ?? null,
808
- messages: prompt.messages ?? [],
809
- variables: prompt.variables ?? [],
810
- variablesSchema: prompt.variablesSchema ?? {},
811
- modelConfig: prompt.modelConfig ?? {},
812
- toolRefs: prompt.toolRefs ?? [],
813
- responseSchema: prompt.responseSchema ?? null
814
- });
815
- return {
816
- key: prompt.key,
817
- name: prompt.name || prompt.key,
818
- description: prompt.description,
819
- sourcePath: prompt.sourcePath,
820
- content: prompt.content,
821
- systemPrompt: prompt.systemPrompt,
822
- messages: prompt.messages,
823
- variables: prompt.variables,
824
- variablesSchema: prompt.variablesSchema,
825
- modelConfig: prompt.modelConfig,
826
- toolRefs: prompt.toolRefs,
827
- responseSchema: prompt.responseSchema,
828
- contentHash: sha256(normalized)
829
- };
830
- }
831
- function readManifest() {
832
- if (!existsSync2(MANIFEST_PATH)) {
833
- return { promptPaths: DEFAULT_PROMPT_PATHS };
834
- }
835
- const text = readFileSync(MANIFEST_PATH, "utf8");
836
- const promptPaths = [...text.matchAll(/^\s*-\s*["']?([^"'\n]+)["']?\s*$/gm)].map((match) => match[1]).filter((path) => path.includes(".prompt."));
837
- return { promptPaths: promptPaths.length ? promptPaths : DEFAULT_PROMPT_PATHS };
838
- }
839
- function readPromptLock() {
840
- return readLock();
841
- }
842
- function parsePromptFileForAb(absPath) {
843
- const parsed = parsePromptFile(absPath);
844
- return { key: parsed.key, content: parsed.content };
845
- }
846
- function readLock() {
847
- if (!existsSync2(LOCK_PATH))
848
- return null;
849
- return JSON.parse(readFileSync(LOCK_PATH, "utf8"));
850
- }
851
- function writeLock(prompts, synced) {
852
- const syncedByKey = new Map(synced.map((item) => [item.key, item]));
853
- const lock = {
854
- version: 1,
855
- generatedAt: new Date().toISOString(),
856
- prompts: Object.fromEntries(prompts.map((prompt) => {
857
- const result = syncedByKey.get(prompt.key);
858
- return [prompt.key, {
859
- key: prompt.key,
860
- sourcePath: prompt.sourcePath,
861
- contentHash: prompt.contentHash,
862
- promptId: result?.promptId,
863
- versionId: result?.versionId
864
- }];
865
- }))
866
- };
867
- mkdirSync(dirname(LOCK_PATH), { recursive: true });
868
- writeFileSync2(LOCK_PATH, `${JSON.stringify(lock, null, 2)}
869
- `);
870
- }
871
- function walk(dir) {
872
- let entries;
873
- try {
874
- entries = readdirSync(dir);
875
- } catch {
876
- return [];
877
- }
878
- const files = [];
879
- for (const entry of entries) {
880
- if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build")
881
- continue;
882
- const fullPath = join(dir, entry);
883
- let stat;
884
- try {
885
- stat = lstatSync(fullPath);
886
- } catch {
887
- continue;
888
- }
889
- if (stat.isSymbolicLink())
890
- continue;
891
- if (stat.isDirectory())
892
- files.push(...walk(fullPath));
893
- else if (stat.isFile())
894
- files.push(fullPath);
895
- }
896
- return files;
897
- }
898
- function parseFrontmatter(raw) {
899
- if (!raw.startsWith("---"))
900
- return { frontmatter: {}, body: raw };
901
- const end = raw.indexOf(`
902
- ---`, 3);
903
- if (end === -1)
904
- return { frontmatter: {}, body: raw };
905
- const frontmatterText = raw.slice(3, end).trim();
906
- const frontmatter = Object.fromEntries(frontmatterText.split(`
907
- `).map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)).filter((match) => Boolean(match)).map((match) => [match[1], match[2].replace(/^["']|["']$/g, "")]));
908
- return { frontmatter, body: raw.slice(end + 4) };
909
- }
910
- function keyFromPath(path) {
911
- return path.replace(/^prompts\//, "").replace(/\.prompt\.(md|json|ya?ml)$/i, "").replace(/[\\/]/g, ".");
912
- }
913
- function hashFile(path) {
914
- return existsSync2(path) ? sha256(readFileSync(path, "utf8")) : "";
915
- }
916
- function getGitCommit() {
917
- try {
918
- return execFileSync("git", ["rev-parse", "HEAD"], {
919
- encoding: "utf8",
920
- stdio: ["ignore", "pipe", "ignore"]
921
- }).trim();
922
- } catch {
923
- return;
924
- }
925
- }
926
- function stableJson(value) {
927
- return JSON.stringify(sortJson(value));
928
- }
929
- function sortJson(value) {
930
- if (Array.isArray(value))
931
- return value.map(sortJson);
932
- if (value && typeof value === "object") {
933
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
934
- }
935
- return value;
936
- }
937
- function sha256(input) {
938
- return createHash("sha256").update(input).digest("hex");
939
- }
940
- function printJson(value) {
941
- console.log(JSON.stringify(value, null, 2));
942
- }
943
-
944
- // src/init/rules.ts
945
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
946
- import { join as join3 } from "node:path";
947
- import { confirm } from "@inquirer/prompts";
948
-
949
- // src/package-assets.ts
950
- import { dirname as dirname2, join as join2 } from "node:path";
951
- import { fileURLToPath } from "node:url";
952
- function packageAssetPath(...parts) {
953
- return join2(dirname2(fileURLToPath(import.meta.url)), "..", ...parts);
954
- }
955
-
956
- // src/init/rules.ts
957
- var SKILLS_SOURCE_DIR = packageAssetPath("skills");
958
- var SKILLS = [
959
- {
960
- id: "moda-cli",
961
- label: "moda-cli — let coding agents query Moda analytics via the CLI"
962
- }
963
- ];
964
- var ADAPTERS = {
965
- "Claude Code": {
966
- name: "Claude Code",
967
- targetFor: (skillId) => ({
968
- dir: join3(".claude", "skills", skillId),
969
- filename: "SKILL.md"
970
- }),
971
- transform: (content) => content
972
- },
973
- Cursor: {
974
- name: "Cursor",
975
- targetFor: (skillId) => ({
976
- dir: join3(".cursor", "rules"),
977
- filename: `${skillId}.mdc`
978
- }),
979
- transform: claudeSkillToCursorRule
980
- }
981
- };
982
- var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
983
- var DESCRIPTION_RE = /^description:\s*(.+?)\s*$/m;
984
- function claudeSkillToCursorRule(skillContent) {
985
- const match = FRONTMATTER_RE.exec(skillContent);
986
- if (!match) {
987
- return skillContent;
988
- }
989
- const [, frontmatter, body] = match;
990
- const descMatch = DESCRIPTION_RE.exec(frontmatter);
991
- const description = descMatch?.[1].trim() ?? "";
992
- const cursorFrontmatter = `---
993
- ` + `description: ${JSON.stringify(description)}
994
- ` + `alwaysApply: false
995
- ` + `---
996
- `;
997
- return `${cursorFrontmatter}
998
- ${body.replace(/^\s+/, "")}`;
999
- }
1000
- function loadSkillSource(skillId) {
1001
- const path = join3(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
1002
- if (!existsSync3(path)) {
1003
- throw new Error(`Skill source missing: ${path}`);
1004
- }
1005
- return readFileSync2(path, "utf8");
1006
- }
1007
- function installSkillForAgent(skill, adapter, content) {
1008
- const { dir, filename } = adapter.targetFor(skill.id);
1009
- const dirPath = join3(process.cwd(), dir);
1010
- const filePath = join3(dirPath, filename);
1011
- const relativePath = join3(dir, filename);
1012
- try {
1013
- mkdirSync2(dirPath, { recursive: true });
1014
- if (existsSync3(filePath)) {
1015
- return { agent: adapter.name, relativePath, status: "exists" };
1016
- }
1017
- writeFileSync3(filePath, adapter.transform(content));
1018
- return { agent: adapter.name, relativePath, status: "wrote" };
1019
- } catch (err) {
1020
- return {
1021
- agent: adapter.name,
1022
- relativePath,
1023
- status: "failed",
1024
- error: err instanceof Error ? err.message : String(err)
1025
- };
1026
- }
1027
- }
1028
- async function installRules(detectedAgents, options = {}) {
1029
- const adapters = detectedAgents.map((a) => ADAPTERS[a]).filter((a) => a !== undefined);
1030
- if (adapters.length === 0) {
1031
- if (!options.silent)
1032
- console.log(" No supported coding agents detected");
1033
- return;
1034
- }
1035
- const agentList = adapters.map((a) => a.name).join(", ");
1036
- const skillList = SKILLS.map((s) => `
1037
- • ${s.label}`).join("");
1038
- const shouldInstall = options.yes ? true : await confirm({
1039
- message: `Install Moda skills for ${agentList}?${skillList}
1040
- Continue?`,
1041
- default: true
1042
- });
1043
- if (!shouldInstall) {
1044
- if (!options.silent)
1045
- console.log(" Skipped");
1046
- return;
1047
- }
1048
- let wrote = 0;
1049
- let skipped = 0;
1050
- let failed = 0;
1051
- for (const skill of SKILLS) {
1052
- let content;
1053
- try {
1054
- content = loadSkillSource(skill.id);
1055
- } catch (err) {
1056
- const message = err instanceof Error ? err.message : String(err);
1057
- console.error(` Warning: ${message}`);
1058
- failed += adapters.length;
1059
- continue;
1060
- }
1061
- for (const adapter of adapters) {
1062
- const outcome = installSkillForAgent(skill, adapter, content);
1063
- switch (outcome.status) {
1064
- case "wrote":
1065
- if (!options.silent)
1066
- console.log(` ${outcome.agent}: wrote ${outcome.relativePath}`);
1067
- wrote++;
1068
- break;
1069
- case "exists":
1070
- if (!options.silent) {
1071
- console.log(` ${outcome.agent}: ${outcome.relativePath} already exists — skipping`);
1072
- }
1073
- skipped++;
1074
- break;
1075
- case "failed":
1076
- if (!options.silent) {
1077
- console.error(` ${outcome.agent}: failed to write ${outcome.relativePath} — ${outcome.error}`);
1078
- }
1079
- failed++;
1080
- break;
1081
- }
1082
- }
1083
- }
1084
- const summary = [
1085
- wrote && `${wrote} written`,
1086
- skipped && `${skipped} already present`,
1087
- failed && `${failed} failed`
1088
- ].filter(Boolean).join(", ");
1089
- if (summary) {
1090
- if (!options.silent)
1091
- console.log(` Done — ${summary}`);
1092
- }
1093
- }
1094
-
1095
- // src/init/mcp.ts
1096
- import { existsSync as existsSync4 } from "node:fs";
1097
- import { join as join4 } from "node:path";
1098
- import { execSync } from "node:child_process";
1099
- function isOnPath(cmd) {
1100
- try {
1101
- execSync(`which ${cmd}`, { stdio: "pipe" });
1102
- return true;
1103
- } catch {
1104
- return false;
1105
- }
1106
- }
1107
- function detectAgents() {
1108
- const cwd = process.cwd();
1109
- const agents = [];
1110
- if (existsSync4(join4(cwd, ".claude")) || isOnPath("claude")) {
1111
- agents.push("Claude Code");
1112
- }
1113
- if (existsSync4(join4(cwd, ".cursor"))) {
1114
- agents.push("Cursor");
1115
- }
1116
- return agents;
1117
- }
1118
-
1119
- // src/skills.ts
1120
- import { execFileSync as execFileSync2 } from "node:child_process";
1121
- import { createHash as createHash2 } from "node:crypto";
1122
- import {
1123
- existsSync as existsSync7,
1124
- lstatSync as lstatSync2,
1125
- mkdirSync as mkdirSync5,
1126
- readdirSync as readdirSync3,
1127
- readFileSync as readFileSync5,
1128
- writeFileSync as writeFileSync6
1129
- } from "node:fs";
1130
- import { basename, dirname as dirname5, join as join7, relative as relative3 } from "node:path";
1131
-
1132
- // src/skills-manifest.ts
1133
- import {
1134
- existsSync as existsSync5,
1135
- mkdirSync as mkdirSync3,
1136
- readdirSync as readdirSync2,
1137
- readFileSync as readFileSync3,
1138
- rmdirSync,
1139
- unlinkSync,
1140
- writeFileSync as writeFileSync4
1141
- } from "node:fs";
1142
- import { dirname as dirname3, isAbsolute as isAbsolute4, join as join5, normalize, relative as relative2, resolve as resolve4 } from "node:path";
1143
- var SKILLS_MANIFEST_PATH = join5(".moda", "skills.yml");
1144
- var SKILL_SOURCE_HARNESS = "moda";
1145
- var SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
1146
- function readSkillsManifest(cwd) {
1147
- const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
1148
- if (!existsSync5(manifestPath))
1149
- return null;
1150
- let text = "";
1151
- try {
1152
- text = readFileSync3(manifestPath, "utf8");
1153
- } catch {
1154
- return null;
1155
- }
1156
- return {
1157
- tenantId: readYamlScalar(text, "tenant_id"),
1158
- generationRunId: readYamlScalar(text, "generation_run_id"),
1159
- generationVersion: Number(readYamlScalar(text, "generation_version") || 0),
1160
- releaseKey: readYamlScalar(text, "release_key"),
1161
- statusFilter: readYamlScalar(text, "status_filter"),
1162
- releasedAt: readYamlScalar(text, "released_at"),
1163
- managedPaths: readManagedPaths(text),
1164
- records: readSkillRecords(text)
1165
- };
1166
- }
1167
- function manifestHeaderFrom(manifest) {
1168
- return {
1169
- tenantId: manifest?.tenantId ?? "",
1170
- releaseKey: manifest?.releaseKey ?? "",
1171
- statusFilter: manifest?.statusFilter ?? "",
1172
- generationRunId: manifest?.generationRunId ?? "",
1173
- generationVersion: manifest?.generationVersion ?? 0,
1174
- releasedAt: manifest?.releasedAt ?? ""
1175
- };
1176
- }
1177
- function writeSkillsManifest(cwd, header, records) {
1178
- const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
1179
- const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
1180
- const lines = [
1181
- "version: 1",
1182
- "source: moda",
1183
- `tenant_id: ${yamlQuote(header.tenantId)}`,
1184
- `release_key: ${yamlQuote(header.releaseKey)}`,
1185
- `status_filter: ${yamlQuote(header.statusFilter)}`,
1186
- `generation_run_id: ${yamlQuote(header.generationRunId)}`,
1187
- `generation_version: ${header.generationVersion}`,
1188
- `released_at: ${yamlQuote(header.releasedAt)}`,
1189
- `installed_at: ${yamlQuote(new Date().toISOString())}`,
1190
- "managed_paths:",
1191
- ...managedPaths.map((path) => ` - ${yamlQuote(path)}`),
1192
- "skills:",
1193
- ...records.flatMap((record) => [
1194
- ` - skill_id: ${yamlQuote(record.skillId)}`,
1195
- ` content_hash: ${yamlQuote(record.contentHash)}`,
1196
- ` source: ${yamlQuote(record.source)}`,
1197
- " paths:",
1198
- ...record.paths.map((path) => ` - ${yamlQuote(path)}`)
1199
- ]),
1200
- ""
1201
- ];
1202
- mkdirSync3(dirname3(manifestPath), { recursive: true });
1203
- writeFileSync4(manifestPath, lines.join(`
1204
- `));
1205
- }
1206
- function upsertSkillManifestRecord(cwd, record) {
1207
- const manifest = readSkillsManifest(cwd);
1208
- const records = (manifest?.records ?? []).filter((existing) => !(existing.skillId === record.skillId && existing.source === record.source));
1209
- records.push(record);
1210
- writeSkillsManifest(cwd, manifestHeaderFrom(manifest), records);
1211
- }
1212
- function pruneManagedSkillPaths(cwd, managedPaths, log) {
1213
- let pruned = 0;
1214
- for (const rawPath of [...new Set(managedPaths)]) {
1215
- const safePath = safeManagedRelativePath(rawPath);
1216
- if (!safePath)
1217
- continue;
1218
- const absolutePath = absoluteManagedPath(cwd, safePath);
1219
- if (!absolutePath || !existsSync5(absolutePath))
1220
- continue;
1221
- try {
1222
- unlinkSync(absolutePath);
1223
- pruned++;
1224
- log(` pruned ${safePath}`);
1225
- pruneEmptyClaudeSkillDir(cwd, safePath);
1226
- } catch {}
1227
- }
1228
- return pruned;
1229
- }
1230
- function pruneEmptyClaudeSkillDir(cwd, relativePath) {
1231
- if (!relativePath.startsWith(".claude/skills/") || !relativePath.endsWith("/SKILL.md"))
1232
- return;
1233
- const dir = dirname3(join5(cwd, relativePath));
1234
- try {
1235
- if (existsSync5(dir) && readdirSync2(dir).length === 0) {
1236
- rmdirSync(dir);
1237
- }
1238
- } catch {}
1239
- }
1240
- function absoluteManagedPath(cwd, relativePath) {
1241
- const abs = resolve4(cwd, relativePath);
1242
- const root = resolve4(cwd);
1243
- const rel = relative2(root, abs);
1244
- if (rel.startsWith("..") || isAbsolute4(rel))
1245
- return null;
1246
- return abs;
1247
- }
1248
- function safeManagedRelativePath(rawPath) {
1249
- if (!rawPath || rawPath.includes("\x00") || isAbsolute4(rawPath))
1250
- return null;
1251
- const normalized = normalize(rawPath).replace(/\\/g, "/");
1252
- if (normalized.startsWith("../") || normalized === ".." || normalized.includes("/../"))
1253
- return null;
1254
- const isClaudeSkill = normalized.startsWith(".claude/skills/") && normalized.endsWith("/SKILL.md");
1255
- const isCursorRule = normalized.startsWith(".cursor/rules/") && normalized.endsWith(".mdc");
1256
- return isClaudeSkill || isCursorRule ? normalized : null;
1257
- }
1258
- function readYamlScalar(text, key) {
1259
- const match = text.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
1260
- return match ? parseYamlScalar(match[1]) : "";
1261
- }
1262
- function readSkillRecords(text) {
1263
- const records = [];
1264
- let inSkills = false;
1265
- let current = null;
1266
- let inPaths = false;
1267
- for (const line of text.split(/\r?\n/)) {
1268
- if (line.trim() === "skills:") {
1269
- inSkills = true;
1270
- continue;
1271
- }
1272
- if (!inSkills)
1273
- continue;
1274
- if (line.length > 0 && !line.startsWith(" "))
1275
- break;
1276
- const skillIdMatch = line.match(/^ {2}- skill_id:\s*(.*?)\s*$/);
1277
- if (skillIdMatch) {
1278
- if (current)
1279
- records.push(current);
1280
- current = {
1281
- skillId: parseYamlScalar(skillIdMatch[1]),
1282
- contentHash: "",
1283
- source: SKILL_SOURCE_HARNESS,
1284
- paths: []
1285
- };
1286
- inPaths = false;
1287
- continue;
1288
- }
1289
- if (!current)
1290
- continue;
1291
- const hashMatch = line.match(/^ {4}content_hash:\s*(.*?)\s*$/);
1292
- if (hashMatch) {
1293
- current.contentHash = parseYamlScalar(hashMatch[1]);
1294
- continue;
1295
- }
1296
- const sourceMatch = line.match(/^ {4}source:\s*(.*?)\s*$/);
1297
- if (sourceMatch) {
1298
- current.source = parseYamlScalar(sourceMatch[1]) || SKILL_SOURCE_HARNESS;
1299
- continue;
1300
- }
1301
- if (/^ {4}paths:\s*$/.test(line)) {
1302
- inPaths = true;
1303
- continue;
1304
- }
1305
- const pathMatch = inPaths ? line.match(/^ {6}-\s*(.*?)\s*$/) : null;
1306
- if (pathMatch) {
1307
- const value = parseYamlScalar(pathMatch[1]);
1308
- if (value)
1309
- current.paths.push(value);
1310
- }
1311
- }
1312
- if (current)
1313
- records.push(current);
1314
- return records;
1315
- }
1316
- function readManagedPaths(text) {
1317
- const paths = [];
1318
- let inManagedPaths = false;
1319
- for (const line of text.split(/\r?\n/)) {
1320
- if (line.trim() === "managed_paths:") {
1321
- inManagedPaths = true;
1322
- continue;
1323
- }
1324
- if (inManagedPaths && line.length > 0 && !line.startsWith(" "))
1325
- break;
1326
- if (!inManagedPaths)
1327
- continue;
1328
- const match = line.match(/^\s*-\s*(.*?)\s*$/);
1329
- if (match)
1330
- paths.push(parseYamlScalar(match[1]));
1331
- }
1332
- return paths.filter(Boolean);
1333
- }
1334
- function parseYamlScalar(raw) {
1335
- const value = raw.trim();
1336
- if (!value)
1337
- return "";
1338
- if (value.startsWith('"')) {
1339
- try {
1340
- return String(JSON.parse(value));
1341
- } catch {
1342
- return value.replace(/^"|"$/g, "");
1343
- }
1344
- }
1345
- return value.replace(/^'|'$/g, "");
1346
- }
1347
- function yamlQuote(value) {
1348
- return JSON.stringify(value);
1349
- }
1350
-
1351
- // src/agents-md.ts
1352
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1353
- import { dirname as dirname4, join as join6 } from "node:path";
1354
- var AGENTS_MD_FILENAME = "AGENTS.md";
1355
- var FIXES_BLOCK_BEGIN = "<!-- moda:fixes:begin -->";
1356
- var FIXES_BLOCK_END = "<!-- moda:fixes:end -->";
1357
- function renderFixesBlock() {
1358
- return [
1359
- FIXES_BLOCK_BEGIN,
1360
- "## Moda Fixes (managed block — edits inside are overwritten by moda-cli)",
1361
- "",
1362
- "- Before editing prompt or skill files, run `moda fixes` to see the ranked",
1363
- ' queue of Moda Fixes ("a fix with proof") already drafted for this repo,',
1364
- " and `moda fix <fix_id> --packet` for the evidence packet.",
1365
- "- Gate any prompt/skill change against production evidence with",
1366
- " `moda fix verify <fix_id> [--prompt-file=<path>]`. Exit codes: 0 = holdout",
1367
- " win vs baseline (pass), 1 = failing (per-case findings in the envelope),",
1368
- " 3 = degraded/inconclusive — never treat exit 3 as a pass.",
1369
- "- Fix PRs must carry the magic word `Fixes MODA-FIX-<SHORTREF>` in the PR",
1370
- " body so the merge confirms the fix and starts production monitoring.",
1371
- FIXES_BLOCK_END
1372
- ].join(`
1373
- `);
1374
- }
1375
- function upsertManagedBlock(existing, block) {
1376
- if (existing === null) {
1377
- return `${block}
1378
- `;
1379
- }
1380
- const beginIdx = existing.indexOf(FIXES_BLOCK_BEGIN);
1381
- const endIdx = existing.indexOf(FIXES_BLOCK_END);
1382
- if (beginIdx !== -1 && endIdx !== -1 && endIdx >= beginIdx) {
1383
- const before = existing.slice(0, beginIdx);
1384
- const after = existing.slice(endIdx + FIXES_BLOCK_END.length);
1385
- return `${before}${block}${after}`;
1386
- }
1387
- const separator = existing === "" || existing.endsWith(`
1388
-
1389
- `) ? "" : existing.endsWith(`
1390
- `) ? `
1391
- ` : `
1392
-
1393
- `;
1394
- return `${existing}${separator}${block}
1395
- `;
1396
- }
1397
- function writeFileIfChanged(filePath, content) {
1398
- try {
1399
- mkdirSync4(dirname4(filePath), { recursive: true });
1400
- if (existsSync6(filePath)) {
1401
- const current = readFileSync4(filePath, "utf8");
1402
- if (current === content)
1403
- return "unchanged";
1404
- writeFileSync5(filePath, content);
1405
- return "updated";
1406
- }
1407
- writeFileSync5(filePath, content);
1408
- return "wrote";
1409
- } catch {
1410
- return "failed";
1411
- }
1412
- }
1413
- function upsertFixesAgentsBlock(cwd) {
1414
- const filePath = join6(cwd, AGENTS_MD_FILENAME);
1415
- let existing = null;
1416
- if (existsSync6(filePath)) {
1417
- try {
1418
- existing = readFileSync4(filePath, "utf8");
1419
- } catch {
1420
- return { path: AGENTS_MD_FILENAME, status: "failed" };
1421
- }
1422
- }
1423
- const next = upsertManagedBlock(existing, renderFixesBlock());
1424
- return { path: AGENTS_MD_FILENAME, status: writeFileIfChanged(filePath, next) };
1425
- }
1426
-
1427
- // src/skills.ts
1428
- var SKILLS_REQUEST_TIMEOUT_MS = 15000;
1429
- var VALID_STATUSES = new Set(["approved", "proposed", "all"]);
1430
- var SKILL_PROPOSALS_MANIFEST_PATH = join7(".moda", "skill-proposals.yml");
1431
- async function fetchSkills(status) {
1432
- const apiKey = resolveApiKey();
1433
- if (!apiKey) {
1434
- throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
1435
- }
1436
- const ingestUrl = resolveIngestUrl().replace(/\/$/, "");
1437
- const url = `${ingestUrl}/v1/skills?status=${encodeURIComponent(status)}`;
1438
- const controller = new AbortController;
1439
- const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
1440
- let response;
1441
- try {
1442
- response = await fetch(url, {
1443
- headers: { Authorization: `Bearer ${apiKey}` },
1444
- signal: controller.signal
1445
- });
1446
- } catch (err) {
1447
- const message = err instanceof Error ? err.message : String(err);
1448
- throw new Error(`Network error: failed to reach ${ingestUrl} (${message})`);
1449
- } finally {
1450
- clearTimeout(timeout);
1451
- }
1452
- if (!response.ok) {
1453
- const body = await response.text().catch(() => "");
1454
- throw new Error(`Failed to fetch skills (HTTP ${response.status}): ${body.slice(0, 200)}`);
1455
- }
1456
- const data = await response.json();
1457
- if (!data.success) {
1458
- throw new Error(data.message || "Failed to fetch skills");
1459
- }
1460
- const skills = Array.isArray(data.skills) ? data.skills : [];
1461
- const release = data.release && typeof data.release === "object" ? data.release : null;
1462
- return {
1463
- skills,
1464
- tenantId: data.tenant_id || resolveTenantId() || "",
1465
- release,
1466
- releaseKey: releaseKeyFor(release, skills, status),
1467
- status
1468
- };
1469
- }
1470
- async function skillRunsRequest(url) {
1471
- const apiKey = resolveApiKey();
1472
- if (!apiKey) {
1473
- throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
1474
- }
1475
- const controller = new AbortController;
1476
- const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
1477
- let response;
1478
- try {
1479
- response = await fetch(url, {
1480
- headers: { Authorization: `Bearer ${apiKey}` },
1481
- signal: controller.signal
1482
- });
1483
- } catch (err) {
1484
- const message = err instanceof Error ? err.message : String(err);
1485
- throw new Error(`Network error: failed to reach ${url} (${message})`);
1486
- } finally {
1487
- clearTimeout(timeout);
1488
- }
1489
- const text = await response.text().catch(() => "");
1490
- if (!response.ok && response.status !== 404) {
1491
- throw new Error(`Failed to fetch run status (HTTP ${response.status}): ${text.slice(0, 200)}`);
1492
- }
1493
- let body = {};
1494
- try {
1495
- body = text ? JSON.parse(text) : {};
1496
- } catch {
1497
- body = {};
1498
- }
1499
- return { status: response.status, body };
1500
- }
1501
- async function fetchSkillRunStatus(runId) {
1502
- const ingestUrl = resolveIngestUrl().replace(/\/$/, "");
1503
- let resolvedRunId = runId;
1504
- if (!resolvedRunId) {
1505
- const list = await skillRunsRequest(`${ingestUrl}/v1/skills/runs?limit=1`);
1506
- const listBody = list.body;
1507
- const latest = Array.isArray(listBody.runs) ? listBody.runs[0] : undefined;
1508
- if (!latest)
1509
- return { run: null, events: [] };
1510
- resolvedRunId = latest.generation_run_id;
1511
- if (!resolvedRunId)
1512
- return { run: latest, events: [] };
1513
- }
1514
- const detail = await skillRunsRequest(`${ingestUrl}/v1/skills/runs/${encodeURIComponent(resolvedRunId)}`);
1515
- if (detail.status === 404) {
1516
- if (runId) {
1517
- throw new CliInputError(`No skill generation run found with id '${runId}'`);
1518
- }
1519
- return { run: null, events: [] };
1520
- }
1521
- const detailBody = detail.body;
1522
- return {
1523
- run: detailBody.run ?? null,
1524
- events: Array.isArray(detailBody.events) ? detailBody.events : []
1525
- };
1526
- }
1527
- var STATUS_HUMAN_EVENT_LIMIT = 10;
1528
- function formatRunStatusHuman(run, events) {
1529
- const counters = run.counters ?? {};
1530
- const lines = [
1531
- run.generation_run_id && `run: ${run.generation_run_id}`,
1532
- `status: ${run.status || "unknown"}`,
1533
- run.current_stage && `stage: ${run.current_stage}`,
1534
- run.outcome && `outcome: ${run.outcome}`,
1535
- `processed: ${Number(counters.sessions_processed ?? 0)}`,
1536
- `failed: ${Number(counters.sessions_failed ?? 0)}`,
1537
- Number(counters.distilled_skills_generated ?? 0) > 0 && `distilled: ${Number(counters.distilled_skills_generated ?? 0)}`,
1538
- Number(counters.skills_indexed ?? 0) > 0 && `indexed: ${Number(counters.skills_indexed ?? 0)}`,
1539
- (run.generated_skill_ids?.length ?? 0) > 0 && `skills: ${(run.generated_skill_ids ?? []).join(", ")}`,
1540
- run.error && `error: ${run.error}`,
1541
- run.started_at && `started: ${run.started_at}`,
1542
- run.updated_at && `updated: ${run.updated_at}`
1543
- ].filter(Boolean);
1544
- if (events.length > 0) {
1545
- lines.push("recent events (newest first):");
1546
- for (const event of events.slice(0, STATUS_HUMAN_EVENT_LIMIT)) {
1547
- lines.push(` ${[event.created_at, event.level, event.stage, event.message].filter(Boolean).join(" ")}`);
1548
- }
1549
- }
1550
- return lines.join(`
1551
- `);
1552
- }
1553
- function writeIfChanged(filePath, content) {
1554
- try {
1555
- mkdirSync5(dirname5(filePath), { recursive: true });
1556
- if (existsSync7(filePath)) {
1557
- const current = readFileSync5(filePath, "utf8");
1558
- if (current === content)
1559
- return "unchanged";
1560
- writeFileSync6(filePath, content);
1561
- return "updated";
1562
- }
1563
- writeFileSync6(filePath, content);
1564
- return "wrote";
1565
- } catch {
1566
- return "failed";
1567
- }
1568
- }
1569
- async function pullSkills(options = {}) {
1570
- const status = options.status ?? "approved";
1571
- const cwd = options.cwd ?? process.cwd();
1572
- const log = (msg) => {
1573
- if (!options.silent)
1574
- console.log(msg);
1575
- };
1576
- const fetched = await fetchSkills(status);
1577
- const { skills } = fetched;
1578
- const summary = {
1579
- total: skills.length,
1580
- pruned: 0,
1581
- wrote: 0,
1582
- updated: 0,
1583
- unchanged: 0,
1584
- failed: 0
1585
- };
1586
- if (skills.length === 0) {
1587
- log(" No skills to pull yet.");
1588
- return summary;
1589
- }
1590
- const previousManifest = readSkillsManifest(cwd);
1591
- const preservedIntegrationRecords = (previousManifest?.records ?? []).filter((record) => record.source === SKILL_SOURCE_INTEGRATION_BUNDLED);
1592
- if (previousManifest && previousManifest.managedPaths.length > 0 && previousManifest.releaseKey !== fetched.releaseKey) {
1593
- const integrationPaths = new Set(preservedIntegrationRecords.flatMap((record) => record.paths));
1594
- const harnessPaths = previousManifest.managedPaths.filter((path) => !integrationPaths.has(path));
1595
- if (harnessPaths.length > 0) {
1596
- summary.pruned = pruneManagedSkillPaths(cwd, harnessPaths, log);
1597
- }
1598
- }
1599
- const agents = detectAgents();
1600
- const writeCursor = agents.includes("Cursor") || existsSync7(join7(cwd, ".cursor"));
1601
- const records = [];
1602
- for (const skill of skills) {
1603
- if (!skill.skill_id || !skill.skill_md) {
1604
- summary.failed++;
1605
- continue;
1606
- }
1607
- const safeId = basename(skill.skill_id);
1608
- if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
1609
- summary.failed++;
1610
- log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
1611
- continue;
1612
- }
1613
- const claudePath = join7(cwd, ".claude", "skills", safeId, "SKILL.md");
1614
- const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
1615
- const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
1616
- recordOutcome(summary, claudeStatus, log, claudeRelativePath);
1617
- const paths = [];
1618
- if (claudeStatus !== "failed")
1619
- paths.push(claudeRelativePath);
1620
- if (writeCursor) {
1621
- const cursorPath = join7(cwd, ".cursor", "rules", `${safeId}.mdc`);
1622
- const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
1623
- const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
1624
- recordOutcome(summary, cursorStatus, log, cursorRelativePath);
1625
- if (cursorStatus !== "failed")
1626
- paths.push(cursorRelativePath);
1627
- }
1628
- if (paths.length > 0) {
1629
- records.push({
1630
- skillId: safeId,
1631
- contentHash: sha2562(skill.skill_md),
1632
- source: SKILL_SOURCE_HARNESS,
1633
- paths
1634
- });
1635
- }
1636
- }
1637
- writeSkillsManifest(cwd, manifestHeaderFor(fetched), [...records, ...preservedIntegrationRecords]);
1638
- return summary;
1639
- }
1640
- function recordOutcome(summary, status, log, relativePath) {
1641
- switch (status) {
1642
- case "wrote":
1643
- summary.wrote++;
1644
- log(` wrote ${relativePath}`);
1645
- break;
1646
- case "updated":
1647
- summary.updated++;
1648
- log(` updated ${relativePath}`);
1649
- break;
1650
- case "unchanged":
1651
- summary.unchanged++;
1652
- break;
1653
- case "failed":
1654
- summary.failed++;
1655
- log(` failed ${relativePath}`);
1656
- break;
1657
- }
1658
- }
1659
- var SKILLS_DIR = join7(".claude", "skills");
1660
- var SKILL_FILE = "SKILL.md";
1661
- var MAX_SKILL_WALK_DEPTH = 4;
1662
- async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
1663
- const cwd = options.cwd ?? process.cwd();
1664
- const dryRun = flags["dry-run"] === "true";
1665
- const agentsMd = dryRun ? undefined : upsertFixesAgentsBlock(cwd);
1666
- const skills = discoverLocalSkills(cwd);
1667
- if (skills.length === 0) {
1668
- return { synced: [], dryRun, ...agentsMd ? { agentsMd } : {} };
1669
- }
1670
- const sourceCommit = getGitCommit2(cwd);
1671
- const response = await callControlAPI("/skills/sync", {
1672
- method: "POST",
1673
- body: JSON.stringify({
1674
- skills: skills.map((skill) => ({
1675
- key: skill.key,
1676
- name: skill.name,
1677
- description: skill.description,
1678
- skillMd: skill.skillMd,
1679
- sourcePath: skill.sourcePath,
1680
- contentHash: skill.contentHash,
1681
- sourceCommit
1682
- })),
1683
- sourceCommit,
1684
- dryRun
1685
- })
1686
- }, profileOptions);
1687
- return { synced: response.synced ?? [], dryRun, ...agentsMd ? { agentsMd } : {} };
1688
- }
1689
- function discoverLocalSkills(cwd) {
1690
- const skillsRoot = join7(cwd, SKILLS_DIR);
1691
- if (!existsSync7(skillsRoot))
1692
- return [];
1693
- const managed = new Set(managedSkillPaths(cwd));
1694
- const skills = [];
1695
- for (const absPath of walkSkillFiles(skillsRoot, 0)) {
1696
- const sourcePath = relative3(cwd, absPath).replace(/\\/g, "/");
1697
- if (managed.has(sourcePath))
1698
- continue;
1699
- const key = basename(dirname5(absPath));
1700
- if (key === "moda-cli" || !/^[A-Za-z0-9_-]+$/.test(key))
1701
- continue;
1702
- let content;
1703
- try {
1704
- content = readFileSync5(absPath, "utf8");
1705
- } catch {
1706
- continue;
1707
- }
1708
- if (!content.trim())
1709
- continue;
1710
- const { name, description } = parseSkillFrontmatter(content, key);
1711
- skills.push({
1712
- key,
1713
- name,
1714
- description,
1715
- skillMd: content,
1716
- sourcePath,
1717
- contentHash: sha2562(content)
1718
- });
1719
- }
1720
- return skills.sort((a, b) => a.key.localeCompare(b.key));
1721
- }
1722
- function managedSkillPaths(cwd) {
1723
- const manifest = readSkillsManifest(cwd);
1724
- if (!manifest)
1725
- return [];
1726
- return manifest.managedPaths.filter((path) => path.startsWith(".claude/skills/") && path.endsWith("/SKILL.md"));
1727
- }
1728
- function walkSkillFiles(dir, depth) {
1729
- if (depth > MAX_SKILL_WALK_DEPTH)
1730
- return [];
1731
- let entries;
1732
- try {
1733
- entries = readdirSync3(dir);
1734
- } catch {
1735
- return [];
1736
- }
1737
- const files = [];
1738
- for (const entry of entries) {
1739
- const fullPath = join7(dir, entry);
1740
- let stat;
1741
- try {
1742
- stat = lstatSync2(fullPath);
1743
- } catch {
1744
- continue;
1745
- }
1746
- if (stat.isSymbolicLink())
1747
- continue;
1748
- if (stat.isDirectory()) {
1749
- files.push(...walkSkillFiles(fullPath, depth + 1));
1750
- } else if (stat.isFile() && entry === SKILL_FILE) {
1751
- files.push(fullPath);
1752
- }
1753
- }
1754
- return files;
1755
- }
1756
- function parseSkillFrontmatter(raw, fallbackKey) {
1757
- let name = fallbackKey;
1758
- let description;
1759
- if (raw.startsWith("---")) {
1760
- const end = raw.indexOf(`
1761
- ---`, 3);
1762
- if (end !== -1) {
1763
- const frontmatter = raw.slice(3, end);
1764
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
1765
- const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
1766
- if (nameMatch)
1767
- name = stripQuotes(nameMatch[1].trim()) || fallbackKey;
1768
- if (descMatch)
1769
- description = stripQuotes(descMatch[1].trim()) || undefined;
1770
- }
1771
- }
1772
- return { name, description };
1773
- }
1774
- function stripQuotes(value) {
1775
- return value.replace(/^["']|["']$/g, "");
1776
- }
1777
- function getGitCommit2(cwd) {
1778
- try {
1779
- return execFileSync2("git", ["rev-parse", "HEAD"], {
1780
- cwd,
1781
- encoding: "utf8",
1782
- stdio: ["ignore", "pipe", "ignore"]
1783
- }).trim();
1784
- } catch {
1785
- return;
1786
- }
1787
- }
1788
- async function fetchSkillProposals(flags, profileOptions = {}) {
1789
- const params = new URLSearchParams;
1790
- if (flags["generation-run-id"])
1791
- params.set("generationRunId", flags["generation-run-id"]);
1792
- const status = flags.status ?? "ready_for_pr";
1793
- if (status !== "all")
1794
- params.set("status", status);
1795
- if (flags["tenant-id"])
1796
- params.set("tenantId", flags["tenant-id"]);
1797
- const suffix = params.toString() ? `?${params.toString()}` : "";
1798
- const response = await callControlAPI(`/skills/proposals${suffix}`, { method: "GET" }, profileOptions);
1799
- return Array.isArray(response.proposals) ? response.proposals : [];
1800
- }
1801
- async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
1802
- const cwd = options.cwd ?? process.cwd();
1803
- const params = new URLSearchParams;
1804
- if (flags["tenant-id"])
1805
- params.set("tenantId", flags["tenant-id"]);
1806
- const suffix = params.toString() ? `?${params.toString()}` : "";
1807
- const proposal = await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: "GET" }, profileOptions);
1808
- if (!proposal.skillMd?.trim()) {
1809
- throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
1810
- }
1811
- const safeId = safeSkillId(proposal.skillId);
1812
- const relativePath = `.claude/skills/${safeId}/SKILL.md`;
1813
- const writeStatus = writeIfChanged(join7(cwd, relativePath), proposal.skillMd);
1814
- if (writeStatus === "failed") {
1815
- throw new Error(`Failed to write ${relativePath}`);
1816
- }
1817
- writeSkillProposalManifest(cwd, proposal, relativePath);
1818
- let acknowledged = true;
1819
- let ackError;
1820
- try {
1821
- await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
1822
- method: "POST",
1823
- body: JSON.stringify({ appliedBy: "moda-cli" })
1824
- }, profileOptions);
1825
- } catch (err) {
1826
- acknowledged = false;
1827
- ackError = err instanceof Error ? err.message : String(err);
1828
- }
1829
- return {
1830
- proposalId,
1831
- skillId: proposal.skillId,
1832
- path: relativePath,
1833
- status: writeStatus,
1834
- acknowledged,
1835
- ackError
1836
- };
1837
- }
1838
- function safeSkillId(skillId) {
1839
- const safeId = basename(skillId || "");
1840
- if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
1841
- throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
1842
- }
1843
- return safeId;
1844
- }
1845
- function writeSkillProposalManifest(cwd, proposal, relativePath) {
1846
- const manifestPath = join7(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
1847
- mkdirSync5(dirname5(manifestPath), { recursive: true });
1848
- const existing = existsSync7(manifestPath) ? readFileSync5(manifestPath, "utf8") : `applied_proposals:
1849
- `;
1850
- const entry = [
1851
- ` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
1852
- ` skill_id: "${escapeYamlString(proposal.skillId)}"`,
1853
- ` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
1854
- ` skill_version: ${Number(proposal.skillVersion || 0)}`,
1855
- ` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
1856
- ` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
1857
- ` topic_id: "${escapeYamlString(proposal.topicId)}"`,
1858
- ` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
1859
- ` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
1860
- ` path: "${escapeYamlString(relativePath)}"`,
1861
- ` applied_at: "${new Date().toISOString()}"`
1862
- ].join(`
1863
- `);
1864
- const prefix = existing.endsWith(`
1865
- `) ? existing : `${existing}
1866
- `;
1867
- writeFileSync6(manifestPath, `${prefix}${entry}
1868
- `);
1869
- }
1870
- function escapeYamlString(value) {
1871
- return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1872
- }
1873
- function formatProposalRate(value) {
1874
- return `${(Number(value || 0) * 100).toFixed(1)}%`;
1875
- }
1876
- async function runSkillsCommand(input) {
1877
- const context = "output" in input ? input : createCommandContext({
1878
- command: input.command,
1879
- positional: input.positional,
1880
- positionals: input.positionals,
1881
- flags: input.flags
1882
- }, {
1883
- outputMode: input.flags.agent === "true" ? "agent" : input.flags.json === "true" ? "json" : "human"
1884
- });
1885
- const sub = context.positional;
1886
- const positionals = context.positionals ?? (sub ? [sub] : []);
1887
- const flags = context.flags;
1888
- if (sub === "gen") {
1889
- const result = await generateSkills(flags);
1890
- if (context.outputMode !== "human") {
1891
- context.output.writeData(result, {
1892
- status: Number(result.sessions_failed ?? 0) > 0 ? "error" : "success"
1893
- });
1894
- } else if (result.accepted) {
1895
- console.log([
1896
- "accepted: true",
1897
- result.generation_run_id && `generation_run_id: ${result.generation_run_id}`,
1898
- result.message && `message: ${result.message}`,
1899
- result.generation_run_id && `follow progress: moda skills status ${result.generation_run_id}`
1900
- ].filter(Boolean).join(`
1901
- `));
1902
- } else {
1903
- console.log([
1904
- `processed: ${result.sessions_processed ?? 0}`,
1905
- `failed: ${result.sessions_failed ?? 0}`,
1906
- `pending: ${result.sessions_pending ?? 0}`,
1907
- `pending_found: ${result.pending_sessions_found ?? 0}`,
1908
- `distilled: ${Number(result.cluster_result?.distilled_skills_generated ?? 0)}`,
1909
- `indexed: ${Number(result.cluster_result?.skills_indexed ?? 0)}`,
1910
- result.distillation_skipped && `distillation skipped: ${result.distillation_skip_reason || "unknown"}`
1911
- ].filter(Boolean).join(`
1912
- `));
1913
- }
1914
- return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
1915
- }
1916
- if (sub === "status") {
1917
- const runId = positionals[1] || flags["run-id"] || undefined;
1918
- const { run, events } = await fetchSkillRunStatus(runId);
1919
- if (context.outputMode !== "human") {
1920
- context.output.writeData({ run, events }, { status: run?.status === "failed" ? "error" : "success" });
1921
- } else if (!run) {
1922
- console.log("No skill generation runs found yet. Start one with `moda skills gen`.");
1923
- } else {
1924
- console.log(formatRunStatusHuman(run, events));
1925
- }
1926
- return run?.status === "failed" ? 1 : 0;
1927
- }
1928
- if (sub === "proposals" && positionals[1] === "list") {
1929
- const proposals = await fetchSkillProposals(flags, { profile: context.profile });
1930
- if (flags.json === "true") {
1931
- console.log(JSON.stringify({ proposals }));
1932
- } else if (proposals.length === 0) {
1933
- console.log("No skill proposals found.");
1934
- } else {
1935
- for (const proposal of proposals) {
1936
- console.log([
1937
- proposal.proposalId,
1938
- proposal.proposalStatus,
1939
- `${proposal.skillId}@v${proposal.skillVersion}`,
1940
- `delta=${formatProposalRate(proposal.deltaPassRate)}`,
1941
- `baseline=${formatProposalRate(proposal.baselinePassRate)}`,
1942
- `candidate=${formatProposalRate(proposal.candidatePassRate)}`,
1943
- proposal.name
1944
- ].filter(Boolean).join("\t"));
1945
- }
1946
- }
1947
- return 0;
1948
- }
1949
- if (sub === "proposal" && positionals[1] === "apply") {
1950
- const proposalId = positionals[2] || flags["proposal-id"];
1951
- if (!proposalId) {
1952
- throw new CliInputError("A proposal id is required", "Usage: moda skills proposal apply <proposal-id>");
1953
- }
1954
- const result = await applySkillProposal(proposalId, flags, { profile: context.profile });
1955
- if (flags.json === "true") {
1956
- console.log(JSON.stringify(result));
1957
- } else {
1958
- console.log([
1959
- `${result.status} ${result.path}`,
1960
- result.acknowledged ? "acknowledged: true" : "acknowledged: false"
1961
- ].join(`
1962
- `));
1963
- }
1964
- if (!result.acknowledged) {
1965
- console.error(`Warning: wrote ${result.path} but failed to acknowledge proposal ${proposalId} on the server` + (result.ackError ? ` (${result.ackError})` : "") + ". The proposal is still listed as ready — re-run `moda skills proposal apply` to acknowledge it.");
1966
- return 1;
1967
- }
1968
- return 0;
1969
- }
1970
- if (sub === "sync") {
1971
- const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
1972
- context.output.writeData({
1973
- synced: result.synced,
1974
- dryRun: result.dryRun,
1975
- ...result.agentsMd ? { agentsMd: result.agentsMd } : {}
1976
- });
1977
- return 0;
1978
- }
1979
- if (sub !== "pull") {
1980
- throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : "A skills subcommand is required", "Usage: moda skills gen [--source=all|sdk] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills status [run-id] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]");
1981
- }
1982
- const status = (flags.status ?? "approved").toLowerCase();
1983
- if (!VALID_STATUSES.has(status)) {
1984
- throw new CliInputError(`Invalid --status '${status}'`, `Must be one of: ${[...VALID_STATUSES].join(", ")}`);
1985
- }
1986
- const summary = await pullSkills({ status, silent: context.outputMode !== "human" });
1987
- const parts = [
1988
- summary.pruned && `${summary.pruned} pruned`,
1989
- summary.wrote && `${summary.wrote} written`,
1990
- summary.updated && `${summary.updated} updated`,
1991
- summary.unchanged && `${summary.unchanged} unchanged`,
1992
- summary.failed && `${summary.failed} failed`
1993
- ].filter(Boolean);
1994
- if (context.outputMode === "human" && summary.total > 0) {
1995
- console.log(` Done — ${parts.join(", ") || "no changes"}`);
1996
- }
1997
- if (context.outputMode !== "human") {
1998
- context.output.writeData({ status, summary });
1999
- }
2000
- return summary.failed > 0 ? 1 : 0;
2001
- }
2002
- async function generateSkills(flags) {
2003
- const apiKey = resolveApiKey();
2004
- if (!apiKey) {
2005
- throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
2006
- }
2007
- const tenantId = flags["tenant-id"] || resolveTenantId();
2008
- const directHarnessUrl = flags["harness-url"] || process.env.MODA_SKILL_HARNESS_URL;
2009
- if (directHarnessUrl && !tenantId) {
2010
- throw new CliInputError("No Moda tenant id found. Run `moda init` first (or set MODA_TENANT_ID / --tenant-id).");
2011
- }
2012
- const endpoint = directHarnessUrl ? `${directHarnessUrl.replace(/\/+$/, "")}/skills/generate` : `${resolveIngestUrl().replace(/\/+$/, "")}/v1/skills/generate`;
2013
- const waitForCompletion = flags.wait === "true" || flags["wait-for-completion"] === "true";
2014
- const forceReprocess = flags["force-reprocess"] === "true";
2015
- const source = normalizeSkillSource(flags.source);
2016
- if (source === "coding_agent") {
2017
- console.error("Note: --source=coding-agent is deprecated for skills gen (SDK-only). The run will find no coding-agent sessions; use --source=all or --source=sdk.");
2018
- }
2019
- const body = compactObject({
2020
- tenant_id: tenantId || undefined,
2021
- max_sessions: parsePositiveInt3(flags["max-sessions"], 0),
2022
- lookback_hours: parsePositiveInt3(flags["lookback-hours"], 0),
2023
- start_at: flags["start-at"] || undefined,
2024
- end_at: flags["end-at"] || undefined,
2025
- clustering_run_id: flags["clustering-run-id"] || undefined,
2026
- replay_set_id: flags["replay-set-id"] || flags["eval-set-id"] || undefined,
2027
- run_clustering: flags["no-clustering"] !== "true",
2028
- source,
2029
- clustering_timeout_seconds: parsePositiveInt3(flags["clustering-timeout-seconds"], 7200),
2030
- clustering_poll_seconds: parsePositiveInt3(flags["clustering-poll-seconds"], 15),
2031
- max_cluster_pct: parseOptionalFloat(flags["max-cluster-pct"]),
2032
- reprocess_segments: flags["reprocess-segments"] === "true" || forceReprocess,
2033
- run_replay: flags.replay === "true" && flags["no-replay"] !== "true",
2034
- max_traces: parsePositiveInt3(flags["max-traces"], 0),
2035
- improve_candidates: flags["improve-candidates"] === "true",
2036
- improvement_rounds: parsePositiveInt3(flags["improvement-rounds"], 2),
2037
- force_reprocess: forceReprocess,
2038
- wait_for_completion: waitForCompletion
2039
- });
2040
- const headers = {
2041
- Authorization: `Bearer ${apiKey}`,
2042
- "Content-Type": "application/json"
2043
- };
2044
- if (tenantId)
2045
- headers["X-Moda-Tenant-Id"] = tenantId;
2046
- const response = await fetch(endpoint, {
2047
- method: "POST",
2048
- headers,
2049
- body: JSON.stringify(body)
2050
- });
2051
- if (!response.ok) {
2052
- const text = await response.text().catch(() => "");
2053
- throw new Error(`Failed to generate skills (HTTP ${response.status}): ${text.slice(0, 200)}`);
2054
- }
2055
- return await response.json();
2056
- }
2057
- function parsePositiveInt3(value, fallback) {
2058
- if (!value)
2059
- return fallback;
2060
- const parsed = Number.parseInt(value, 10);
2061
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
2062
- }
2063
- function parseOptionalFloat(value) {
2064
- if (!value)
2065
- return;
2066
- const parsed = Number.parseFloat(value);
2067
- return Number.isFinite(parsed) ? parsed : undefined;
2068
- }
2069
- function compactObject(input) {
2070
- return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
2071
- }
2072
- function normalizeSkillSource(value) {
2073
- const normalized = (value || "all").toLowerCase().replace(/-/g, "_");
2074
- if (["all", "sdk", "coding_agent"].includes(normalized))
2075
- return normalized;
2076
- return "all";
2077
- }
2078
- function releaseKeyFor(release, skills, status) {
2079
- if (release?.generation_run_id)
2080
- return `${status}:run:${release.generation_run_id}`;
2081
- if (Number(release?.generation_version || 0) > 0) {
2082
- return `${status}:version:${Number(release?.generation_version)}`;
2083
- }
2084
- return `${status}:legacy:${sha2562(JSON.stringify(skills.map((skill) => ({
2085
- skill_id: skill.skill_id,
2086
- hash: sha2562(skill.skill_md || "")
2087
- })).sort((a, b) => a.skill_id.localeCompare(b.skill_id))))}`;
2088
- }
2089
- function manifestHeaderFor(fetched) {
2090
- const release = fetched.release;
2091
- return {
2092
- tenantId: fetched.tenantId,
2093
- releaseKey: fetched.releaseKey,
2094
- statusFilter: fetched.status,
2095
- generationRunId: release?.generation_run_id || "",
2096
- generationVersion: Number(release?.generation_version || 0),
2097
- releasedAt: release?.released_at || ""
2098
- };
2099
- }
2100
- function sha2562(input) {
2101
- return createHash2("sha256").update(input).digest("hex");
2102
- }
2103
-
2104
- // src/doctor.ts
2105
- import { createHash as createHash3 } from "node:crypto";
2106
- import { existsSync as existsSync8, readFileSync as readFileSync6, statSync } from "node:fs";
2107
- import { join as join8 } from "node:path";
2108
- async function runDoctorCommand(context) {
2109
- const report = await buildDoctorReport(context, {
2110
- offline: context.flags.offline === "true"
2111
- });
2112
- if (context.outputMode === "human") {
2113
- if (!context.quiet)
2114
- console.log(renderDoctorHuman(report));
2115
- return { exitCode: 0 };
2116
- }
2117
- context.output.writeData(report, {
2118
- status: report.summary.fail > 0 ? "error" : "success",
2119
- ...doctorAgentWriteOptions(report, "doctor")
2120
- });
2121
- return { exitCode: 0 };
2122
- }
2123
- async function runStatusCommand(context) {
2124
- const report = await buildDoctorReport(context, {
2125
- offline: context.flags.offline === "true" || context.flags.online !== "true"
2126
- });
2127
- const topBroken = firstBrokenCheck(report.checks);
2128
- const statusPayload = {
2129
- schemaVersion: "status.v0.1",
2130
- generatedAt: report.generatedAt,
2131
- status: report.summary.status,
2132
- activeProfile: report.profile.active,
2133
- auth: {
2134
- apiKey: resolveApiKey() ? "configured" : "missing"
2135
- },
2136
- workspace: {
2137
- tenantId: resolveTenantId() ?? null,
2138
- baseUrl: resolveModaBaseUrl(),
2139
- ingestUrl: resolveIngestUrl()
2140
- },
2141
- dataFlow: checkStatus(report.checks, "cloud_data_flow"),
2142
- harness: checkStatus(report.checks, "harness_manifest_current"),
2143
- harnessSync: checkStatus(report.checks, "harness_sync_state_present"),
2144
- harnessReport: checkStatus(report.checks, "harness_report_approved_synced"),
2145
- shadowSpark: checkStatus(report.checks, "shadow_spark_workspace_health"),
2146
- prompts: checkStatus(report.checks, "prompts_discovered"),
2147
- skills: checkStatus(report.checks, "skills_discovered"),
2148
- topBrokenCheck: topBroken ? {
2149
- id: topBroken.id,
2150
- status: topBroken.status,
2151
- severity: topBroken.severity,
2152
- nextCommand: topBroken.nextCommand
2153
- } : null,
2154
- nextCommands: report.nextCommands.slice(0, 3)
2155
- };
2156
- if (context.outputMode === "human") {
2157
- if (!context.quiet)
2158
- console.log(renderStatusHuman(statusPayload));
2159
- return { exitCode: 0 };
2160
- }
2161
- context.output.writeData(statusPayload, {
2162
- status: report.summary.fail > 0 ? "error" : "success",
2163
- ...doctorAgentWriteOptions(report, "status")
2164
- });
2165
- return { exitCode: 0 };
2166
- }
2167
- async function buildDoctorReport(context, options) {
2168
- const checks = [];
2169
- const profileConfig = loadProfileConfig({ env: context.env });
2170
- const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
2171
- const paths = getModaPaths({ env: context.env });
2172
- const profileOptions = { profile: context.profile, env: context.env };
2173
- const apiKey = resolveApiKey(profileOptions);
2174
- const tenantId = resolveTenantId(profileOptions);
2175
- const baseUrl = resolveModaBaseUrl(profileOptions);
2176
- const ingestUrl = resolveIngestUrl(profileOptions);
2177
- checks.push({
2178
- id: "active_profile_resolves",
2179
- group: "config",
2180
- title: "Active profile resolves",
2181
- status: activeProfile ? "pass" : "fail",
2182
- severity: activeProfile ? "info" : "critical",
2183
- evidence: [
2184
- { label: "active_profile", value: profileConfig.active_profile },
2185
- { label: "environment", value: activeProfile.environment }
2186
- ],
2187
- remediation: activeProfile ? undefined : "Create or select a Moda profile.",
2188
- nextCommand: activeProfile ? undefined : "moda profiles list"
2189
- });
2190
- checks.push({
2191
- id: "api_key_resolves",
2192
- group: "config",
2193
- title: "API key resolves",
2194
- status: apiKey ? "pass" : "fail",
2195
- severity: apiKey ? "info" : "critical",
2196
- evidence: [{ label: "source", value: apiKey ? "environment/profile/config" : "none" }],
2197
- remediation: apiKey ? undefined : "Authenticate and create a tenant API key.",
2198
- nextCommand: apiKey ? undefined : "moda init"
2199
- });
2200
- checks.push({
2201
- id: "tenant_resolves",
2202
- group: "config",
2203
- title: "Tenant/workspace resolves",
2204
- status: tenantId ? "pass" : "warning",
2205
- severity: tenantId ? "info" : "warning",
2206
- evidence: [{ label: "tenant_id", value: tenantId ? "configured" : "missing" }],
2207
- remediation: tenantId ? undefined : "Select a tenant during init or set MODA_TENANT_ID.",
2208
- nextCommand: tenantId ? undefined : "moda init"
2209
- });
2210
- checks.push(authTokenCheck(context, Boolean(apiKey)));
2211
- checks.push(configPermissionCheck(paths.legacyConfigPath));
2212
- if (activeProfile.api_key_ref) {
2213
- const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
2214
- if (secretPath)
2215
- checks.push(secretPermissionCheck(secretPath));
2216
- checks.push({
2217
- id: "profile_api_key_ref_resolves",
2218
- group: "config",
2219
- title: "Profile API key reference resolves",
2220
- status: resolveSecretReference(activeProfile.api_key_ref) ? "pass" : "warning",
2221
- severity: "warning",
2222
- evidence: [{ label: "api_key_ref", value: activeProfile.api_key_ref }],
2223
- remediation: "Create or update the profile API key reference.",
2224
- nextCommand: "moda profiles create"
2225
- });
2226
- }
2227
- checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
2228
- checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
2229
- checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
2230
- const manifestPath = join8(context.cwd, ".moda", "harness.json");
2231
- const manifestExists = existsSync8(manifestPath);
2232
- checks.push({
2233
- id: "harness_manifest_exists",
2234
- group: "harness",
2235
- title: "Harness manifest exists",
2236
- status: manifestExists ? "pass" : "warning",
2237
- severity: manifestExists ? "info" : "warning",
2238
- evidence: [{ label: "path", value: ".moda/harness.json" }],
2239
- remediation: manifestExists ? undefined : "Run `moda harness scan --summary` to scan the project and write a harness manifest.",
2240
- nextCommand: manifestExists ? undefined : "moda harness scan --summary"
2241
- });
2242
- checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
2243
- checks.push(harnessSyncStateCheck(context.cwd));
2244
- checks.push(harnessReportStateCheck(context.cwd));
2245
- checks.push(discoveryCheck("prompts_discovered", "harness", "Prompts are discovered", context.cwd, [
2246
- "prompts",
2247
- ".moda/prompts.yml"
2248
- ], "moda prompts init"));
2249
- checks.push(discoveryCheck("skills_discovered", "harness", "Skills are discovered", context.cwd, [
2250
- "skills",
2251
- ".claude/skills",
2252
- ".cursor/rules"
2253
- ], "moda skills pull"));
2254
- checks.push(discoveryCheck("evals_discovered", "harness", "Evals/tests are discovered", context.cwd, [
2255
- "evals",
2256
- "tests",
2257
- "__tests__"
2258
- ]));
2259
- checks.push(sdkInitializationCheck(context.cwd));
2260
- checks.push(validationEventPathCheck(ingestUrl));
2261
- if (options.offline || !apiKey) {
2262
- checks.push(...offlineCloudChecks(options.offline));
2263
- } else {
2264
- checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
2265
- }
2266
- const nextCommands = nextCommandsForChecks(checks);
2267
- const summary = summarizeChecks(checks);
2268
- return {
2269
- schemaVersion: "doctor.v0.1",
2270
- generatedAt: new Date().toISOString(),
2271
- profile: {
2272
- active: activeProfile.name,
2273
- requested: context.profile
2274
- },
2275
- summary,
2276
- checks,
2277
- nextCommands,
2278
- fixPlan: checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => ({
2279
- checkId: check.id,
2280
- command: check.nextCommand,
2281
- action: check.remediation ?? "Review this diagnostic."
2282
- }))
2283
- };
2284
- }
2285
- function authTokenCheck(context, hasApiKey) {
2286
- const session = loadAuthSession({
2287
- profile: context.profile,
2288
- env: context.env,
2289
- includeExpired: true
2290
- });
2291
- const valid = isAuthSessionValid(session);
2292
- if (!valid && hasApiKey) {
2293
- return {
2294
- id: "auth_token_valid",
2295
- group: "config",
2296
- title: "Browser auth token is valid",
2297
- status: "skipped",
2298
- severity: "info",
2299
- evidence: [{ label: "reason", value: "api key configured" }]
2300
- };
2301
- }
2302
- return {
2303
- id: "auth_token_valid",
2304
- group: "config",
2305
- title: "Browser auth token is valid",
2306
- status: valid ? "pass" : "warning",
2307
- severity: valid ? "info" : "warning",
2308
- evidence: [
2309
- { label: "token", value: session ? "present" : "missing" },
2310
- ...session ? [{ label: "expires_at", value: new Date(session.expires_at * 1000).toISOString() }] : []
2311
- ],
2312
- remediation: valid ? undefined : "Refresh browser authentication.",
2313
- nextCommand: valid ? undefined : "moda init"
2314
- };
2315
- }
2316
- function configPermissionCheck(path) {
2317
- if (!existsSync8(path)) {
2318
- return {
2319
- id: "config_file_permissions",
2320
- group: "config",
2321
- title: "Config file permissions",
2322
- status: "skipped",
2323
- severity: "info",
2324
- evidence: [{ label: "path", value: path }]
2325
- };
2326
- }
2327
- const permission = ensureSecretFilePermissions(path);
2328
- return {
2329
- id: "config_file_permissions",
2330
- group: "config",
2331
- title: "Config file permissions",
2332
- status: permission.secure ? "pass" : "fail",
2333
- severity: permission.secure ? "info" : "critical",
2334
- evidence: [
2335
- { label: "path", value: path },
2336
- { label: "mode", value: permission.mode ?? "unknown" }
2337
- ],
2338
- remediation: permission.secure ? undefined : "Restrict config file permissions to the current user."
2339
- };
2340
- }
2341
- function secretPermissionCheck(path) {
2342
- const permission = ensureSecretFilePermissions(path);
2343
- return {
2344
- id: "secret_file_permissions",
2345
- group: "config",
2346
- title: "Secret file permissions",
2347
- status: permission.secure ? "pass" : "fail",
2348
- severity: permission.secure ? "info" : "critical",
2349
- evidence: [
2350
- { label: "path", value: path },
2351
- { label: "mode", value: permission.mode ?? "unknown" }
2352
- ],
2353
- remediation: permission.secure ? undefined : "Restrict secret file permissions to the current user."
2354
- };
2355
- }
2356
- function urlCheck(id, group, title, value) {
2357
- let valid = false;
2358
- try {
2359
- const parsed = new URL(value);
2360
- valid = parsed.protocol === "http:" || parsed.protocol === "https:";
2361
- } catch {
2362
- valid = false;
2363
- }
2364
- return {
2365
- id,
2366
- group,
2367
- title,
2368
- status: valid ? "pass" : "fail",
2369
- severity: valid ? "info" : "critical",
2370
- evidence: [{ label: "url", value }],
2371
- remediation: valid ? undefined : "Configure a valid http(s) URL.",
2372
- nextCommand: valid ? undefined : "moda profiles create"
2373
- };
2374
- }
2375
- function localBaseUrlCheck(environment, baseUrl) {
2376
- if (environment !== "local") {
2377
- return {
2378
- id: "local_base_url_frontend",
2379
- group: "config",
2380
- title: "Local base URL targets the frontend",
2381
- status: "skipped",
2382
- severity: "info",
2383
- evidence: [{ label: "environment", value: environment }]
2384
- };
2385
- }
2386
- let port = null;
2387
- let loopback = false;
2388
- try {
2389
- const parsed = new URL(baseUrl);
2390
- loopback = isLoopbackBaseUrl(baseUrl);
2391
- port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
2392
- } catch {
2393
- return {
2394
- id: "local_base_url_frontend",
2395
- group: "config",
2396
- title: "Local base URL targets the frontend",
2397
- status: "fail",
2398
- severity: "critical",
2399
- evidence: [{ label: "base_url", value: baseUrl }],
2400
- remediation: "Set the local profile base URL to the frontend origin (http://localhost:3000).",
2401
- nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000"
2402
- };
2403
- }
2404
- const looksLikeBackend = loopback && (port === "3001" || port === "3002");
2405
- if (looksLikeBackend) {
2406
- return {
2407
- id: "local_base_url_frontend",
2408
- group: "config",
2409
- title: "Local base URL targets the frontend",
2410
- status: "warning",
2411
- severity: "warning",
2412
- evidence: [
2413
- { label: "base_url", value: baseUrl },
2414
- { label: "port", value: port ?? "unknown" }
2415
- ],
2416
- remediation: "Local CLI auth and control APIs are served by the frontend. " + "Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.",
2417
- nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000 --use"
2418
- };
2419
- }
2420
- if (!loopback) {
2421
- return {
2422
- id: "local_base_url_frontend",
2423
- group: "config",
2424
- title: "Local base URL targets the frontend",
2425
- status: "warning",
2426
- severity: "warning",
2427
- evidence: [{ label: "base_url", value: baseUrl }],
2428
- remediation: "Local profile should use a loopback frontend URL (http://localhost:3000). " + "Activate with `moda profiles use local` or set MODA_PROFILE=local.",
2429
- nextCommand: "moda profiles use local"
2430
- };
2431
- }
2432
- return {
2433
- id: "local_base_url_frontend",
2434
- group: "config",
2435
- title: "Local base URL targets the frontend",
2436
- status: "pass",
2437
- severity: "info",
2438
- evidence: [{ label: "base_url", value: baseUrl }]
2439
- };
2440
- }
2441
- function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
2442
- if (!manifestExists) {
2443
- return {
2444
- id: "harness_manifest_current",
2445
- group: "harness",
2446
- title: "Harness manifest matches scanner output",
2447
- status: "skipped",
2448
- severity: "info",
2449
- evidence: [{ label: "reason", value: "manifest missing" }],
2450
- nextCommand: "moda harness scan --summary"
2451
- };
2452
- }
2453
- try {
2454
- const current = scanHarness({
2455
- rootDir: cwd,
2456
- scannerVersion: "doctor",
2457
- tenantId,
2458
- tenantSource: tenantId ? "env_or_config" : undefined
2459
- });
2460
- const saved = JSON.parse(readFileSync6(manifestPath, "utf8"));
2461
- const savedHash = stableGraphHash(saved);
2462
- const currentHash = stableGraphHash(current);
2463
- const matches = savedHash === currentHash;
2464
- return {
2465
- id: "harness_manifest_current",
2466
- group: "harness",
2467
- title: "Harness manifest matches scanner output",
2468
- status: matches ? "pass" : "warning",
2469
- severity: matches ? "info" : "warning",
2470
- evidence: [
2471
- { label: "saved_hash", value: savedHash.slice(0, 12) },
2472
- { label: "current_hash", value: currentHash.slice(0, 12) },
2473
- ...matches ? [] : [{ label: "diff", value: "Manifest hash differs from fresh scanner output." }]
2474
- ],
2475
- remediation: matches ? undefined : "Refresh the local harness manifest.",
2476
- nextCommand: matches ? undefined : "moda harness scan --summary"
2477
- };
2478
- } catch (err) {
2479
- return {
2480
- id: "harness_manifest_current",
2481
- group: "harness",
2482
- title: "Harness manifest matches scanner output",
2483
- status: "fail",
2484
- severity: "critical",
2485
- evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
2486
- remediation: "Regenerate the harness manifest.",
2487
- nextCommand: "moda harness scan --summary"
2488
- };
2489
- }
2490
- }
2491
- function harnessSyncStateCheck(cwd) {
2492
- const state = readHarnessSyncState(cwd);
2493
- if (!state) {
2494
- return {
2495
- id: "harness_sync_state_present",
2496
- group: "harness",
2497
- title: "Harness sync state exists",
2498
- status: "warning",
2499
- severity: "warning",
2500
- evidence: [{ label: "path", value: ".moda/sync-state.json" }],
2501
- remediation: "Sync the harness graph so Moda can correlate local state with cloud state.",
2502
- nextCommand: "moda harness sync --dry-run"
2503
- };
2504
- }
2505
- return {
2506
- id: "harness_sync_state_present",
2507
- group: "harness",
2508
- title: "Harness sync state exists",
2509
- status: "pass",
2510
- severity: "info",
2511
- evidence: [
2512
- { label: "updated_at", value: state.updatedAt },
2513
- { label: "profile", value: state.profile },
2514
- { label: "harness_id", value: state.harnessId },
2515
- { label: "remote_version_id", value: state.remote.versionId || "none" }
2516
- ]
2517
- };
2518
- }
2519
- function harnessReportStateCheck(cwd) {
2520
- const reportPath = join8(cwd, HARNESS_REPORT_PATH);
2521
- if (!existsSync8(reportPath)) {
2522
- return {
2523
- id: "harness_report_approved_synced",
2524
- group: "harness",
2525
- title: "Harness analyst report is approved and synced",
2526
- status: "warning",
2527
- severity: "warning",
2528
- evidence: [{ label: "path", value: HARNESS_REPORT_PATH }],
2529
- remediation: "Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.",
2530
- nextCommand: "moda harness analyze --experimental --summary"
2531
- };
2532
- }
2533
- try {
2534
- const report = readHarnessReport(reportPath);
2535
- const validation = validateHarnessReport(report, cwd);
2536
- const reportHash = hashHarnessReport(report);
2537
- const evidence = [
2538
- { label: "path", value: HARNESS_REPORT_PATH },
2539
- { label: "report_hash", value: reportHash.slice(0, 12) },
2540
- { label: "validation", value: validation.status }
2541
- ];
2542
- if (validation.status !== "pass") {
2543
- return {
2544
- id: "harness_report_approved_synced",
2545
- group: "harness",
2546
- title: "Harness analyst report is approved and synced",
2547
- status: "fail",
2548
- severity: "critical",
2549
- evidence: [
2550
- ...evidence,
2551
- { label: "errors", value: validation.errors.slice(0, 3).join(" | ") || "validation failed" }
2552
- ],
2553
- remediation: "Regenerate or fix the harness report before approving or syncing.",
2554
- nextCommand: "moda harness analyze --experimental --summary"
2555
- };
2556
- }
2557
- const approval = readHarnessReportApproval(cwd);
2558
- const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION && approval.reportId === report.reportId && approval.reportHash === reportHash && approval.approved === true;
2559
- evidence.push({ label: "approval", value: approved ? "approved" : approval ? "stale_or_invalid" : "missing" });
2560
- if (approval && !approved) {
2561
- return {
2562
- id: "harness_report_approved_synced",
2563
- group: "harness",
2564
- title: "Harness analyst report is approved and synced",
2565
- status: "fail",
2566
- severity: "critical",
2567
- evidence,
2568
- remediation: "Approval no longer matches the current report. Review and approve the report again.",
2569
- nextCommand: "moda harness approve --yes"
2570
- };
2571
- }
2572
- if (!approved) {
2573
- return {
2574
- id: "harness_report_approved_synced",
2575
- group: "harness",
2576
- title: "Harness analyst report is approved and synced",
2577
- status: "warning",
2578
- severity: "warning",
2579
- evidence,
2580
- remediation: "Review and approve the validated harness report before syncing.",
2581
- nextCommand: "moda harness approve --yes"
2582
- };
2583
- }
2584
- const state = readHarnessSyncState(cwd);
2585
- const synced = state?.source === "approved_report" && state.report?.reportHash === reportHash;
2586
- evidence.push({ label: "sync", value: synced ? "synced" : "not_synced_to_this_report" }, ...state?.report?.reportHash ? [{ label: "sync_report_hash", value: state.report.reportHash.slice(0, 12) }] : []);
2587
- return {
2588
- id: "harness_report_approved_synced",
2589
- group: "harness",
2590
- title: "Harness analyst report is approved and synced",
2591
- status: synced ? "pass" : "warning",
2592
- severity: synced ? "info" : "warning",
2593
- evidence,
2594
- remediation: synced ? undefined : "Sync the approved harness report so Moda cloud has the same harness graph.",
2595
- nextCommand: synced ? undefined : "moda harness sync --from-report .moda/harness-report.json"
2596
- };
2597
- } catch (err) {
2598
- return {
2599
- id: "harness_report_approved_synced",
2600
- group: "harness",
2601
- title: "Harness analyst report is approved and synced",
2602
- status: "fail",
2603
- severity: "critical",
2604
- evidence: [
2605
- { label: "path", value: HARNESS_REPORT_PATH },
2606
- { label: "error", value: err instanceof Error ? err.message : String(err) }
2607
- ],
2608
- remediation: "Regenerate the harness report.",
2609
- nextCommand: "moda harness analyze --experimental --summary"
2610
- };
2611
- }
2612
- }
2613
- function readHarnessReportApproval(cwd) {
2614
- const approvalPath = join8(cwd, HARNESS_REPORT_APPROVAL_PATH);
2615
- if (!existsSync8(approvalPath))
2616
- return null;
2617
- try {
2618
- return JSON.parse(readFileSync6(approvalPath, "utf8"));
2619
- } catch {
2620
- return {
2621
- schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
2622
- reportId: "unreadable",
2623
- reportHash: "unreadable",
2624
- approved: true,
2625
- approvedAt: "",
2626
- approvedBy: "local-human",
2627
- reportPath: HARNESS_REPORT_PATH
2628
- };
2629
- }
2630
- }
2631
- function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
2632
- const found = relativePaths.filter((path) => existsSync8(join8(cwd, path)));
2633
- return {
2634
- id,
2635
- group,
2636
- title,
2637
- status: found.length > 0 ? "pass" : "warning",
2638
- severity: found.length > 0 ? "info" : "warning",
2639
- evidence: [{ label: "paths", value: found.length > 0 ? found.join(", ") : "none" }],
2640
- remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(", ")}.`,
2641
- nextCommand: found.length > 0 ? undefined : nextCommand
2642
- };
2643
- }
2644
- function sdkInitializationCheck(cwd) {
2645
- const candidates = ["src", "app", "agent_harness"];
2646
- let detected = false;
2647
- for (const candidate of candidates) {
2648
- const path = join8(cwd, candidate);
2649
- if (!existsSync8(path))
2650
- continue;
2651
- try {
2652
- const stat = statSync(path);
2653
- detected ||= stat.isDirectory() || stat.isFile();
2654
- } catch {}
2655
- }
2656
- return {
2657
- id: "sdk_initialization_detected",
2658
- group: "harness",
2659
- title: "SDK initialization is detectable",
2660
- status: detected ? "warning" : "skipped",
2661
- severity: "warning",
2662
- evidence: [{ label: "status", value: detected ? "code present, instrumentation not verified" : "no source candidates" }],
2663
- remediation: "Use the generated integration guide to add Moda SDK instrumentation.",
2664
- nextCommand: "moda init"
2665
- };
2666
- }
2667
- function validationEventPathCheck(ingestUrl) {
2668
- return {
2669
- id: "validation_event_path_defined",
2670
- group: "cloud",
2671
- title: "Validation event path is defined",
2672
- status: "pass",
2673
- severity: "info",
2674
- evidence: [{ label: "path", value: `${ingestUrl.replace(/\/+$/, "")}/v1/otel/validation` }]
2675
- };
2676
- }
2677
- function offlineCloudChecks(isOffline) {
2678
- const status = isOffline ? "skipped" : "warning";
2679
- const evidence = [{ label: "mode", value: isOffline ? "offline" : "missing api key" }];
2680
- const remediation = isOffline ? "Rerun without --offline to check Moda Cloud." : "Configure an API key before checking cloud data flow.";
2681
- const nextCommand = isOffline ? "moda doctor --online" : "moda init";
2682
- return [
2683
- {
2684
- id: "cloud_api_reachable",
2685
- group: "cloud",
2686
- title: "Moda Cloud API is reachable",
2687
- status,
2688
- severity: "warning",
2689
- evidence,
2690
- remediation,
2691
- nextCommand
2692
- },
2693
- {
2694
- id: "ingest_endpoint_reachable",
2695
- group: "cloud",
2696
- title: "Moda ingest endpoint is reachable",
2697
- status,
2698
- severity: "warning",
2699
- evidence,
2700
- remediation,
2701
- nextCommand
2702
- },
2703
- {
2704
- id: "recent_data_flow",
2705
- group: "cloud",
2706
- title: "Recent traces/logs/runs exist",
2707
- status,
2708
- severity: "warning",
2709
- evidence,
2710
- remediation,
2711
- nextCommand
2712
- },
2713
- {
2714
- id: "cloud_data_flow",
2715
- group: "cloud",
2716
- title: "Moda Cloud data flow",
2717
- status,
2718
- severity: "warning",
2719
- evidence,
2720
- remediation,
2721
- nextCommand
2722
- }
2723
- ];
2724
- }
2725
- async function cloudDataFlowChecks(ingestUrl, apiKey) {
2726
- let overview;
2727
- let cloudApiCheck;
2728
- try {
2729
- overview = await callDataAPI("/overview?days_back=1");
2730
- cloudApiCheck = {
2731
- id: "cloud_api_reachable",
2732
- group: "cloud",
2733
- title: "Moda Cloud API is reachable",
2734
- status: "pass",
2735
- severity: "info",
2736
- evidence: [{ label: "overview", value: "reachable" }]
2737
- };
2738
- } catch (err) {
2739
- const error = err instanceof Error ? err.message : String(err);
2740
- return [
2741
- {
2742
- id: "cloud_api_reachable",
2743
- group: "cloud",
2744
- title: "Moda Cloud API is reachable",
2745
- status: "warning",
2746
- severity: "warning",
2747
- evidence: [{ label: "error", value: error }],
2748
- remediation: "Check API key and base URL.",
2749
- nextCommand: "moda doctor --offline"
2750
- },
2751
- {
2752
- id: "ingest_endpoint_reachable",
2753
- group: "cloud",
2754
- title: "Moda ingest endpoint is reachable",
2755
- status: "skipped",
2756
- severity: "warning",
2757
- evidence: [{ label: "reason", value: "cloud API unreachable" }]
2758
- },
2759
- {
2760
- id: "recent_data_flow",
2761
- group: "cloud",
2762
- title: "Recent traces/logs/runs exist",
2763
- status: "skipped",
2764
- severity: "warning",
2765
- evidence: [{ label: "reason", value: "overview unavailable" }]
2766
- },
2767
- {
2768
- id: "cloud_data_flow",
2769
- group: "cloud",
2770
- title: "Moda Cloud data flow",
2771
- status: "warning",
2772
- severity: "warning",
2773
- evidence: [{ label: "error", value: error }],
2774
- remediation: "Check API key, base URL, and whether data has reached Moda.",
2775
- nextCommand: "moda doctor --offline"
2776
- }
2777
- ];
2778
- }
2779
- const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
2780
- const recentCheck = recentDataFlowCheck(overview);
2781
- const cloudStatus = cloudApiCheck.status === "pass" && ingestCheck.status === "pass" && recentCheck.status === "pass" ? "pass" : "warning";
2782
- return [
2783
- cloudApiCheck,
2784
- ingestCheck,
2785
- recentCheck,
2786
- {
2787
- id: "cloud_data_flow",
2788
- group: "cloud",
2789
- title: "Moda Cloud data flow",
2790
- status: cloudStatus,
2791
- severity: cloudStatus === "pass" ? "info" : "warning",
2792
- evidence: [
2793
- { label: "cloud_api", value: cloudApiCheck.status },
2794
- { label: "ingest", value: ingestCheck.status },
2795
- { label: "recent_data", value: recentCheck.status }
2796
- ],
2797
- remediation: cloudStatus === "pass" ? undefined : "Check API key, ingest URL, and whether data has reached Moda.",
2798
- nextCommand: cloudStatus === "pass" ? undefined : "moda doctor --offline"
2799
- }
2800
- ];
2801
- }
2802
- async function ingestReachabilityCheck(ingestUrl, apiKey) {
2803
- const url = `${ingestUrl.replace(/\/+$/, "")}/`;
2804
- try {
2805
- const response = await fetch(url, {
2806
- method: "GET",
2807
- headers: { Authorization: `Bearer ${apiKey}` },
2808
- signal: AbortSignal.timeout(1e4)
2809
- });
2810
- return {
2811
- id: "ingest_endpoint_reachable",
2812
- group: "cloud",
2813
- title: "Moda ingest endpoint is reachable",
2814
- status: "pass",
2815
- severity: "info",
2816
- evidence: [
2817
- { label: "url", value: url },
2818
- { label: "http_status", value: String(response.status) }
2819
- ]
2820
- };
2821
- } catch (err) {
2822
- return {
2823
- id: "ingest_endpoint_reachable",
2824
- group: "cloud",
2825
- title: "Moda ingest endpoint is reachable",
2826
- status: "warning",
2827
- severity: "warning",
2828
- evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
2829
- remediation: "Check the configured ingest URL and network reachability.",
2830
- nextCommand: "moda profiles create"
2831
- };
2832
- }
2833
- }
2834
- function recentDataFlowCheck(overview) {
2835
- const data = isRecord(overview) ? overview : {};
2836
- const conversations = numberAt(data, ["conversations", "total"]);
2837
- const toolFailures = numberAt(data, ["tool_failures", "total"]);
2838
- const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
2839
- const lastEventAt = lastActivityTimestamp(recentActivity);
2840
- const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
2841
- return {
2842
- id: "recent_data_flow",
2843
- group: "cloud",
2844
- title: "Recent traces/logs/runs exist",
2845
- status: hasData ? "pass" : "warning",
2846
- severity: hasData ? "info" : "warning",
2847
- evidence: [
2848
- { label: "conversations", value: String(conversations) },
2849
- { label: "tool_failures", value: String(toolFailures) },
2850
- { label: "last_event_at", value: lastEventAt ?? "none" }
2851
- ],
2852
- remediation: hasData ? undefined : "No recent Moda data was visible in the overview window.",
2853
- nextCommand: hasData ? undefined : "moda hooks install --agent=all"
2854
- };
2855
- }
2856
- function numberAt(value, path) {
2857
- let cursor = value;
2858
- for (const key of path) {
2859
- if (!isRecord(cursor))
2860
- return 0;
2861
- cursor = cursor[key];
2862
- }
2863
- return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
2864
- }
2865
- function lastActivityTimestamp(items) {
2866
- const timestamps = items.map((item) => isRecord(item) && typeof item.timestamp === "string" ? item.timestamp : undefined).filter((item) => Boolean(item)).sort();
2867
- return timestamps.at(-1);
2868
- }
2869
- function summarizeChecks(checks) {
2870
- const fail = checks.filter((check) => check.status === "fail").length;
2871
- const warning = checks.filter((check) => check.status === "warning").length;
2872
- const skipped = checks.filter((check) => check.status === "skipped").length;
2873
- const pass = checks.filter((check) => check.status === "pass").length;
2874
- return {
2875
- status: fail > 0 ? "fail" : warning > 0 ? "warning" : "pass",
2876
- pass,
2877
- warning,
2878
- fail,
2879
- skipped
2880
- };
2881
- }
2882
- function nextCommandsForChecks(checks) {
2883
- return [...new Set(checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => check.nextCommand).filter((command) => Boolean(command)))];
2884
- }
2885
- function firstBrokenCheck(checks) {
2886
- return checks.find((check) => check.status === "fail") ?? checks.find((check) => check.status === "warning");
2887
- }
2888
- function checkStatus(checks, id) {
2889
- return checks.find((check) => check.id === id)?.status ?? "skipped";
2890
- }
2891
- function doctorAgentWriteOptions(report, command) {
2892
- const broken = report.checks.filter((check) => check.status === "fail" || check.status === "warning");
2893
- return {
2894
- summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
2895
- findings: broken.slice(0, 16).map((check) => ({
2896
- id: check.id,
2897
- title: check.title,
2898
- status: check.status,
2899
- severity: check.severity,
2900
- description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
2901
- evidence_ref_ids: [`doctor:${check.id}`]
2902
- })),
2903
- evidenceRefs: report.checks.map((check) => ({
2904
- id: `doctor:${check.id}`,
2905
- kind: "doctor_check",
2906
- label: check.title,
2907
- status: check.status
2908
- })),
2909
- recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
2910
- id: `doctor_action_${index + 1}_${item.checkId}`,
2911
- title: item.action,
2912
- command: item.command,
2913
- rationale: `Doctor check ${item.checkId} is not passing.`,
2914
- mutability: item.command ? agentMutabilityForCommand(item.command) : "unknown",
2915
- requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true
2916
- })),
2917
- nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
2918
- command: nextCommand,
2919
- purpose: agentPurposeForCommand(nextCommand),
2920
- mutability: agentMutabilityForCommand(nextCommand),
2921
- requires_approval: agentCommandRequiresApproval(nextCommand)
2922
- })),
2923
- warnings: broken.filter((check) => check.status === "warning").slice(0, 16).map((check) => `${check.title}: ${check.remediation ?? check.status}`)
2924
- };
2925
- }
2926
- function agentMutabilityForCommand(command) {
2927
- if (/\bsync\b/.test(command))
2928
- return "sync";
2929
- if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
2930
- return "write";
2931
- return "read";
2932
- }
2933
- function agentCommandRequiresApproval(command) {
2934
- return agentMutabilityForCommand(command) !== "read" && !command.includes("--dry-run");
2935
- }
2936
- function agentPurposeForCommand(command) {
2937
- if (command.includes("doctor"))
2938
- return "Re-run Moda diagnostics.";
2939
- if (command.includes("harness approve"))
2940
- return "Approve the validated harness report after review.";
2941
- if (command.includes("harness sync"))
2942
- return "Sync harness state to Moda Cloud.";
2943
- if (command.includes("harness scan"))
2944
- return "Refresh the local harness manifest.";
2945
- if (command.includes("init"))
2946
- return "Run Moda onboarding.";
2947
- return "Inspect or repair Moda setup.";
2948
- }
2949
- function stableGraphHash(graph) {
2950
- return createHash3("sha256").update(JSON.stringify(sortJson2({
2951
- ...graph,
2952
- scan: {
2953
- ...graph.scan,
2954
- createdAt: undefined,
2955
- scannerVersion: undefined,
2956
- skipped: undefined
2957
- }
2958
- }))).digest("hex");
2959
- }
2960
- function sortJson2(value) {
2961
- if (Array.isArray(value))
2962
- return value.map((item) => sortJson2(item));
2963
- if (value && typeof value === "object") {
2964
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson2(item)]));
2965
- }
2966
- return value;
2967
- }
2968
- function isRecord(value) {
2969
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2970
- }
2971
-
2972
- export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, packageAssetPath, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, upsertSkillManifestRecord, runSkillSync, runSkillsCommand, runDoctorCommand, runStatusCommand, buildDoctorReport };