@moda-ai/cli 1.55.10 → 1.55.12

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.
@@ -6,55 +6,34 @@ import {
6
6
  $isSeq,
7
7
  $parseDocument,
8
8
  $stringify,
9
- assertFlags,
10
- assertPositionals,
11
9
  backfillPromptSource,
12
- callControlAPI,
13
- callDataAPI,
14
10
  inventoryFiles,
15
11
  normalizeHarnessReport,
16
- readHarnessSyncState,
17
- renderDoctorHuman,
18
- renderStatusHuman,
19
12
  require_typescript,
20
13
  resolvePromptSource,
21
- safePromptEvidencePath,
22
- scanHarness,
23
- setPlatformTenantOverride
24
- } from "./cli-v27q5dvh.js";
14
+ safePromptEvidencePath
15
+ } from "./cli-m0fa54ny.js";
25
16
  import {
26
- SKILL_SOURCE_HARNESS,
27
- SKILL_SOURCE_INTEGRATION_BUNDLED,
28
- claudeSkillToCursorRule,
29
- detectAgents,
30
- pruneManagedSkillPaths,
31
- readSkillsManifest,
32
- writeSkillsManifest
33
- } from "./cli-ssfq84k5.js";
17
+ createCommandContext
18
+ } from "./cli-4x9w6ey7.js";
34
19
  import {
35
- isAuthSessionValid,
36
- loadAuthSession
37
- } from "./cli-4f0p8erk.js";
20
+ assertFlags,
21
+ assertPositionals
22
+ } from "./cli-k7vwt56h.js";
23
+ import {
24
+ callControlAPI,
25
+ callDataAPI
26
+ } from "./cli-ryd7nzke.js";
27
+ import {
28
+ setPlatformTenantOverride
29
+ } from "./cli-09rr5smg.js";
38
30
  import {
39
31
  CliInputError,
40
32
  EXIT_ERROR,
41
33
  EXIT_OK,
42
- createCommandContext,
43
- ensureSecretFilePermissions,
44
- getActiveProfile,
45
- getModaPaths,
46
- isLoopbackBaseUrl,
47
- loadProfileConfig,
48
- recordApiRequest,
49
- resolveApiKey,
50
34
  resolveApiTenantId,
51
- resolveIngestUrl,
52
- resolveModaBaseUrl,
53
- resolveSecretReference,
54
- resolveTenantId,
55
- secretFilePathFromRef,
56
35
  validateConfig
57
- } from "./cli-0qnghzg4.js";
36
+ } from "./cli-kwz900s4.js";
58
37
  import {
59
38
  __commonJS,
60
39
  __require,
@@ -6752,7 +6731,7 @@ async function runPromptSyncExclusive(flags, profileOptions, options) {
6752
6731
  const analyzedDefinitions = new Map;
6753
6732
  if (flags["no-analyze"] !== "true") {
6754
6733
  options.onProgress?.("Analyzing prompt sources before sync.");
6755
- const { analyzeHarnessPrompts } = await import("./harness-0tnqw849.js");
6734
+ const { analyzeHarnessPrompts } = await import("./harness-9psndqtm.js");
6756
6735
  const result = await analyzeHarnessPrompts({
6757
6736
  rootDir: process.cwd(),
6758
6737
  existing: established.flatMap((prompt) => prompt.sourceDefinition ? [prompt.sourceDefinition.source] : []),
@@ -7082,7 +7061,7 @@ async function migratePromptManifest(options = {}) {
7082
7061
  let addedSources = [];
7083
7062
  let pendingSources = [];
7084
7063
  if (options.discover) {
7085
- const { analyzeHarnessPrompts } = await import("./harness-0tnqw849.js");
7064
+ const { analyzeHarnessPrompts } = await import("./harness-9psndqtm.js");
7086
7065
  const result = await analyzeHarnessPrompts({
7087
7066
  rootDir: root,
7088
7067
  existing: manifest.sources,
@@ -7527,1698 +7506,4 @@ function asRecordValue(value) {
7527
7506
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
7528
7507
  }
7529
7508
 
7530
- // src/skills.ts
7531
- import { execFileSync as execFileSync2 } from "node:child_process";
7532
- import { createHash as createHash2 } from "node:crypto";
7533
- import {
7534
- existsSync as existsSync4,
7535
- lstatSync as lstatSync2,
7536
- mkdirSync as mkdirSync3,
7537
- readdirSync,
7538
- readFileSync as readFileSync3,
7539
- writeFileSync as writeFileSync4
7540
- } from "node:fs";
7541
- import { basename, dirname as dirname4, join as join3, relative as relative2 } from "node:path";
7542
-
7543
- // src/agents-md.ts
7544
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
7545
- import { dirname as dirname3, join as join2 } from "node:path";
7546
- var AGENTS_MD_FILENAME = "AGENTS.md";
7547
- var FIXES_BLOCK_BEGIN = "<!-- moda:fixes:begin -->";
7548
- var FIXES_BLOCK_END = "<!-- moda:fixes:end -->";
7549
- function renderFixesBlock() {
7550
- return [
7551
- FIXES_BLOCK_BEGIN,
7552
- "## Fixing production issues with Moda (managed block — edits inside are overwritten by moda-cli)",
7553
- "",
7554
- "Moda supplies the production evidence; you propose and make the fix yourself.",
7555
- "",
7556
- "- Find what to fix: `moda investigate` ranks tool failures, user frustrations,",
7557
- " and top Problems with evidence; `moda problems` ranks cross-signal Problems",
7558
- " by root cause; `moda failures` lists tool failures only.",
7559
- "- Read the evidence before editing: `moda problem <problem_id>` (dossier),",
7560
- " `moda problem <problem_id> --evidence` (attribution rationales and anchors),",
7561
- " `moda problem <problem_id> --traces` (affected traces),",
7562
- " `moda tool-failure-detail <tool_name>` (failing calls in context), and",
7563
- " `moda context <conversation_id>` (the messages around a turn; `--all` for",
7564
- " the whole trace).",
7565
- "- Propose the change in the code, prompt, tool definition, or skill that",
7566
- " causes the issue, and cite the Moda problem or trace ids it is based on",
7567
- " in the PR description.",
7568
- "- Prompts live in `.moda/prompts.yml` (version 2: `prompt_paths` globs plus",
7569
- " `sources:` mappings for prompts defined in code); after `moda prompts sync`,",
7570
- " pass `prompt: lock.prompts['<key>']` to createThinkTelemetry/createAI7Telemetry.",
7571
- "- After the fix ships, record it with",
7572
- " `moda problem-feedback <problem_id> --action=mark_fixed`; if the issue",
7573
- " is detected again, the Problem comes back.",
7574
- FIXES_BLOCK_END
7575
- ].join(`
7576
- `);
7577
- }
7578
- function upsertManagedBlock(existing, block) {
7579
- if (existing === null) {
7580
- return `${block}
7581
- `;
7582
- }
7583
- const beginIdx = existing.indexOf(FIXES_BLOCK_BEGIN);
7584
- const endIdx = existing.indexOf(FIXES_BLOCK_END);
7585
- if (beginIdx !== -1 && endIdx !== -1 && endIdx >= beginIdx) {
7586
- const before = existing.slice(0, beginIdx);
7587
- const after = existing.slice(endIdx + FIXES_BLOCK_END.length);
7588
- return `${before}${block}${after}`;
7589
- }
7590
- const separator = existing === "" || existing.endsWith(`
7591
-
7592
- `) ? "" : existing.endsWith(`
7593
- `) ? `
7594
- ` : `
7595
-
7596
- `;
7597
- return `${existing}${separator}${block}
7598
- `;
7599
- }
7600
- function writeFileIfChanged(filePath, content) {
7601
- try {
7602
- mkdirSync2(dirname3(filePath), { recursive: true });
7603
- if (existsSync3(filePath)) {
7604
- const current = readFileSync2(filePath, "utf8");
7605
- if (current === content)
7606
- return "unchanged";
7607
- writeFileSync3(filePath, content);
7608
- return "updated";
7609
- }
7610
- writeFileSync3(filePath, content);
7611
- return "wrote";
7612
- } catch {
7613
- return "failed";
7614
- }
7615
- }
7616
- function upsertFixesAgentsBlock(cwd) {
7617
- const filePath = join2(cwd, AGENTS_MD_FILENAME);
7618
- let existing = null;
7619
- if (existsSync3(filePath)) {
7620
- try {
7621
- existing = readFileSync2(filePath, "utf8");
7622
- } catch {
7623
- return { path: AGENTS_MD_FILENAME, status: "failed" };
7624
- }
7625
- }
7626
- const next = upsertManagedBlock(existing, renderFixesBlock());
7627
- return { path: AGENTS_MD_FILENAME, status: writeFileIfChanged(filePath, next) };
7628
- }
7629
-
7630
- // src/skills.ts
7631
- var SKILLS_REQUEST_TIMEOUT_MS = 15000;
7632
- var VALID_STATUSES = new Set(["approved", "proposed", "all"]);
7633
- var SKILL_PROPOSALS_MANIFEST_PATH = join3(".moda", "skill-proposals.yml");
7634
- async function fetchSkills(status) {
7635
- const apiKey = resolveApiKey();
7636
- if (!apiKey) {
7637
- throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
7638
- }
7639
- const ingestUrl = resolveIngestUrl().replace(/\/$/, "");
7640
- const url = `${ingestUrl}/v1/skills?status=${encodeURIComponent(status)}`;
7641
- const controller = new AbortController;
7642
- const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
7643
- let response;
7644
- try {
7645
- recordApiRequest();
7646
- response = await fetch(url, {
7647
- headers: { Authorization: `Bearer ${apiKey}` },
7648
- signal: controller.signal
7649
- });
7650
- } catch (err) {
7651
- const message = err instanceof Error ? err.message : String(err);
7652
- throw new Error(`Network error: failed to reach ${ingestUrl} (${message})`);
7653
- } finally {
7654
- clearTimeout(timeout);
7655
- }
7656
- if (!response.ok) {
7657
- const body = await response.text().catch(() => "");
7658
- throw new Error(`Failed to fetch skills (HTTP ${response.status}): ${body.slice(0, 200)}`);
7659
- }
7660
- const data = await response.json();
7661
- if (!data.success) {
7662
- throw new Error(data.message || "Failed to fetch skills");
7663
- }
7664
- const skills = Array.isArray(data.skills) ? data.skills : [];
7665
- const release = data.release && typeof data.release === "object" ? data.release : null;
7666
- return {
7667
- skills,
7668
- tenantId: data.tenant_id || resolveTenantId() || "",
7669
- release,
7670
- releaseKey: releaseKeyFor(release, skills, status),
7671
- status
7672
- };
7673
- }
7674
- async function skillRunsRequest(url) {
7675
- const apiKey = resolveApiKey();
7676
- if (!apiKey) {
7677
- throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
7678
- }
7679
- const controller = new AbortController;
7680
- const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
7681
- let response;
7682
- try {
7683
- recordApiRequest();
7684
- response = await fetch(url, {
7685
- headers: { Authorization: `Bearer ${apiKey}` },
7686
- signal: controller.signal
7687
- });
7688
- } catch (err) {
7689
- const message = err instanceof Error ? err.message : String(err);
7690
- throw new Error(`Network error: failed to reach ${url} (${message})`);
7691
- } finally {
7692
- clearTimeout(timeout);
7693
- }
7694
- const text = await response.text().catch(() => "");
7695
- if (!response.ok && response.status !== 404) {
7696
- throw new Error(`Failed to fetch run status (HTTP ${response.status}): ${text.slice(0, 200)}`);
7697
- }
7698
- let body = {};
7699
- try {
7700
- body = text ? JSON.parse(text) : {};
7701
- } catch {
7702
- body = {};
7703
- }
7704
- return { status: response.status, body };
7705
- }
7706
- async function fetchSkillRunStatus(runId) {
7707
- const ingestUrl = resolveIngestUrl().replace(/\/$/, "");
7708
- let resolvedRunId = runId;
7709
- if (!resolvedRunId) {
7710
- const list = await skillRunsRequest(`${ingestUrl}/v1/skills/runs?limit=1`);
7711
- const listBody = list.body;
7712
- const latest = Array.isArray(listBody.runs) ? listBody.runs[0] : undefined;
7713
- if (!latest)
7714
- return { run: null, events: [] };
7715
- resolvedRunId = latest.generation_run_id;
7716
- if (!resolvedRunId)
7717
- return { run: latest, events: [] };
7718
- }
7719
- const detail = await skillRunsRequest(`${ingestUrl}/v1/skills/runs/${encodeURIComponent(resolvedRunId)}`);
7720
- if (detail.status === 404) {
7721
- if (runId) {
7722
- throw new CliInputError(`No skill generation run found with id '${runId}'`);
7723
- }
7724
- return { run: null, events: [] };
7725
- }
7726
- const detailBody = detail.body;
7727
- return {
7728
- run: detailBody.run ?? null,
7729
- events: Array.isArray(detailBody.events) ? detailBody.events : []
7730
- };
7731
- }
7732
- var STATUS_HUMAN_EVENT_LIMIT = 10;
7733
- function formatRunStatusHuman(run, events) {
7734
- const counters = run.counters ?? {};
7735
- const lines = [
7736
- run.generation_run_id && `run: ${run.generation_run_id}`,
7737
- `status: ${run.status || "unknown"}`,
7738
- run.current_stage && `stage: ${run.current_stage}`,
7739
- run.outcome && `outcome: ${run.outcome}`,
7740
- `processed: ${Number(counters.sessions_processed ?? 0)}`,
7741
- `failed: ${Number(counters.sessions_failed ?? 0)}`,
7742
- Number(counters.distilled_skills_generated ?? 0) > 0 && `distilled: ${Number(counters.distilled_skills_generated ?? 0)}`,
7743
- Number(counters.skills_indexed ?? 0) > 0 && `indexed: ${Number(counters.skills_indexed ?? 0)}`,
7744
- (run.generated_skill_ids?.length ?? 0) > 0 && `skills: ${(run.generated_skill_ids ?? []).join(", ")}`,
7745
- run.error && `error: ${run.error}`,
7746
- run.started_at && `started: ${run.started_at}`,
7747
- run.updated_at && `updated: ${run.updated_at}`
7748
- ].filter(Boolean);
7749
- if (events.length > 0) {
7750
- lines.push("recent events (newest first):");
7751
- for (const event of events.slice(0, STATUS_HUMAN_EVENT_LIMIT)) {
7752
- lines.push(` ${[event.created_at, event.level, event.stage, event.message].filter(Boolean).join(" ")}`);
7753
- }
7754
- }
7755
- return lines.join(`
7756
- `);
7757
- }
7758
- function writeIfChanged(filePath, content) {
7759
- try {
7760
- mkdirSync3(dirname4(filePath), { recursive: true });
7761
- if (existsSync4(filePath)) {
7762
- const current = readFileSync3(filePath, "utf8");
7763
- if (current === content)
7764
- return "unchanged";
7765
- writeFileSync4(filePath, content);
7766
- return "updated";
7767
- }
7768
- writeFileSync4(filePath, content);
7769
- return "wrote";
7770
- } catch {
7771
- return "failed";
7772
- }
7773
- }
7774
- async function pullSkills(options = {}) {
7775
- const status = options.status ?? "approved";
7776
- const cwd = options.cwd ?? process.cwd();
7777
- const log = (msg) => {
7778
- if (!options.silent)
7779
- console.log(msg);
7780
- };
7781
- const fetched = await fetchSkills(status);
7782
- const { skills } = fetched;
7783
- const summary = {
7784
- total: skills.length,
7785
- pruned: 0,
7786
- wrote: 0,
7787
- updated: 0,
7788
- unchanged: 0,
7789
- failed: 0
7790
- };
7791
- if (skills.length === 0) {
7792
- log(" No skills to pull yet.");
7793
- return summary;
7794
- }
7795
- const previousManifest = readSkillsManifest(cwd);
7796
- const preservedIntegrationRecords = (previousManifest?.records ?? []).filter((record) => record.source === SKILL_SOURCE_INTEGRATION_BUNDLED);
7797
- if (previousManifest && previousManifest.managedPaths.length > 0 && previousManifest.releaseKey !== fetched.releaseKey) {
7798
- const integrationPaths = new Set(preservedIntegrationRecords.flatMap((record) => record.paths));
7799
- const harnessPaths = previousManifest.managedPaths.filter((path) => !integrationPaths.has(path));
7800
- if (harnessPaths.length > 0) {
7801
- summary.pruned = pruneManagedSkillPaths(cwd, harnessPaths, log);
7802
- }
7803
- }
7804
- const agents = detectAgents();
7805
- const writeCursor = agents.includes("Cursor") || existsSync4(join3(cwd, ".cursor"));
7806
- const records = [];
7807
- for (const skill of skills) {
7808
- if (!skill.skill_id || !skill.skill_md) {
7809
- summary.failed++;
7810
- continue;
7811
- }
7812
- const safeId = basename(skill.skill_id);
7813
- if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
7814
- summary.failed++;
7815
- log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
7816
- continue;
7817
- }
7818
- const claudePath = join3(cwd, ".claude", "skills", safeId, "SKILL.md");
7819
- const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
7820
- const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
7821
- recordOutcome(summary, claudeStatus, log, claudeRelativePath);
7822
- const paths = [];
7823
- if (claudeStatus !== "failed")
7824
- paths.push(claudeRelativePath);
7825
- if (writeCursor) {
7826
- const cursorPath = join3(cwd, ".cursor", "rules", `${safeId}.mdc`);
7827
- const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
7828
- const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
7829
- recordOutcome(summary, cursorStatus, log, cursorRelativePath);
7830
- if (cursorStatus !== "failed")
7831
- paths.push(cursorRelativePath);
7832
- }
7833
- if (paths.length > 0) {
7834
- records.push({
7835
- skillId: safeId,
7836
- contentHash: sha2562(skill.skill_md),
7837
- source: SKILL_SOURCE_HARNESS,
7838
- paths
7839
- });
7840
- }
7841
- }
7842
- writeSkillsManifest(cwd, manifestHeaderFor(fetched), [...records, ...preservedIntegrationRecords]);
7843
- return summary;
7844
- }
7845
- function recordOutcome(summary, status, log, relativePath) {
7846
- switch (status) {
7847
- case "wrote":
7848
- summary.wrote++;
7849
- log(` wrote ${relativePath}`);
7850
- break;
7851
- case "updated":
7852
- summary.updated++;
7853
- log(` updated ${relativePath}`);
7854
- break;
7855
- case "unchanged":
7856
- summary.unchanged++;
7857
- break;
7858
- case "failed":
7859
- summary.failed++;
7860
- log(` failed ${relativePath}`);
7861
- break;
7862
- }
7863
- }
7864
- var SKILLS_DIR = join3(".claude", "skills");
7865
- var SKILL_FILE = "SKILL.md";
7866
- var MAX_SKILL_WALK_DEPTH = 4;
7867
- async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
7868
- const cwd = options.cwd ?? process.cwd();
7869
- const dryRun = flags["dry-run"] === "true";
7870
- const agentsMd = dryRun ? undefined : upsertFixesAgentsBlock(cwd);
7871
- const skills = discoverLocalSkills(cwd);
7872
- if (skills.length === 0) {
7873
- return { synced: [], dryRun, ...agentsMd ? { agentsMd } : {} };
7874
- }
7875
- const sourceCommit = getGitCommit2(cwd);
7876
- const response = await callControlAPI("/skills/sync", {
7877
- method: "POST",
7878
- body: JSON.stringify({
7879
- skills: skills.map((skill) => ({
7880
- key: skill.key,
7881
- name: skill.name,
7882
- description: skill.description,
7883
- skillMd: skill.skillMd,
7884
- sourcePath: skill.sourcePath,
7885
- contentHash: skill.contentHash,
7886
- sourceCommit
7887
- })),
7888
- sourceCommit,
7889
- dryRun
7890
- })
7891
- }, profileOptions);
7892
- return { synced: response.synced ?? [], dryRun, ...agentsMd ? { agentsMd } : {} };
7893
- }
7894
- function discoverLocalSkills(cwd) {
7895
- const skillsRoot = join3(cwd, SKILLS_DIR);
7896
- if (!existsSync4(skillsRoot))
7897
- return [];
7898
- const managed = new Set(managedSkillPaths(cwd));
7899
- const skills = [];
7900
- for (const absPath of walkSkillFiles(skillsRoot, 0)) {
7901
- const sourcePath = relative2(cwd, absPath).replace(/\\/g, "/");
7902
- if (managed.has(sourcePath))
7903
- continue;
7904
- const key = basename(dirname4(absPath));
7905
- if (key === "moda-cli" || !/^[A-Za-z0-9_-]+$/.test(key))
7906
- continue;
7907
- let content;
7908
- try {
7909
- content = readFileSync3(absPath, "utf8");
7910
- } catch {
7911
- continue;
7912
- }
7913
- if (!content.trim())
7914
- continue;
7915
- const { name, description } = parseSkillFrontmatter(content, key);
7916
- skills.push({
7917
- key,
7918
- name,
7919
- description,
7920
- skillMd: content,
7921
- sourcePath,
7922
- contentHash: sha2562(content)
7923
- });
7924
- }
7925
- return skills.sort((a, b) => a.key.localeCompare(b.key));
7926
- }
7927
- function managedSkillPaths(cwd) {
7928
- const manifest = readSkillsManifest(cwd);
7929
- if (!manifest)
7930
- return [];
7931
- return manifest.managedPaths.filter((path) => path.startsWith(".claude/skills/") && path.endsWith("/SKILL.md"));
7932
- }
7933
- function walkSkillFiles(dir, depth) {
7934
- if (depth > MAX_SKILL_WALK_DEPTH)
7935
- return [];
7936
- let entries;
7937
- try {
7938
- entries = readdirSync(dir);
7939
- } catch {
7940
- return [];
7941
- }
7942
- const files = [];
7943
- for (const entry of entries) {
7944
- const fullPath = join3(dir, entry);
7945
- let stat;
7946
- try {
7947
- stat = lstatSync2(fullPath);
7948
- } catch {
7949
- continue;
7950
- }
7951
- if (stat.isSymbolicLink())
7952
- continue;
7953
- if (stat.isDirectory()) {
7954
- files.push(...walkSkillFiles(fullPath, depth + 1));
7955
- } else if (stat.isFile() && entry === SKILL_FILE) {
7956
- files.push(fullPath);
7957
- }
7958
- }
7959
- return files;
7960
- }
7961
- function parseSkillFrontmatter(raw, fallbackKey) {
7962
- let name = fallbackKey;
7963
- let description;
7964
- if (raw.startsWith("---")) {
7965
- const end = raw.indexOf(`
7966
- ---`, 3);
7967
- if (end !== -1) {
7968
- const frontmatter = raw.slice(3, end);
7969
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
7970
- const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
7971
- if (nameMatch)
7972
- name = stripQuotes(nameMatch[1].trim()) || fallbackKey;
7973
- if (descMatch)
7974
- description = stripQuotes(descMatch[1].trim()) || undefined;
7975
- }
7976
- }
7977
- return { name, description };
7978
- }
7979
- function stripQuotes(value) {
7980
- return value.replace(/^["']|["']$/g, "");
7981
- }
7982
- function getGitCommit2(cwd) {
7983
- try {
7984
- return execFileSync2("git", ["rev-parse", "HEAD"], {
7985
- cwd,
7986
- encoding: "utf8",
7987
- stdio: ["ignore", "pipe", "ignore"]
7988
- }).trim();
7989
- } catch {
7990
- return;
7991
- }
7992
- }
7993
- async function fetchSkillProposals(flags, profileOptions = {}) {
7994
- const params = new URLSearchParams;
7995
- if (flags["generation-run-id"])
7996
- params.set("generationRunId", flags["generation-run-id"]);
7997
- const status = flags.status ?? "ready_for_pr";
7998
- if (status !== "all")
7999
- params.set("status", status);
8000
- if (flags["tenant-id"])
8001
- params.set("tenantId", flags["tenant-id"]);
8002
- const suffix = params.toString() ? `?${params.toString()}` : "";
8003
- const response = await callControlAPI(`/skills/proposals${suffix}`, { method: "GET" }, profileOptions);
8004
- return Array.isArray(response.proposals) ? response.proposals : [];
8005
- }
8006
- async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
8007
- const cwd = options.cwd ?? process.cwd();
8008
- const params = new URLSearchParams;
8009
- if (flags["tenant-id"])
8010
- params.set("tenantId", flags["tenant-id"]);
8011
- const suffix = params.toString() ? `?${params.toString()}` : "";
8012
- const proposal = await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: "GET" }, profileOptions);
8013
- if (!proposal.skillMd?.trim()) {
8014
- throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
8015
- }
8016
- const safeId = safeSkillId(proposal.skillId);
8017
- const relativePath = `.claude/skills/${safeId}/SKILL.md`;
8018
- const writeStatus = writeIfChanged(join3(cwd, relativePath), proposal.skillMd);
8019
- if (writeStatus === "failed") {
8020
- throw new Error(`Failed to write ${relativePath}`);
8021
- }
8022
- writeSkillProposalManifest(cwd, proposal, relativePath);
8023
- let acknowledged = true;
8024
- let ackError;
8025
- try {
8026
- await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
8027
- method: "POST",
8028
- body: JSON.stringify({ appliedBy: "moda-cli" })
8029
- }, profileOptions);
8030
- } catch (err) {
8031
- acknowledged = false;
8032
- ackError = err instanceof Error ? err.message : String(err);
8033
- }
8034
- return {
8035
- proposalId,
8036
- skillId: proposal.skillId,
8037
- path: relativePath,
8038
- status: writeStatus,
8039
- acknowledged,
8040
- ackError
8041
- };
8042
- }
8043
- function safeSkillId(skillId) {
8044
- const safeId = basename(skillId || "");
8045
- if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
8046
- throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
8047
- }
8048
- return safeId;
8049
- }
8050
- function writeSkillProposalManifest(cwd, proposal, relativePath) {
8051
- const manifestPath = join3(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
8052
- mkdirSync3(dirname4(manifestPath), { recursive: true });
8053
- const existing = existsSync4(manifestPath) ? readFileSync3(manifestPath, "utf8") : `applied_proposals:
8054
- `;
8055
- const entry = [
8056
- ` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
8057
- ` skill_id: "${escapeYamlString(proposal.skillId)}"`,
8058
- ` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
8059
- ` skill_version: ${Number(proposal.skillVersion || 0)}`,
8060
- ` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
8061
- ` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
8062
- ` topic_id: "${escapeYamlString(proposal.topicId)}"`,
8063
- ` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
8064
- ` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
8065
- ` path: "${escapeYamlString(relativePath)}"`,
8066
- ` applied_at: "${new Date().toISOString()}"`
8067
- ].join(`
8068
- `);
8069
- const prefix = existing.endsWith(`
8070
- `) ? existing : `${existing}
8071
- `;
8072
- writeFileSync4(manifestPath, `${prefix}${entry}
8073
- `);
8074
- }
8075
- function escapeYamlString(value) {
8076
- return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
8077
- }
8078
- function formatProposalRate(value) {
8079
- return `${(Number(value || 0) * 100).toFixed(1)}%`;
8080
- }
8081
- var SKILLS_SUBCOMMAND_FLAGS = {
8082
- gen: [
8083
- "replay",
8084
- "source",
8085
- "wait",
8086
- "clustering-poll-seconds",
8087
- "clustering-run-id",
8088
- "clustering-timeout-seconds",
8089
- "end-at",
8090
- "eval-set-id",
8091
- "force-reprocess",
8092
- "harness-url",
8093
- "improve-candidates",
8094
- "improvement-rounds",
8095
- "lookback-hours",
8096
- "max-cluster-pct",
8097
- "max-sessions",
8098
- "max-traces",
8099
- "no-clustering",
8100
- "no-replay",
8101
- "replay-set-id",
8102
- "reprocess-segments",
8103
- "start-at",
8104
- "tenant-id",
8105
- "wait-for-completion"
8106
- ],
8107
- status: ["run-id"],
8108
- pull: ["status"],
8109
- proposals: ["status", "generation-run-id", "tenant-id"],
8110
- proposal: ["proposal-id", "tenant-id"],
8111
- install: ["list"]
8112
- };
8113
- async function runSkillsCommand(input) {
8114
- const context = "output" in input ? input : createCommandContext({
8115
- command: input.command,
8116
- positional: input.positional,
8117
- positionals: input.positionals,
8118
- flags: input.flags
8119
- }, {
8120
- outputMode: input.flags.agent === "true" ? "agent" : input.flags.json === "true" ? "json" : "human"
8121
- });
8122
- const sub = context.positional;
8123
- const positionals = context.positionals ?? (sub ? [sub] : []);
8124
- const flags = context.flags;
8125
- if (sub && SKILLS_SUBCOMMAND_FLAGS[sub])
8126
- assertFlags(`skills ${sub}`, flags, SKILLS_SUBCOMMAND_FLAGS[sub]);
8127
- if (sub === "gen") {
8128
- const result = await generateSkills(flags);
8129
- if (context.outputMode !== "human") {
8130
- context.output.writeData(result, {
8131
- status: Number(result.sessions_failed ?? 0) > 0 ? "error" : "success"
8132
- });
8133
- } else if (result.accepted) {
8134
- console.log([
8135
- "accepted: true",
8136
- result.generation_run_id && `generation_run_id: ${result.generation_run_id}`,
8137
- result.message && `message: ${result.message}`,
8138
- result.generation_run_id && `follow progress: moda skills status ${result.generation_run_id}`
8139
- ].filter(Boolean).join(`
8140
- `));
8141
- } else {
8142
- console.log([
8143
- `processed: ${result.sessions_processed ?? 0}`,
8144
- `failed: ${result.sessions_failed ?? 0}`,
8145
- `pending: ${result.sessions_pending ?? 0}`,
8146
- `pending_found: ${result.pending_sessions_found ?? 0}`,
8147
- `distilled: ${Number(result.cluster_result?.distilled_skills_generated ?? 0)}`,
8148
- `indexed: ${Number(result.cluster_result?.skills_indexed ?? 0)}`,
8149
- result.distillation_skipped && `distillation skipped: ${result.distillation_skip_reason || "unknown"}`
8150
- ].filter(Boolean).join(`
8151
- `));
8152
- }
8153
- return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
8154
- }
8155
- if (sub === "status") {
8156
- const runId = positionals[1] || flags["run-id"] || undefined;
8157
- const { run, events } = await fetchSkillRunStatus(runId);
8158
- if (context.outputMode !== "human") {
8159
- context.output.writeData({ run, events }, { status: run?.status === "failed" ? "error" : "success" });
8160
- } else if (!run) {
8161
- console.log("No skill generation runs found yet. Start one with `moda skills gen`.");
8162
- } else {
8163
- console.log(formatRunStatusHuman(run, events));
8164
- }
8165
- return run?.status === "failed" ? 1 : 0;
8166
- }
8167
- if (sub === "proposals" && positionals[1] === "list") {
8168
- const proposals = await fetchSkillProposals(flags, { profile: context.profile });
8169
- if (flags.json === "true") {
8170
- console.log(JSON.stringify({ proposals }));
8171
- } else if (proposals.length === 0) {
8172
- console.log("No skill proposals found.");
8173
- } else {
8174
- for (const proposal of proposals) {
8175
- console.log([
8176
- proposal.proposalId,
8177
- proposal.proposalStatus,
8178
- `${proposal.skillId}@v${proposal.skillVersion}`,
8179
- `delta=${formatProposalRate(proposal.deltaPassRate)}`,
8180
- `baseline=${formatProposalRate(proposal.baselinePassRate)}`,
8181
- `candidate=${formatProposalRate(proposal.candidatePassRate)}`,
8182
- proposal.name
8183
- ].filter(Boolean).join("\t"));
8184
- }
8185
- }
8186
- return 0;
8187
- }
8188
- if (sub === "proposal" && positionals[1] === "apply") {
8189
- const proposalId = positionals[2] || flags["proposal-id"];
8190
- if (!proposalId) {
8191
- throw new CliInputError("A proposal id is required", "Usage: moda skills proposal apply <proposal-id>");
8192
- }
8193
- const result = await applySkillProposal(proposalId, flags, { profile: context.profile });
8194
- if (flags.json === "true") {
8195
- console.log(JSON.stringify(result));
8196
- } else {
8197
- console.log([
8198
- `${result.status} ${result.path}`,
8199
- result.acknowledged ? "acknowledged: true" : "acknowledged: false"
8200
- ].join(`
8201
- `));
8202
- }
8203
- if (!result.acknowledged) {
8204
- 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.");
8205
- return 1;
8206
- }
8207
- return 0;
8208
- }
8209
- if (sub === "list" || sub === "show" || sub === "policy") {
8210
- return runSkillRegistryCommand(sub, positionals, flags, context);
8211
- }
8212
- if (sub === "sync") {
8213
- assertFlags("skills sync", flags, ["dry-run"]);
8214
- const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
8215
- context.output.writeData({
8216
- synced: result.synced,
8217
- dryRun: result.dryRun,
8218
- ...result.agentsMd ? { agentsMd: result.agentsMd } : {}
8219
- });
8220
- return 0;
8221
- }
8222
- if (sub === "install") {
8223
- return runSkillsInstall(context, positionals.slice(1));
8224
- }
8225
- if (sub !== "pull") {
8226
- throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : "A skills subcommand is required", "Usage: moda skills gen [--source=all|sdk] [--max-sessions=N (default 200)] [--lookback-hours=N (default 720)] [--max-traces=N] [--improvement-rounds=1-3] [--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] | moda skills install [--list] <id...>");
8227
- }
8228
- const status = (flags.status ?? "approved").toLowerCase();
8229
- if (!VALID_STATUSES.has(status)) {
8230
- throw new CliInputError(`Invalid --status '${status}'`, `Must be one of: ${[...VALID_STATUSES].join(", ")}`);
8231
- }
8232
- const summary = await pullSkills({ status, silent: context.outputMode !== "human" });
8233
- const parts = [
8234
- summary.pruned && `${summary.pruned} pruned`,
8235
- summary.wrote && `${summary.wrote} written`,
8236
- summary.updated && `${summary.updated} updated`,
8237
- summary.unchanged && `${summary.unchanged} unchanged`,
8238
- summary.failed && `${summary.failed} failed`
8239
- ].filter(Boolean);
8240
- if (context.outputMode === "human" && summary.total > 0) {
8241
- console.log(` Done — ${parts.join(", ") || "no changes"}`);
8242
- }
8243
- if (context.outputMode !== "human") {
8244
- context.output.writeData({ status, summary });
8245
- }
8246
- return summary.failed > 0 ? 1 : 0;
8247
- }
8248
- async function generateSkills(flags) {
8249
- const apiKey = resolveApiKey();
8250
- if (!apiKey) {
8251
- throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
8252
- }
8253
- const tenantId = flags["tenant-id"] || resolveTenantId();
8254
- const directHarnessUrl = flags["harness-url"] || process.env.MODA_SKILL_HARNESS_URL;
8255
- if (directHarnessUrl && !tenantId) {
8256
- throw new CliInputError("No Moda tenant id found. Run `moda init` first (or set MODA_TENANT_ID / --tenant-id).");
8257
- }
8258
- const endpoint = directHarnessUrl ? `${directHarnessUrl.replace(/\/+$/, "")}/skills/generate` : `${resolveIngestUrl().replace(/\/+$/, "")}/v1/skills/generate`;
8259
- const waitForCompletion = flags.wait === "true" || flags["wait-for-completion"] === "true";
8260
- const forceReprocess = flags["force-reprocess"] === "true";
8261
- const source = normalizeSkillSource(flags.source);
8262
- if (source === "coding_agent") {
8263
- 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.");
8264
- }
8265
- const body = compactObject({
8266
- tenant_id: tenantId || undefined,
8267
- max_sessions: parseScopeInt(flags["max-sessions"], "max-sessions"),
8268
- lookback_hours: parseScopeInt(flags["lookback-hours"], "lookback-hours"),
8269
- start_at: flags["start-at"] || undefined,
8270
- end_at: flags["end-at"] || undefined,
8271
- clustering_run_id: flags["clustering-run-id"] || undefined,
8272
- replay_set_id: flags["replay-set-id"] || flags["eval-set-id"] || undefined,
8273
- run_clustering: flags["no-clustering"] !== "true",
8274
- source,
8275
- clustering_timeout_seconds: parsePositiveInt3(flags["clustering-timeout-seconds"], 7200),
8276
- clustering_poll_seconds: parsePositiveInt3(flags["clustering-poll-seconds"], 15),
8277
- max_cluster_pct: parseOptionalFloat(flags["max-cluster-pct"]),
8278
- reprocess_segments: flags["reprocess-segments"] === "true" || forceReprocess,
8279
- run_replay: flags.replay === "true" && flags["no-replay"] !== "true",
8280
- max_traces: parseScopeInt(flags["max-traces"], "max-traces"),
8281
- improve_candidates: flags["improve-candidates"] === "true",
8282
- improvement_rounds: parseScopeInt(flags["improvement-rounds"], "improvement-rounds", MAX_IMPROVEMENT_ROUNDS) ?? 2,
8283
- force_reprocess: forceReprocess,
8284
- wait_for_completion: waitForCompletion
8285
- });
8286
- const headers = {
8287
- Authorization: `Bearer ${apiKey}`,
8288
- "Content-Type": "application/json"
8289
- };
8290
- if (tenantId)
8291
- headers["X-Moda-Tenant-Id"] = tenantId;
8292
- recordApiRequest();
8293
- const response = await fetch(endpoint, {
8294
- method: "POST",
8295
- headers,
8296
- body: JSON.stringify(body)
8297
- });
8298
- if (!response.ok) {
8299
- const text = await response.text().catch(() => "");
8300
- throw new Error(`Failed to generate skills (HTTP ${response.status}): ${text.slice(0, 200)}`);
8301
- }
8302
- return await response.json();
8303
- }
8304
- var MAX_IMPROVEMENT_ROUNDS = 3;
8305
- function parseScopeInt(value, flag, max) {
8306
- if (value === undefined)
8307
- return;
8308
- const parsed = /^\d+$/.test(value.trim()) ? Number.parseInt(value, 10) : Number.NaN;
8309
- if (!Number.isFinite(parsed) || parsed < 1) {
8310
- throw new CliInputError(`--${flag}=${value} is not a positive integer, so nothing was sent.`, `0 would mean unlimited server-side; omit --${flag} for the server default, or pass a number of 1 or more.`);
8311
- }
8312
- if (max !== undefined && parsed > max) {
8313
- throw new CliInputError(`--${flag}=${value} is above the maximum of ${max}, so nothing was sent.`);
8314
- }
8315
- return parsed;
8316
- }
8317
- function parsePositiveInt3(value, fallback) {
8318
- if (!value)
8319
- return fallback;
8320
- const parsed = Number.parseInt(value, 10);
8321
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
8322
- }
8323
- function parseOptionalFloat(value) {
8324
- if (!value)
8325
- return;
8326
- const parsed = Number.parseFloat(value);
8327
- return Number.isFinite(parsed) ? parsed : undefined;
8328
- }
8329
- function compactObject(input) {
8330
- return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
8331
- }
8332
- function normalizeSkillSource(value) {
8333
- const normalized = (value || "all").toLowerCase().replace(/-/g, "_");
8334
- if (["all", "sdk", "coding_agent"].includes(normalized))
8335
- return normalized;
8336
- return "all";
8337
- }
8338
- function releaseKeyFor(release, skills, status) {
8339
- if (release?.generation_run_id)
8340
- return `${status}:run:${release.generation_run_id}`;
8341
- if (Number(release?.generation_version || 0) > 0) {
8342
- return `${status}:version:${Number(release?.generation_version)}`;
8343
- }
8344
- return `${status}:legacy:${sha2562(JSON.stringify(skills.map((skill) => ({
8345
- skill_id: skill.skill_id,
8346
- hash: sha2562(skill.skill_md || "")
8347
- })).sort((a, b) => a.skill_id.localeCompare(b.skill_id))))}`;
8348
- }
8349
- function manifestHeaderFor(fetched) {
8350
- const release = fetched.release;
8351
- return {
8352
- tenantId: fetched.tenantId,
8353
- releaseKey: fetched.releaseKey,
8354
- statusFilter: fetched.status,
8355
- generationRunId: release?.generation_run_id || "",
8356
- generationVersion: Number(release?.generation_version || 0),
8357
- releasedAt: release?.released_at || ""
8358
- };
8359
- }
8360
- function sha2562(input) {
8361
- return createHash2("sha256").update(input).digest("hex");
8362
- }
8363
- async function runSkillsInstall(context, ids) {
8364
- const { installIntegrationSkill, readBundledIndex, readBundledSkillMd } = await import("./skills-bundled-mdmhzfsr.js");
8365
- const index = readBundledIndex();
8366
- if (context.flags.list === "true" || ids.length === 0) {
8367
- if (!index) {
8368
- throw new CliInputError("This CLI build has no bundled integration guides", "Install a published @moda-ai/cli, or run `bun run bundle:skills` in apps/moda-cli.");
8369
- }
8370
- const skills = index.skills.map((skill) => ({
8371
- id: skill.id,
8372
- version: skill.version,
8373
- language: skill.targets.language,
8374
- summary: skill.summary
8375
- }));
8376
- if (context.outputMode !== "human") {
8377
- context.output.writeData({ skills });
8378
- } else {
8379
- for (const skill of skills)
8380
- console.log(`${skill.id} ${skill.version} ${skill.summary}`);
8381
- if (ids.length === 0 && context.flags.list !== "true") {
8382
- console.error("Install one or more with: moda skills install <id...>");
8383
- }
8384
- }
8385
- return 0;
8386
- }
8387
- const unknown = ids.filter((id) => !/^[a-z0-9-]+$/.test(id) || readBundledSkillMd(id) === null);
8388
- if (unknown.length > 0) {
8389
- throw new CliInputError(`Not bundled with this CLI: ${unknown.map((id) => JSON.stringify(id).slice(0, 80)).join(", ")}`, "List the bundled guides with: moda skills install --list");
8390
- }
8391
- const results = [];
8392
- for (const id of ids) {
8393
- try {
8394
- results.push(installIntegrationSkill(id));
8395
- } catch (error) {
8396
- const done = results.map((result) => result.skillId);
8397
- throw new CliInputError(error instanceof Error ? error.message : String(error), done.length > 0 ? `Already installed before the failure: ${done.join(", ")}. Fix the error and rerun; installed guides are left as they are.` : "List the bundled guides with: moda skills install --list");
8398
- }
8399
- }
8400
- if (context.outputMode !== "human") {
8401
- context.output.writeData({ installed: results });
8402
- } else {
8403
- for (const result of results) {
8404
- console.log(`${result.status} ${result.skillId}`);
8405
- for (const path of result.paths)
8406
- console.log(` ${path}`);
8407
- }
8408
- }
8409
- return 0;
8410
- }
8411
- var RESERVED_SKILL_PATHS = new Set(["inbox", "library", "observed", "runs", "sync", "surface", "families", "proposals"]);
8412
- async function runSkillRegistryCommand(sub, positionals, flags, context) {
8413
- const profileOptions = { profile: context.profile, env: context.env };
8414
- assertFlags(`skills ${sub}`, flags, sub === "policy" ? ["live", "local", "dry-run"] : []);
8415
- assertPositionals(`skills ${sub}`, positionals, sub === "list" ? 1 : 2, sub === "list" ? "moda skills list" : `moda skills ${sub} <key>${sub === "policy" ? " --live|--local" : ""}`);
8416
- if (sub === "list") {
8417
- context.output.writeData(await callControlAPI("/skills", {}, profileOptions));
8418
- return 0;
8419
- }
8420
- const idOrKey = positionals[1];
8421
- if (!idOrKey) {
8422
- throw new CliInputError(`A skill key or id is required for skills ${sub}.`, sub === "show" ? "Usage: moda skills show <key>" : "Usage: moda skills policy <key> --live|--local");
8423
- }
8424
- if (RESERVED_SKILL_PATHS.has(idOrKey)) {
8425
- throw new CliInputError(`'${idOrKey}' is a reserved path, so this skill must be addressed by its id.`, "Find the id (skill_…) with `moda skills list`, then pass that instead of the key.");
8426
- }
8427
- if (sub === "show") {
8428
- context.output.writeData(await callControlAPI(`/skills/${encodeURIComponent(idOrKey)}`, {}, profileOptions));
8429
- return 0;
8430
- }
8431
- const live = flags.live === "true";
8432
- const local = flags.local === "true";
8433
- if (live === local) {
8434
- throw new CliInputError("Pass exactly one of --live or --local.", "Usage: moda skills policy <key> --live|--local");
8435
- }
8436
- const surfacePolicy = live ? "include" : "local_only";
8437
- if (context.dryRun) {
8438
- context.output.writeData({ dryRun: true, planned: { method: "PATCH", endpoint: `/api/skills/${encodeURIComponent(idOrKey)}/surface-policy`, body: { surfacePolicy } } });
8439
- return 0;
8440
- }
8441
- context.output.writeData(await callControlAPI(`/skills/${encodeURIComponent(idOrKey)}/surface-policy`, { method: "PATCH", body: JSON.stringify({ surfacePolicy }) }, profileOptions, { retries: 0 }));
8442
- return 0;
8443
- }
8444
-
8445
- // src/doctor.ts
8446
- import { createHash as createHash3 } from "node:crypto";
8447
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
8448
- import { join as join4 } from "node:path";
8449
- async function runDoctorCommand(context) {
8450
- const report = await buildDoctorReport(context, {
8451
- offline: context.flags.offline === "true"
8452
- });
8453
- if (context.outputMode === "human") {
8454
- if (!context.quiet)
8455
- console.log(renderDoctorHuman(report));
8456
- return { exitCode: 0 };
8457
- }
8458
- context.output.writeData(report, {
8459
- status: report.summary.fail > 0 ? "error" : "success",
8460
- ...doctorAgentWriteOptions(report, "doctor")
8461
- });
8462
- return { exitCode: 0 };
8463
- }
8464
- async function runStatusCommand(context) {
8465
- const report = await buildDoctorReport(context, {
8466
- offline: context.flags.offline === "true" || context.flags.online !== "true"
8467
- });
8468
- const topBroken = firstBrokenCheck(report.checks);
8469
- const statusPayload = {
8470
- schemaVersion: "status.v0.1",
8471
- generatedAt: report.generatedAt,
8472
- status: report.summary.status,
8473
- activeProfile: report.profile.active,
8474
- auth: {
8475
- apiKey: resolveApiKey() ? "configured" : "missing"
8476
- },
8477
- workspace: {
8478
- tenantId: resolveTenantId() ?? null,
8479
- baseUrl: resolveModaBaseUrl(),
8480
- ingestUrl: resolveIngestUrl()
8481
- },
8482
- dataFlow: checkStatus(report.checks, "cloud_data_flow"),
8483
- harness: checkStatus(report.checks, "harness_manifest_current"),
8484
- harnessSync: checkStatus(report.checks, "harness_sync_state_present"),
8485
- harnessReport: "skipped",
8486
- shadowSpark: "skipped",
8487
- prompts: checkStatus(report.checks, "prompts_discovered"),
8488
- skills: checkStatus(report.checks, "skills_discovered"),
8489
- topBrokenCheck: topBroken ? {
8490
- id: topBroken.id,
8491
- status: topBroken.status,
8492
- severity: topBroken.severity,
8493
- nextCommand: topBroken.nextCommand
8494
- } : null,
8495
- nextCommands: report.nextCommands.slice(0, 3)
8496
- };
8497
- if (context.outputMode === "human") {
8498
- if (!context.quiet)
8499
- console.log(renderStatusHuman(statusPayload));
8500
- return { exitCode: 0 };
8501
- }
8502
- context.output.writeData(statusPayload, {
8503
- status: report.summary.fail > 0 ? "error" : "success",
8504
- ...doctorAgentWriteOptions(report, "status")
8505
- });
8506
- return { exitCode: 0 };
8507
- }
8508
- async function buildDoctorReport(context, options) {
8509
- const checks = [];
8510
- const profileConfig = loadProfileConfig({ env: context.env });
8511
- const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
8512
- const paths = getModaPaths({ env: context.env });
8513
- const profileOptions = { profile: context.profile, env: context.env };
8514
- const apiKey = resolveApiKey(profileOptions);
8515
- const tenantId = resolveTenantId(profileOptions);
8516
- const baseUrl = resolveModaBaseUrl(profileOptions);
8517
- const ingestUrl = resolveIngestUrl(profileOptions);
8518
- checks.push({
8519
- id: "active_profile_resolves",
8520
- group: "config",
8521
- title: "Active profile resolves",
8522
- status: activeProfile ? "pass" : "fail",
8523
- severity: activeProfile ? "info" : "critical",
8524
- evidence: [
8525
- { label: "active_profile", value: profileConfig.active_profile },
8526
- { label: "environment", value: activeProfile.environment }
8527
- ],
8528
- remediation: activeProfile ? undefined : "Create or select a Moda profile.",
8529
- nextCommand: activeProfile ? undefined : "moda profiles list"
8530
- });
8531
- checks.push({
8532
- id: "api_key_resolves",
8533
- group: "config",
8534
- title: "API key resolves",
8535
- status: apiKey ? "pass" : "fail",
8536
- severity: apiKey ? "info" : "critical",
8537
- evidence: [{ label: "source", value: apiKey ? "environment/profile/config" : "none" }],
8538
- remediation: apiKey ? undefined : "Authenticate and create a tenant API key.",
8539
- nextCommand: apiKey ? undefined : "moda init"
8540
- });
8541
- checks.push({
8542
- id: "tenant_resolves",
8543
- group: "config",
8544
- title: "Tenant/workspace resolves",
8545
- status: tenantId ? "pass" : "warning",
8546
- severity: tenantId ? "info" : "warning",
8547
- evidence: [{ label: "tenant_id", value: tenantId ? "configured" : "missing" }],
8548
- remediation: tenantId ? undefined : "Select a tenant during init or set MODA_TENANT_ID.",
8549
- nextCommand: tenantId ? undefined : "moda init"
8550
- });
8551
- checks.push(authTokenCheck(context, Boolean(apiKey)));
8552
- checks.push(configPermissionCheck(paths.legacyConfigPath));
8553
- if (activeProfile.api_key_ref) {
8554
- const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
8555
- if (secretPath)
8556
- checks.push(secretPermissionCheck(secretPath));
8557
- checks.push({
8558
- id: "profile_api_key_ref_resolves",
8559
- group: "config",
8560
- title: "Profile API key reference resolves",
8561
- status: resolveSecretReference(activeProfile.api_key_ref) ? "pass" : "warning",
8562
- severity: "warning",
8563
- evidence: [{ label: "api_key_ref", value: activeProfile.api_key_ref }],
8564
- remediation: "Create or update the profile API key reference.",
8565
- nextCommand: "moda profiles create"
8566
- });
8567
- }
8568
- checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
8569
- checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
8570
- checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
8571
- const manifestPath = join4(context.cwd, ".moda", "harness.json");
8572
- const manifestExists = existsSync5(manifestPath);
8573
- checks.push({
8574
- id: "harness_manifest_exists",
8575
- group: "harness",
8576
- title: "Harness manifest exists",
8577
- status: manifestExists ? "pass" : "warning",
8578
- severity: manifestExists ? "info" : "warning",
8579
- evidence: [{ label: "path", value: ".moda/harness.json" }],
8580
- remediation: manifestExists ? undefined : "Run `moda harness scan --summary` to scan the project and write a harness manifest.",
8581
- nextCommand: manifestExists ? undefined : "moda harness scan --summary"
8582
- });
8583
- checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
8584
- checks.push(harnessSyncStateCheck(context.cwd));
8585
- checks.push(promptsDiscoveredCheck(context.cwd));
8586
- checks.push(discoveryCheck("skills_discovered", "harness", "Skills are discovered", context.cwd, [
8587
- "skills",
8588
- ".claude/skills",
8589
- ".cursor/rules"
8590
- ], "moda skills pull"));
8591
- checks.push(discoveryCheck("evals_discovered", "harness", "Evals/tests are discovered", context.cwd, [
8592
- "evals",
8593
- "tests",
8594
- "__tests__"
8595
- ]));
8596
- checks.push(validationEventPathCheck(ingestUrl));
8597
- if (options.offline || !apiKey) {
8598
- checks.push(...offlineCloudChecks(options.offline));
8599
- } else {
8600
- checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
8601
- }
8602
- const nextCommands = nextCommandsForChecks(checks);
8603
- const summary = summarizeChecks(checks);
8604
- return {
8605
- schemaVersion: "doctor.v0.1",
8606
- generatedAt: new Date().toISOString(),
8607
- profile: {
8608
- active: activeProfile.name,
8609
- requested: context.profile
8610
- },
8611
- summary,
8612
- checks,
8613
- nextCommands,
8614
- fixPlan: checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => ({
8615
- checkId: check.id,
8616
- command: check.nextCommand,
8617
- action: check.remediation ?? "Review this diagnostic."
8618
- }))
8619
- };
8620
- }
8621
- function authTokenCheck(context, hasApiKey) {
8622
- const session = loadAuthSession({
8623
- profile: context.profile,
8624
- env: context.env,
8625
- includeExpired: true
8626
- });
8627
- const valid = isAuthSessionValid(session);
8628
- if (!valid && hasApiKey) {
8629
- return {
8630
- id: "auth_token_valid",
8631
- group: "config",
8632
- title: "Browser auth token is valid",
8633
- status: "skipped",
8634
- severity: "info",
8635
- evidence: [{ label: "reason", value: "api key configured" }]
8636
- };
8637
- }
8638
- return {
8639
- id: "auth_token_valid",
8640
- group: "config",
8641
- title: "Browser auth token is valid",
8642
- status: valid ? "pass" : "warning",
8643
- severity: valid ? "info" : "warning",
8644
- evidence: [
8645
- { label: "token", value: session ? "present" : "missing" },
8646
- ...session ? [{ label: "expires_at", value: new Date(session.expires_at * 1000).toISOString() }] : []
8647
- ],
8648
- remediation: valid ? undefined : "Refresh browser authentication.",
8649
- nextCommand: valid ? undefined : "moda init"
8650
- };
8651
- }
8652
- function configPermissionCheck(path) {
8653
- if (!existsSync5(path)) {
8654
- return {
8655
- id: "config_file_permissions",
8656
- group: "config",
8657
- title: "Config file permissions",
8658
- status: "skipped",
8659
- severity: "info",
8660
- evidence: [{ label: "path", value: path }]
8661
- };
8662
- }
8663
- const permission = ensureSecretFilePermissions(path);
8664
- return {
8665
- id: "config_file_permissions",
8666
- group: "config",
8667
- title: "Config file permissions",
8668
- status: permission.secure ? "pass" : "fail",
8669
- severity: permission.secure ? "info" : "critical",
8670
- evidence: [
8671
- { label: "path", value: path },
8672
- { label: "mode", value: permission.mode ?? "unknown" }
8673
- ],
8674
- remediation: permission.secure ? undefined : "Restrict config file permissions to the current user."
8675
- };
8676
- }
8677
- function secretPermissionCheck(path) {
8678
- const permission = ensureSecretFilePermissions(path);
8679
- return {
8680
- id: "secret_file_permissions",
8681
- group: "config",
8682
- title: "Secret file permissions",
8683
- status: permission.secure ? "pass" : "fail",
8684
- severity: permission.secure ? "info" : "critical",
8685
- evidence: [
8686
- { label: "path", value: path },
8687
- { label: "mode", value: permission.mode ?? "unknown" }
8688
- ],
8689
- remediation: permission.secure ? undefined : "Restrict secret file permissions to the current user."
8690
- };
8691
- }
8692
- function urlCheck(id, group, title, value) {
8693
- let valid = false;
8694
- try {
8695
- const parsed = new URL(value);
8696
- valid = parsed.protocol === "http:" || parsed.protocol === "https:";
8697
- } catch {
8698
- valid = false;
8699
- }
8700
- return {
8701
- id,
8702
- group,
8703
- title,
8704
- status: valid ? "pass" : "fail",
8705
- severity: valid ? "info" : "critical",
8706
- evidence: [{ label: "url", value }],
8707
- remediation: valid ? undefined : "Configure a valid http(s) URL.",
8708
- nextCommand: valid ? undefined : "moda profiles create"
8709
- };
8710
- }
8711
- function localBaseUrlCheck(environment, baseUrl) {
8712
- if (environment !== "local") {
8713
- return {
8714
- id: "local_base_url_frontend",
8715
- group: "config",
8716
- title: "Local base URL targets the frontend",
8717
- status: "skipped",
8718
- severity: "info",
8719
- evidence: [{ label: "environment", value: environment }]
8720
- };
8721
- }
8722
- let port = null;
8723
- let loopback = false;
8724
- try {
8725
- const parsed = new URL(baseUrl);
8726
- loopback = isLoopbackBaseUrl(baseUrl);
8727
- port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
8728
- } catch {
8729
- return {
8730
- id: "local_base_url_frontend",
8731
- group: "config",
8732
- title: "Local base URL targets the frontend",
8733
- status: "fail",
8734
- severity: "critical",
8735
- evidence: [{ label: "base_url", value: baseUrl }],
8736
- remediation: "Set the local profile base URL to the frontend origin (http://localhost:3000).",
8737
- nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000"
8738
- };
8739
- }
8740
- const looksLikeBackend = loopback && (port === "3001" || port === "3002");
8741
- if (looksLikeBackend) {
8742
- return {
8743
- id: "local_base_url_frontend",
8744
- group: "config",
8745
- title: "Local base URL targets the frontend",
8746
- status: "warning",
8747
- severity: "warning",
8748
- evidence: [
8749
- { label: "base_url", value: baseUrl },
8750
- { label: "port", value: port ?? "unknown" }
8751
- ],
8752
- 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.",
8753
- nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000 --use"
8754
- };
8755
- }
8756
- if (!loopback) {
8757
- return {
8758
- id: "local_base_url_frontend",
8759
- group: "config",
8760
- title: "Local base URL targets the frontend",
8761
- status: "warning",
8762
- severity: "warning",
8763
- evidence: [{ label: "base_url", value: baseUrl }],
8764
- remediation: "Local profile should use a loopback frontend URL (http://localhost:3000). " + "Activate with `moda profiles use local` or set MODA_PROFILE=local.",
8765
- nextCommand: "moda profiles use local"
8766
- };
8767
- }
8768
- return {
8769
- id: "local_base_url_frontend",
8770
- group: "config",
8771
- title: "Local base URL targets the frontend",
8772
- status: "pass",
8773
- severity: "info",
8774
- evidence: [{ label: "base_url", value: baseUrl }]
8775
- };
8776
- }
8777
- function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
8778
- if (!manifestExists) {
8779
- return {
8780
- id: "harness_manifest_current",
8781
- group: "harness",
8782
- title: "Harness manifest matches scanner output",
8783
- status: "skipped",
8784
- severity: "info",
8785
- evidence: [{ label: "reason", value: "manifest missing" }],
8786
- nextCommand: "moda harness scan --summary"
8787
- };
8788
- }
8789
- try {
8790
- const current = scanHarness({
8791
- rootDir: cwd,
8792
- scannerVersion: "doctor",
8793
- tenantId,
8794
- tenantSource: tenantId ? "env_or_config" : undefined
8795
- });
8796
- const saved = JSON.parse(readFileSync4(manifestPath, "utf8"));
8797
- const savedHash = stableGraphHash(saved);
8798
- const currentHash = stableGraphHash(current);
8799
- const matches = savedHash === currentHash;
8800
- return {
8801
- id: "harness_manifest_current",
8802
- group: "harness",
8803
- title: "Harness manifest matches scanner output",
8804
- status: matches ? "pass" : "warning",
8805
- severity: matches ? "info" : "warning",
8806
- evidence: [
8807
- { label: "saved_hash", value: savedHash.slice(0, 12) },
8808
- { label: "current_hash", value: currentHash.slice(0, 12) },
8809
- ...matches ? [] : [{ label: "diff", value: "Manifest hash differs from fresh scanner output." }]
8810
- ],
8811
- remediation: matches ? undefined : "Refresh the local harness manifest.",
8812
- nextCommand: matches ? undefined : "moda harness scan --summary"
8813
- };
8814
- } catch (err) {
8815
- return {
8816
- id: "harness_manifest_current",
8817
- group: "harness",
8818
- title: "Harness manifest matches scanner output",
8819
- status: "fail",
8820
- severity: "critical",
8821
- evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
8822
- remediation: "Regenerate the harness manifest.",
8823
- nextCommand: "moda harness scan --summary"
8824
- };
8825
- }
8826
- }
8827
- function harnessSyncStateCheck(cwd) {
8828
- const state = readHarnessSyncState(cwd);
8829
- if (!state) {
8830
- return {
8831
- id: "harness_sync_state_present",
8832
- group: "harness",
8833
- title: "Harness sync state exists",
8834
- status: "warning",
8835
- severity: "warning",
8836
- evidence: [{ label: "path", value: ".moda/sync-state.json" }],
8837
- remediation: "Sync the harness graph so Moda can correlate local state with cloud state.",
8838
- nextCommand: "moda harness sync --dry-run"
8839
- };
8840
- }
8841
- return {
8842
- id: "harness_sync_state_present",
8843
- group: "harness",
8844
- title: "Harness sync state exists",
8845
- status: "pass",
8846
- severity: "info",
8847
- evidence: [
8848
- { label: "updated_at", value: state.updatedAt },
8849
- { label: "profile", value: state.profile },
8850
- { label: "harness_id", value: state.harnessId },
8851
- { label: "remote_version_id", value: state.remote.versionId || "none" }
8852
- ]
8853
- };
8854
- }
8855
- function promptsDiscoveredCheck(cwd) {
8856
- const base = { id: "prompts_discovered", group: "harness", title: "Prompts are discovered" };
8857
- if (!existsSync5(join4(cwd, ".moda", "prompts.yml"))) {
8858
- return discoveryCheck(base.id, base.group, base.title, cwd, ["prompts", ".moda/prompts.yml"], "moda prompts init");
8859
- }
8860
- let inspection;
8861
- try {
8862
- inspection = inspectPromptManifest(cwd);
8863
- } catch (error) {
8864
- return {
8865
- ...base,
8866
- status: "fail",
8867
- severity: "warning",
8868
- evidence: [{ label: "manifest", value: ".moda/prompts.yml" }, { label: "error", value: error instanceof Error ? error.message : String(error) }],
8869
- remediation: "Fix .moda/prompts.yml (version, prompt_paths, sources).",
8870
- nextCommand: "moda prompts status"
8871
- };
8872
- }
8873
- const evidence = [
8874
- { label: "manifest", value: `.moda/prompts.yml (version ${inspection.manifestVersion})` },
8875
- { label: "matched_files", value: String(inspection.matchedFiles) },
8876
- { label: "sources", value: String(inspection.sources) },
8877
- ...inspection.zeroMatchPatterns.length > 0 ? [{ label: "zero_match_patterns", value: inspection.zeroMatchPatterns.join(", ") }] : [],
8878
- ...inspection.legacyOnlyFiles.length > 0 ? [{
8879
- label: "legacy_only_files",
8880
- value: `${inspection.legacyOnlyFiles.length} (${inspection.legacyOnlyFiles.slice(0, 3).join(", ")}${inspection.legacyOnlyFiles.length > 3 ? ", ..." : ""})`
8881
- }] : []
8882
- ];
8883
- if (inspection.legacyOnlyFiles.length > 0) {
8884
- return {
8885
- ...base,
8886
- status: "warning",
8887
- severity: "warning",
8888
- evidence,
8889
- remediation: "Prompt files found by older CLIs no longer match the manifest globs (they are exact paths from the repository root). Migrate the manifest to keep them tracked under their existing keys.",
8890
- nextCommand: "moda prompts migrate --dry-run"
8891
- };
8892
- }
8893
- if (inspection.matchedFiles === 0 && inspection.sources === 0) {
8894
- return {
8895
- ...base,
8896
- status: "warning",
8897
- severity: "warning",
8898
- evidence,
8899
- remediation: "The prompt manifest matches no files and maps no sources, so sync registers nothing. Fix prompt_paths or preview source discovery.",
8900
- nextCommand: "moda prompts sync --dry-run"
8901
- };
8902
- }
8903
- return { ...base, status: "pass", severity: "info", evidence };
8904
- }
8905
- function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
8906
- const found = relativePaths.filter((path) => existsSync5(join4(cwd, path)));
8907
- return {
8908
- id,
8909
- group,
8910
- title,
8911
- status: found.length > 0 ? "pass" : "warning",
8912
- severity: found.length > 0 ? "info" : "warning",
8913
- evidence: [{ label: "paths", value: found.length > 0 ? found.join(", ") : "none" }],
8914
- remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(", ")}.`,
8915
- nextCommand: found.length > 0 ? undefined : nextCommand
8916
- };
8917
- }
8918
- function validationEventPathCheck(ingestUrl) {
8919
- return {
8920
- id: "validation_event_path_defined",
8921
- group: "cloud",
8922
- title: "Validation event path is defined",
8923
- status: "pass",
8924
- severity: "info",
8925
- evidence: [{ label: "path", value: `${ingestUrl.replace(/\/+$/, "")}/v1/otel/validation` }]
8926
- };
8927
- }
8928
- function offlineCloudChecks(isOffline) {
8929
- const status = isOffline ? "skipped" : "warning";
8930
- const evidence = [{ label: "mode", value: isOffline ? "offline" : "missing api key" }];
8931
- const remediation = isOffline ? "Rerun without --offline to check Moda Cloud." : "Configure an API key before checking cloud data flow.";
8932
- const nextCommand = isOffline ? "moda doctor --online" : "moda init";
8933
- return [
8934
- {
8935
- id: "cloud_api_reachable",
8936
- group: "cloud",
8937
- title: "Moda Cloud API is reachable",
8938
- status,
8939
- severity: "warning",
8940
- evidence,
8941
- remediation,
8942
- nextCommand
8943
- },
8944
- {
8945
- id: "ingest_endpoint_reachable",
8946
- group: "cloud",
8947
- title: "Moda ingest endpoint is reachable",
8948
- status,
8949
- severity: "warning",
8950
- evidence,
8951
- remediation,
8952
- nextCommand
8953
- },
8954
- {
8955
- id: "recent_data_flow",
8956
- group: "cloud",
8957
- title: "Recent traces/logs/runs exist",
8958
- status,
8959
- severity: "warning",
8960
- evidence,
8961
- remediation,
8962
- nextCommand
8963
- },
8964
- {
8965
- id: "cloud_data_flow",
8966
- group: "cloud",
8967
- title: "Moda Cloud data flow",
8968
- status,
8969
- severity: "warning",
8970
- evidence,
8971
- remediation,
8972
- nextCommand
8973
- }
8974
- ];
8975
- }
8976
- async function cloudDataFlowChecks(ingestUrl, apiKey) {
8977
- let overview;
8978
- let cloudApiCheck;
8979
- try {
8980
- overview = await callDataAPI("/overview?days_back=1");
8981
- cloudApiCheck = {
8982
- id: "cloud_api_reachable",
8983
- group: "cloud",
8984
- title: "Moda Cloud API is reachable",
8985
- status: "pass",
8986
- severity: "info",
8987
- evidence: [{ label: "overview", value: "reachable" }]
8988
- };
8989
- } catch (err) {
8990
- const error = err instanceof Error ? err.message : String(err);
8991
- return [
8992
- {
8993
- id: "cloud_api_reachable",
8994
- group: "cloud",
8995
- title: "Moda Cloud API is reachable",
8996
- status: "warning",
8997
- severity: "warning",
8998
- evidence: [{ label: "error", value: error }],
8999
- remediation: "Check API key and base URL.",
9000
- nextCommand: "moda doctor --offline"
9001
- },
9002
- {
9003
- id: "ingest_endpoint_reachable",
9004
- group: "cloud",
9005
- title: "Moda ingest endpoint is reachable",
9006
- status: "skipped",
9007
- severity: "warning",
9008
- evidence: [{ label: "reason", value: "cloud API unreachable" }]
9009
- },
9010
- {
9011
- id: "recent_data_flow",
9012
- group: "cloud",
9013
- title: "Recent traces/logs/runs exist",
9014
- status: "skipped",
9015
- severity: "warning",
9016
- evidence: [{ label: "reason", value: "overview unavailable" }]
9017
- },
9018
- {
9019
- id: "cloud_data_flow",
9020
- group: "cloud",
9021
- title: "Moda Cloud data flow",
9022
- status: "warning",
9023
- severity: "warning",
9024
- evidence: [{ label: "error", value: error }],
9025
- remediation: "Check API key, base URL, and whether data has reached Moda.",
9026
- nextCommand: "moda doctor --offline"
9027
- }
9028
- ];
9029
- }
9030
- const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
9031
- const recentCheck = recentDataFlowCheck(overview);
9032
- const cloudStatus = cloudApiCheck.status === "pass" && ingestCheck.status === "pass" && recentCheck.status === "pass" ? "pass" : "warning";
9033
- return [
9034
- cloudApiCheck,
9035
- ingestCheck,
9036
- recentCheck,
9037
- {
9038
- id: "cloud_data_flow",
9039
- group: "cloud",
9040
- title: "Moda Cloud data flow",
9041
- status: cloudStatus,
9042
- severity: cloudStatus === "pass" ? "info" : "warning",
9043
- evidence: [
9044
- { label: "cloud_api", value: cloudApiCheck.status },
9045
- { label: "ingest", value: ingestCheck.status },
9046
- { label: "recent_data", value: recentCheck.status }
9047
- ],
9048
- remediation: cloudStatus === "pass" ? undefined : "Check API key, ingest URL, and whether data has reached Moda.",
9049
- nextCommand: cloudStatus === "pass" ? undefined : "moda doctor --offline"
9050
- }
9051
- ];
9052
- }
9053
- async function ingestReachabilityCheck(ingestUrl, apiKey) {
9054
- const url = `${ingestUrl.replace(/\/+$/, "")}/`;
9055
- try {
9056
- recordApiRequest();
9057
- const response = await fetch(url, {
9058
- method: "GET",
9059
- headers: { Authorization: `Bearer ${apiKey}` },
9060
- signal: AbortSignal.timeout(1e4)
9061
- });
9062
- return {
9063
- id: "ingest_endpoint_reachable",
9064
- group: "cloud",
9065
- title: "Moda ingest endpoint is reachable",
9066
- status: "pass",
9067
- severity: "info",
9068
- evidence: [
9069
- { label: "url", value: url },
9070
- { label: "http_status", value: String(response.status) }
9071
- ]
9072
- };
9073
- } catch (err) {
9074
- return {
9075
- id: "ingest_endpoint_reachable",
9076
- group: "cloud",
9077
- title: "Moda ingest endpoint is reachable",
9078
- status: "warning",
9079
- severity: "warning",
9080
- evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
9081
- remediation: "Check the configured ingest URL and network reachability.",
9082
- nextCommand: "moda profiles create"
9083
- };
9084
- }
9085
- }
9086
- function recentDataFlowCheck(overview) {
9087
- const data = isRecord(overview) ? overview : {};
9088
- const conversations = numberAt(data, ["conversations", "total"]);
9089
- const toolFailures = numberAt(data, ["tool_failures", "total"]);
9090
- const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
9091
- const lastEventAt = lastActivityTimestamp(recentActivity);
9092
- const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
9093
- return {
9094
- id: "recent_data_flow",
9095
- group: "cloud",
9096
- title: "Recent traces/logs/runs exist",
9097
- status: hasData ? "pass" : "warning",
9098
- severity: hasData ? "info" : "warning",
9099
- evidence: [
9100
- { label: "conversations", value: String(conversations) },
9101
- { label: "tool_failures", value: String(toolFailures) },
9102
- { label: "last_event_at", value: lastEventAt ?? "none" }
9103
- ],
9104
- remediation: hasData ? undefined : "No recent Moda data was visible in the overview window.",
9105
- nextCommand: hasData ? undefined : "moda init"
9106
- };
9107
- }
9108
- function numberAt(value, path) {
9109
- let cursor = value;
9110
- for (const key of path) {
9111
- if (!isRecord(cursor))
9112
- return 0;
9113
- cursor = cursor[key];
9114
- }
9115
- return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
9116
- }
9117
- function lastActivityTimestamp(items) {
9118
- const timestamps = items.map((item) => isRecord(item) && typeof item.timestamp === "string" ? item.timestamp : undefined).filter((item) => Boolean(item)).sort();
9119
- return timestamps.at(-1);
9120
- }
9121
- function summarizeChecks(checks) {
9122
- const fail = checks.filter((check) => check.status === "fail").length;
9123
- const warning = checks.filter((check) => check.status === "warning").length;
9124
- const skipped = checks.filter((check) => check.status === "skipped").length;
9125
- const pass = checks.filter((check) => check.status === "pass").length;
9126
- return {
9127
- status: fail > 0 ? "fail" : warning > 0 ? "warning" : "pass",
9128
- pass,
9129
- warning,
9130
- fail,
9131
- skipped
9132
- };
9133
- }
9134
- function nextCommandsForChecks(checks) {
9135
- return [...new Set(checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => check.nextCommand).filter((command) => Boolean(command)))];
9136
- }
9137
- function firstBrokenCheck(checks) {
9138
- return checks.find((check) => check.status === "fail") ?? checks.find((check) => check.status === "warning");
9139
- }
9140
- function checkStatus(checks, id) {
9141
- return checks.find((check) => check.id === id)?.status ?? "skipped";
9142
- }
9143
- function doctorAgentWriteOptions(report, command) {
9144
- const broken = report.checks.filter((check) => check.status === "fail" || check.status === "warning");
9145
- return {
9146
- summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
9147
- findings: broken.slice(0, 16).map((check) => ({
9148
- id: check.id,
9149
- title: check.title,
9150
- status: check.status,
9151
- severity: check.severity,
9152
- description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
9153
- evidence_ref_ids: [`doctor:${check.id}`]
9154
- })),
9155
- evidenceRefs: report.checks.map((check) => ({
9156
- id: `doctor:${check.id}`,
9157
- kind: "doctor_check",
9158
- label: check.title,
9159
- status: check.status
9160
- })),
9161
- recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
9162
- id: `doctor_action_${index + 1}_${item.checkId}`,
9163
- title: item.action,
9164
- command: item.command,
9165
- rationale: `Doctor check ${item.checkId} is not passing.`,
9166
- mutability: item.command ? agentMutabilityForCommand(item.command) : "unknown",
9167
- requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true
9168
- })),
9169
- nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
9170
- command: nextCommand,
9171
- purpose: agentPurposeForCommand(nextCommand),
9172
- mutability: agentMutabilityForCommand(nextCommand),
9173
- requires_approval: agentCommandRequiresApproval(nextCommand)
9174
- })),
9175
- warnings: broken.filter((check) => check.status === "warning").slice(0, 16).map((check) => `${check.title}: ${check.remediation ?? check.status}`)
9176
- };
9177
- }
9178
- function agentMutabilityForCommand(command) {
9179
- if (/\bsync\b/.test(command))
9180
- return "sync";
9181
- if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
9182
- return "write";
9183
- return "read";
9184
- }
9185
- function agentCommandRequiresApproval(command) {
9186
- return agentMutabilityForCommand(command) !== "read" && !command.includes("--dry-run");
9187
- }
9188
- function agentPurposeForCommand(command) {
9189
- if (command.includes("doctor"))
9190
- return "Re-run Moda diagnostics.";
9191
- if (command.includes("harness approve"))
9192
- return "Approve the validated harness report after review.";
9193
- if (command.includes("harness sync"))
9194
- return "Sync harness state to Moda Cloud.";
9195
- if (command.includes("harness scan"))
9196
- return "Refresh the local harness manifest.";
9197
- if (command.includes("init"))
9198
- return "Run Moda onboarding.";
9199
- return "Inspect or repair Moda setup.";
9200
- }
9201
- function stableGraphHash(graph) {
9202
- return createHash3("sha256").update(JSON.stringify(sortJson({
9203
- ...graph,
9204
- scan: {
9205
- ...graph.scan,
9206
- createdAt: undefined,
9207
- scannerVersion: undefined,
9208
- skipped: undefined
9209
- }
9210
- }))).digest("hex");
9211
- }
9212
- function sortJson(value) {
9213
- if (Array.isArray(value))
9214
- return value.map((item) => sortJson(item));
9215
- if (value && typeof value === "object") {
9216
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
9217
- }
9218
- return value;
9219
- }
9220
- function isRecord(value) {
9221
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9222
- }
9223
-
9224
- export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, runSkillSync, runSkillsCommand, runDoctorCommand, runStatusCommand, buildDoctorReport };
7509
+ export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, writePromptSyncProgress, discoverPrompts, inspectPromptManifest, promptManifestWarnings, migratePromptManifest, importHarnessPromptSources, readPromptLock, parsePromptFileForAb, assertReplayablePrompt };