@moda-ai/cli 1.30.2 → 1.31.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,16 @@ export MODA_API_KEY="moda_sk_..."
32
32
 
33
33
  Get your API key from [moda.dev/settings](https://moda.dev/settings).
34
34
 
35
+ ## Diagnostics
36
+
37
+ `moda doctor --online` checks configuration and cloud data flow. Experimental
38
+ harness report citations, approval, and report sync do not affect normal doctor
39
+ or status health; use the explicit harness commands to validate those reports.
40
+ The legacy `status.harnessReport` JSON field is retained as `skipped`. Source
41
+ folder presence is not evidence of SDK initialization, so doctor does not warn
42
+ about instrumentation solely because a `src` or `app` directory exists. Verify
43
+ your integration with a fresh application conversation and `moda audit`.
44
+
35
45
  ## Usage
36
46
 
37
47
  Find things by meaning, then drill in:
@@ -126,6 +136,11 @@ Prompt management is code-first. Keep prompts in `prompts/**/*.prompt.md` or
126
136
  `prompts/**/*.prompt.json`, sync them with the CLI, and render them through the
127
137
  SDK so LLM spans include prompt metadata.
128
138
 
139
+ JSON definitions can provide `content`, `systemPrompt`, or `messages`. Existing
140
+ files with a nonempty text `template` also work: the CLI uses it as content when
141
+ none of those fields supplies usable text. Sync preserves placeholders and
142
+ leaves the source file unchanged; empty definitions still produce an error.
143
+
129
144
  ```bash
130
145
  moda prompts init
131
146
  moda prompts status
@@ -1654,7 +1654,7 @@ function citationExcerptMatches(expected, actualLine) {
1654
1654
  return expectedNormalized.includes(actualNormalized) || actualNormalized.includes(expectedNormalized);
1655
1655
  }
1656
1656
  function normalizeComparableText(value) {
1657
- return value.replace(/\s+/g, " ").trim().slice(0, 240);
1657
+ return value.replace(/\s+/g, " ").trim();
1658
1658
  }
1659
1659
  function validateCitation(citation, rootDir, errors, warnings, options) {
1660
1660
  if (citation.type === "command_output" || citation.type === "production_evidence") {
@@ -3040,7 +3040,6 @@ function renderStatusHuman(status, options = {}) {
3040
3040
  lines.push(` Data flow ${ui.status(status.dataFlow)}`);
3041
3041
  lines.push(` Harness ${ui.status(status.harness)}`);
3042
3042
  lines.push(` Harness sync ${ui.status(status.harnessSync)}`);
3043
- lines.push(` Harness report ${ui.status(status.harnessReport)}`);
3044
3043
  lines.push(` Shadow Spark ${ui.status(status.shadowSpark)}`);
3045
3044
  lines.push(` Prompts ${ui.status(status.prompts)}`);
3046
3045
  lines.push(` Skills ${ui.status(status.skills)}`);
@@ -3546,7 +3545,7 @@ async function runHarnessCommand(context) {
3546
3545
  elapsed_ms: Date.now() - context.startedAt
3547
3546
  });
3548
3547
  if (context.flags["github-actions"] === "true") {
3549
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-ze17s10y.js");
3548
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-89sn0hqd.js");
3550
3549
  const result = await runGithubActionsAnalyze(rootDir, {
3551
3550
  writeReport: (report2) => {
3552
3551
  const normalized = normalizeHarnessReport(report2);
@@ -4237,9 +4236,22 @@ function repairCitationLines(report, rootDir) {
4237
4236
  const currentLine = typeof citation.line === "number" ? lines[citation.line - 1] : undefined;
4238
4237
  if (currentLine && citationLineMatches(currentLine, excerpt))
4239
4238
  continue;
4240
- const matchedIndex = lines.findIndex((line) => citationLineMatches(line, excerpt));
4241
- if (matchedIndex >= 0)
4242
- citation.line = matchedIndex + 1;
4239
+ const needle = normalizeComparableLine(excerpt);
4240
+ if (!needle)
4241
+ continue;
4242
+ const sourceLines = lines.map((line, index) => ({ text: normalizeComparableLine(line), index })).filter((line) => line.text.length > 0);
4243
+ const source = sourceLines.map((line) => line.text).join(" ");
4244
+ const match = source.indexOf(needle);
4245
+ if (match < 0 || source.indexOf(needle, match + 1) >= 0)
4246
+ continue;
4247
+ let offset = 0;
4248
+ for (const line of sourceLines) {
4249
+ if (match >= offset && match < offset + line.text.length) {
4250
+ citation.line = line.index + 1;
4251
+ break;
4252
+ }
4253
+ offset += line.text.length + 1;
4254
+ }
4243
4255
  }
4244
4256
  }
4245
4257
  function citationLineMatches(line, excerpt) {
@@ -4745,7 +4757,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
4745
4757
  if (options.adapter === "claude") {
4746
4758
  const extraArgs = externalAnalystExtraArgs(options.adapter);
4747
4759
  const model = resolveAnalystModel(options.adapter, options.flags);
4748
- const permission = mode === "write" ? ["--permission-mode", "acceptEdits", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash"] : ["--allowedTools", `Read,Grep,Glob,Write(${HARNESS_REPORT_PATH}),Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)`];
4760
+ const permission = mode === "write" ? ["--permission-mode", "acceptEdits", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash"] : ["--allowedTools", `Read,Grep,Glob,Edit(${HARNESS_REPORT_PATH}),Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)`];
4749
4761
  return {
4750
4762
  adapter: options.adapter,
4751
4763
  command,
@@ -7773,4 +7785,4 @@ async function remoteAnalyzeFetch(url, apiKey, init, timeoutMs) {
7773
7785
  return response.json();
7774
7786
  }
7775
7787
 
7776
- export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, shouldColorTerminalOutput, terminalStyles, stripAnsi, renderMarkdownForTerminal, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, validateHarnessReport, readHarnessReport, hashHarnessReport, hashStableJson, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, renderDoctorHuman, renderStatusHuman, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, describeCodingAgentEvent, isDeterministicLocalAnalyst, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, scanHarness, summarizeHarness, buildSourceSnapshot, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, graphForSync };
7788
+ export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, shouldColorTerminalOutput, terminalStyles, stripAnsi, renderMarkdownForTerminal, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, validateHarnessReport, readHarnessReport, hashHarnessReport, hashStableJson, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, renderDoctorHuman, renderStatusHuman, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, describeCodingAgentEvent, isDeterministicLocalAnalyst, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, scanHarness, summarizeHarness, buildSourceSnapshot, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, graphForSync };
@@ -1,17 +1,11 @@
1
1
  import {
2
- HARNESS_REPORT_APPROVAL_PATH,
3
- HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
4
- HARNESS_REPORT_PATH,
5
2
  callControlAPI,
6
3
  callDataAPI,
7
- hashHarnessReport,
8
- readHarnessReport,
9
4
  readHarnessSyncState,
10
5
  renderDoctorHuman,
11
6
  renderStatusHuman,
12
- scanHarness,
13
- validateHarnessReport
14
- } from "./cli-ts6nhn51.js";
7
+ scanHarness
8
+ } from "./cli-76tsfpta.js";
15
9
  import {
16
10
  isAuthSessionValid,
17
11
  loadAuthSession
@@ -772,7 +766,19 @@ function parsePromptFile(absPath) {
772
766
  if (absPath.endsWith(".json")) {
773
767
  const parsed = JSON.parse(raw);
774
768
  const key2 = parsed.key || keyFromPath(sourcePath);
775
- const content = parsed.content || JSON.stringify(parsed.messages ?? [], null, 2);
769
+ const hasContent = typeof parsed.content === "string" && parsed.content.trim().length > 0;
770
+ const hasSystemPrompt = typeof parsed.systemPrompt === "string" && parsed.systemPrompt.trim().length > 0;
771
+ const hasTemplate = typeof parsed.template === "string" && parsed.template.trim().length > 0;
772
+ const hasMessages = Array.isArray(parsed.messages) && parsed.messages.some((message) => {
773
+ if (!message || typeof message !== "object" || !("role" in message) || !["system", "developer", "user", "assistant", "tool"].includes(String(message.role)) || !("content" in message))
774
+ return false;
775
+ const content2 = message.content;
776
+ 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);
777
+ });
778
+ if (!hasContent && !hasSystemPrompt && !hasMessages && !hasTemplate) {
779
+ throw new Error(`${sourcePath}: prompt JSON requires non-empty content, systemPrompt, messages, or a text template.`);
780
+ }
781
+ const content = hasContent ? parsed.content : hasSystemPrompt ? parsed.systemPrompt : hasMessages ? JSON.stringify(parsed.messages, null, 2) : parsed.template;
776
782
  return normalizePrompt({ ...parsed, key: key2, name: parsed.name || key2, sourcePath, content });
777
783
  }
778
784
  const { frontmatter, body } = parseFrontmatter(raw);
@@ -931,11 +937,18 @@ function printJson(value) {
931
937
 
932
938
  // src/init/rules.ts
933
939
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
940
+ import { join as join3 } from "node:path";
941
+ import { confirm } from "@inquirer/prompts";
942
+
943
+ // src/package-assets.ts
934
944
  import { dirname as dirname2, join as join2 } from "node:path";
935
945
  import { fileURLToPath } from "node:url";
936
- import { confirm } from "@inquirer/prompts";
937
- var PACKAGE_ROOT = join2(dirname2(fileURLToPath(import.meta.url)), "..", "..");
938
- var SKILLS_SOURCE_DIR = join2(PACKAGE_ROOT, "skills");
946
+ function packageAssetPath(...parts) {
947
+ return join2(dirname2(fileURLToPath(import.meta.url)), "..", ...parts);
948
+ }
949
+
950
+ // src/init/rules.ts
951
+ var SKILLS_SOURCE_DIR = packageAssetPath("skills");
939
952
  var SKILLS = [
940
953
  {
941
954
  id: "moda-cli",
@@ -946,7 +959,7 @@ var ADAPTERS = {
946
959
  "Claude Code": {
947
960
  name: "Claude Code",
948
961
  targetFor: (skillId) => ({
949
- dir: join2(".claude", "skills", skillId),
962
+ dir: join3(".claude", "skills", skillId),
950
963
  filename: "SKILL.md"
951
964
  }),
952
965
  transform: (content) => content
@@ -954,7 +967,7 @@ var ADAPTERS = {
954
967
  Cursor: {
955
968
  name: "Cursor",
956
969
  targetFor: (skillId) => ({
957
- dir: join2(".cursor", "rules"),
970
+ dir: join3(".cursor", "rules"),
958
971
  filename: `${skillId}.mdc`
959
972
  }),
960
973
  transform: claudeSkillToCursorRule
@@ -979,7 +992,7 @@ function claudeSkillToCursorRule(skillContent) {
979
992
  ${body.replace(/^\s+/, "")}`;
980
993
  }
981
994
  function loadSkillSource(skillId) {
982
- const path = join2(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
995
+ const path = join3(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
983
996
  if (!existsSync3(path)) {
984
997
  throw new Error(`Skill source missing: ${path}`);
985
998
  }
@@ -987,9 +1000,9 @@ function loadSkillSource(skillId) {
987
1000
  }
988
1001
  function installSkillForAgent(skill, adapter, content) {
989
1002
  const { dir, filename } = adapter.targetFor(skill.id);
990
- const dirPath = join2(process.cwd(), dir);
991
- const filePath = join2(dirPath, filename);
992
- const relativePath = join2(dir, filename);
1003
+ const dirPath = join3(process.cwd(), dir);
1004
+ const filePath = join3(dirPath, filename);
1005
+ const relativePath = join3(dir, filename);
993
1006
  try {
994
1007
  mkdirSync2(dirPath, { recursive: true });
995
1008
  if (existsSync3(filePath)) {
@@ -1075,7 +1088,7 @@ async function installRules(detectedAgents, options = {}) {
1075
1088
 
1076
1089
  // src/init/mcp.ts
1077
1090
  import { existsSync as existsSync4 } from "node:fs";
1078
- import { join as join3 } from "node:path";
1091
+ import { join as join4 } from "node:path";
1079
1092
  import { execSync } from "node:child_process";
1080
1093
  function isOnPath(cmd) {
1081
1094
  try {
@@ -1088,10 +1101,10 @@ function isOnPath(cmd) {
1088
1101
  function detectAgents() {
1089
1102
  const cwd = process.cwd();
1090
1103
  const agents = [];
1091
- if (existsSync4(join3(cwd, ".claude")) || isOnPath("claude")) {
1104
+ if (existsSync4(join4(cwd, ".claude")) || isOnPath("claude")) {
1092
1105
  agents.push("Claude Code");
1093
1106
  }
1094
- if (existsSync4(join3(cwd, ".cursor"))) {
1107
+ if (existsSync4(join4(cwd, ".cursor"))) {
1095
1108
  agents.push("Cursor");
1096
1109
  }
1097
1110
  return agents;
@@ -1108,7 +1121,7 @@ import {
1108
1121
  readFileSync as readFileSync5,
1109
1122
  writeFileSync as writeFileSync6
1110
1123
  } from "node:fs";
1111
- import { basename, dirname as dirname5, join as join6, relative as relative3 } from "node:path";
1124
+ import { basename, dirname as dirname5, join as join7, relative as relative3 } from "node:path";
1112
1125
 
1113
1126
  // src/skills-manifest.ts
1114
1127
  import {
@@ -1120,12 +1133,12 @@ import {
1120
1133
  unlinkSync,
1121
1134
  writeFileSync as writeFileSync4
1122
1135
  } from "node:fs";
1123
- import { dirname as dirname3, isAbsolute as isAbsolute4, join as join4, normalize, relative as relative2, resolve as resolve4 } from "node:path";
1124
- var SKILLS_MANIFEST_PATH = join4(".moda", "skills.yml");
1136
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join5, normalize, relative as relative2, resolve as resolve4 } from "node:path";
1137
+ var SKILLS_MANIFEST_PATH = join5(".moda", "skills.yml");
1125
1138
  var SKILL_SOURCE_HARNESS = "moda";
1126
1139
  var SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
1127
1140
  function readSkillsManifest(cwd) {
1128
- const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
1141
+ const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
1129
1142
  if (!existsSync5(manifestPath))
1130
1143
  return null;
1131
1144
  let text = "";
@@ -1156,7 +1169,7 @@ function manifestHeaderFrom(manifest) {
1156
1169
  };
1157
1170
  }
1158
1171
  function writeSkillsManifest(cwd, header, records) {
1159
- const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
1172
+ const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
1160
1173
  const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
1161
1174
  const lines = [
1162
1175
  "version: 1",
@@ -1211,7 +1224,7 @@ function pruneManagedSkillPaths(cwd, managedPaths, log) {
1211
1224
  function pruneEmptyClaudeSkillDir(cwd, relativePath) {
1212
1225
  if (!relativePath.startsWith(".claude/skills/") || !relativePath.endsWith("/SKILL.md"))
1213
1226
  return;
1214
- const dir = dirname3(join4(cwd, relativePath));
1227
+ const dir = dirname3(join5(cwd, relativePath));
1215
1228
  try {
1216
1229
  if (existsSync5(dir) && readdirSync2(dir).length === 0) {
1217
1230
  rmdirSync(dir);
@@ -1331,7 +1344,7 @@ function yamlQuote(value) {
1331
1344
 
1332
1345
  // src/agents-md.ts
1333
1346
  import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1334
- import { dirname as dirname4, join as join5 } from "node:path";
1347
+ import { dirname as dirname4, join as join6 } from "node:path";
1335
1348
  var AGENTS_MD_FILENAME = "AGENTS.md";
1336
1349
  var FIXES_BLOCK_BEGIN = "<!-- moda:fixes:begin -->";
1337
1350
  var FIXES_BLOCK_END = "<!-- moda:fixes:end -->";
@@ -1392,7 +1405,7 @@ function writeFileIfChanged(filePath, content) {
1392
1405
  }
1393
1406
  }
1394
1407
  function upsertFixesAgentsBlock(cwd) {
1395
- const filePath = join5(cwd, AGENTS_MD_FILENAME);
1408
+ const filePath = join6(cwd, AGENTS_MD_FILENAME);
1396
1409
  let existing = null;
1397
1410
  if (existsSync6(filePath)) {
1398
1411
  try {
@@ -1408,7 +1421,7 @@ function upsertFixesAgentsBlock(cwd) {
1408
1421
  // src/skills.ts
1409
1422
  var SKILLS_REQUEST_TIMEOUT_MS = 15000;
1410
1423
  var VALID_STATUSES = new Set(["approved", "proposed", "all"]);
1411
- var SKILL_PROPOSALS_MANIFEST_PATH = join6(".moda", "skill-proposals.yml");
1424
+ var SKILL_PROPOSALS_MANIFEST_PATH = join7(".moda", "skill-proposals.yml");
1412
1425
  async function fetchSkills(status) {
1413
1426
  const apiKey = resolveApiKey();
1414
1427
  if (!apiKey) {
@@ -1578,7 +1591,7 @@ async function pullSkills(options = {}) {
1578
1591
  }
1579
1592
  }
1580
1593
  const agents = detectAgents();
1581
- const writeCursor = agents.includes("Cursor") || existsSync7(join6(cwd, ".cursor"));
1594
+ const writeCursor = agents.includes("Cursor") || existsSync7(join7(cwd, ".cursor"));
1582
1595
  const records = [];
1583
1596
  for (const skill of skills) {
1584
1597
  if (!skill.skill_id || !skill.skill_md) {
@@ -1591,7 +1604,7 @@ async function pullSkills(options = {}) {
1591
1604
  log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
1592
1605
  continue;
1593
1606
  }
1594
- const claudePath = join6(cwd, ".claude", "skills", safeId, "SKILL.md");
1607
+ const claudePath = join7(cwd, ".claude", "skills", safeId, "SKILL.md");
1595
1608
  const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
1596
1609
  const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
1597
1610
  recordOutcome(summary, claudeStatus, log, claudeRelativePath);
@@ -1599,7 +1612,7 @@ async function pullSkills(options = {}) {
1599
1612
  if (claudeStatus !== "failed")
1600
1613
  paths.push(claudeRelativePath);
1601
1614
  if (writeCursor) {
1602
- const cursorPath = join6(cwd, ".cursor", "rules", `${safeId}.mdc`);
1615
+ const cursorPath = join7(cwd, ".cursor", "rules", `${safeId}.mdc`);
1603
1616
  const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
1604
1617
  const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
1605
1618
  recordOutcome(summary, cursorStatus, log, cursorRelativePath);
@@ -1637,7 +1650,7 @@ function recordOutcome(summary, status, log, relativePath) {
1637
1650
  break;
1638
1651
  }
1639
1652
  }
1640
- var SKILLS_DIR = join6(".claude", "skills");
1653
+ var SKILLS_DIR = join7(".claude", "skills");
1641
1654
  var SKILL_FILE = "SKILL.md";
1642
1655
  var MAX_SKILL_WALK_DEPTH = 4;
1643
1656
  async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
@@ -1668,7 +1681,7 @@ async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
1668
1681
  return { synced: response.synced ?? [], dryRun, ...agentsMd ? { agentsMd } : {} };
1669
1682
  }
1670
1683
  function discoverLocalSkills(cwd) {
1671
- const skillsRoot = join6(cwd, SKILLS_DIR);
1684
+ const skillsRoot = join7(cwd, SKILLS_DIR);
1672
1685
  if (!existsSync7(skillsRoot))
1673
1686
  return [];
1674
1687
  const managed = new Set(managedSkillPaths(cwd));
@@ -1717,7 +1730,7 @@ function walkSkillFiles(dir, depth) {
1717
1730
  }
1718
1731
  const files = [];
1719
1732
  for (const entry of entries) {
1720
- const fullPath = join6(dir, entry);
1733
+ const fullPath = join7(dir, entry);
1721
1734
  let stat;
1722
1735
  try {
1723
1736
  stat = lstatSync2(fullPath);
@@ -1791,7 +1804,7 @@ async function applySkillProposal(proposalId, flags, profileOptions = {}, option
1791
1804
  }
1792
1805
  const safeId = safeSkillId(proposal.skillId);
1793
1806
  const relativePath = `.claude/skills/${safeId}/SKILL.md`;
1794
- const writeStatus = writeIfChanged(join6(cwd, relativePath), proposal.skillMd);
1807
+ const writeStatus = writeIfChanged(join7(cwd, relativePath), proposal.skillMd);
1795
1808
  if (writeStatus === "failed") {
1796
1809
  throw new Error(`Failed to write ${relativePath}`);
1797
1810
  }
@@ -1824,7 +1837,7 @@ function safeSkillId(skillId) {
1824
1837
  return safeId;
1825
1838
  }
1826
1839
  function writeSkillProposalManifest(cwd, proposal, relativePath) {
1827
- const manifestPath = join6(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
1840
+ const manifestPath = join7(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
1828
1841
  mkdirSync5(dirname5(manifestPath), { recursive: true });
1829
1842
  const existing = existsSync7(manifestPath) ? readFileSync5(manifestPath, "utf8") : `applied_proposals:
1830
1843
  `;
@@ -2085,7 +2098,7 @@ function sha2562(input) {
2085
2098
  // src/doctor.ts
2086
2099
  import { createHash as createHash3 } from "node:crypto";
2087
2100
  import { existsSync as existsSync8, readFileSync as readFileSync6, statSync } from "node:fs";
2088
- import { join as join7 } from "node:path";
2101
+ import { join as join8 } from "node:path";
2089
2102
  async function runDoctorCommand(context) {
2090
2103
  const report = await buildDoctorReport(context, {
2091
2104
  offline: context.flags.offline === "true"
@@ -2122,7 +2135,7 @@ async function runStatusCommand(context) {
2122
2135
  dataFlow: checkStatus(report.checks, "cloud_data_flow"),
2123
2136
  harness: checkStatus(report.checks, "harness_manifest_current"),
2124
2137
  harnessSync: checkStatus(report.checks, "harness_sync_state_present"),
2125
- harnessReport: checkStatus(report.checks, "harness_report_approved_synced"),
2138
+ harnessReport: "skipped",
2126
2139
  shadowSpark: checkStatus(report.checks, "shadow_spark_workspace_health"),
2127
2140
  prompts: checkStatus(report.checks, "prompts_discovered"),
2128
2141
  skills: checkStatus(report.checks, "skills_discovered"),
@@ -2208,7 +2221,7 @@ async function buildDoctorReport(context, options) {
2208
2221
  checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
2209
2222
  checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
2210
2223
  checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
2211
- const manifestPath = join7(context.cwd, ".moda", "harness.json");
2224
+ const manifestPath = join8(context.cwd, ".moda", "harness.json");
2212
2225
  const manifestExists = existsSync8(manifestPath);
2213
2226
  checks.push({
2214
2227
  id: "harness_manifest_exists",
@@ -2222,7 +2235,6 @@ async function buildDoctorReport(context, options) {
2222
2235
  });
2223
2236
  checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
2224
2237
  checks.push(harnessSyncStateCheck(context.cwd));
2225
- checks.push(harnessReportStateCheck(context.cwd));
2226
2238
  checks.push(discoveryCheck("prompts_discovered", "harness", "Prompts are discovered", context.cwd, [
2227
2239
  "prompts",
2228
2240
  ".moda/prompts.yml"
@@ -2237,7 +2249,6 @@ async function buildDoctorReport(context, options) {
2237
2249
  "tests",
2238
2250
  "__tests__"
2239
2251
  ]));
2240
- checks.push(sdkInitializationCheck(context.cwd));
2241
2252
  checks.push(validationEventPathCheck(ingestUrl));
2242
2253
  if (options.offline || !apiKey) {
2243
2254
  checks.push(...offlineCloudChecks(options.offline));
@@ -2497,120 +2508,8 @@ function harnessSyncStateCheck(cwd) {
2497
2508
  ]
2498
2509
  };
2499
2510
  }
2500
- function harnessReportStateCheck(cwd) {
2501
- const reportPath = join7(cwd, HARNESS_REPORT_PATH);
2502
- if (!existsSync8(reportPath)) {
2503
- return {
2504
- id: "harness_report_approved_synced",
2505
- group: "harness",
2506
- title: "Harness analyst report is approved and synced",
2507
- status: "warning",
2508
- severity: "warning",
2509
- evidence: [{ label: "path", value: HARNESS_REPORT_PATH }],
2510
- remediation: "Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.",
2511
- nextCommand: "moda harness analyze --experimental --summary"
2512
- };
2513
- }
2514
- try {
2515
- const report = readHarnessReport(reportPath);
2516
- const validation = validateHarnessReport(report, cwd);
2517
- const reportHash = hashHarnessReport(report);
2518
- const evidence = [
2519
- { label: "path", value: HARNESS_REPORT_PATH },
2520
- { label: "report_hash", value: reportHash.slice(0, 12) },
2521
- { label: "validation", value: validation.status }
2522
- ];
2523
- if (validation.status !== "pass") {
2524
- return {
2525
- id: "harness_report_approved_synced",
2526
- group: "harness",
2527
- title: "Harness analyst report is approved and synced",
2528
- status: "fail",
2529
- severity: "critical",
2530
- evidence: [
2531
- ...evidence,
2532
- { label: "errors", value: validation.errors.slice(0, 3).join(" | ") || "validation failed" }
2533
- ],
2534
- remediation: "Regenerate or fix the harness report before approving or syncing.",
2535
- nextCommand: "moda harness analyze --experimental --summary"
2536
- };
2537
- }
2538
- const approval = readHarnessReportApproval(cwd);
2539
- const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION && approval.reportId === report.reportId && approval.reportHash === reportHash && approval.approved === true;
2540
- evidence.push({ label: "approval", value: approved ? "approved" : approval ? "stale_or_invalid" : "missing" });
2541
- if (approval && !approved) {
2542
- return {
2543
- id: "harness_report_approved_synced",
2544
- group: "harness",
2545
- title: "Harness analyst report is approved and synced",
2546
- status: "fail",
2547
- severity: "critical",
2548
- evidence,
2549
- remediation: "Approval no longer matches the current report. Review and approve the report again.",
2550
- nextCommand: "moda harness approve --yes"
2551
- };
2552
- }
2553
- if (!approved) {
2554
- return {
2555
- id: "harness_report_approved_synced",
2556
- group: "harness",
2557
- title: "Harness analyst report is approved and synced",
2558
- status: "warning",
2559
- severity: "warning",
2560
- evidence,
2561
- remediation: "Review and approve the validated harness report before syncing.",
2562
- nextCommand: "moda harness approve --yes"
2563
- };
2564
- }
2565
- const state = readHarnessSyncState(cwd);
2566
- const synced = state?.source === "approved_report" && state.report?.reportHash === reportHash;
2567
- 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) }] : []);
2568
- return {
2569
- id: "harness_report_approved_synced",
2570
- group: "harness",
2571
- title: "Harness analyst report is approved and synced",
2572
- status: synced ? "pass" : "warning",
2573
- severity: synced ? "info" : "warning",
2574
- evidence,
2575
- remediation: synced ? undefined : "Sync the approved harness report so Moda cloud has the same harness graph.",
2576
- nextCommand: synced ? undefined : "moda harness sync --from-report .moda/harness-report.json"
2577
- };
2578
- } catch (err) {
2579
- return {
2580
- id: "harness_report_approved_synced",
2581
- group: "harness",
2582
- title: "Harness analyst report is approved and synced",
2583
- status: "fail",
2584
- severity: "critical",
2585
- evidence: [
2586
- { label: "path", value: HARNESS_REPORT_PATH },
2587
- { label: "error", value: err instanceof Error ? err.message : String(err) }
2588
- ],
2589
- remediation: "Regenerate the harness report.",
2590
- nextCommand: "moda harness analyze --experimental --summary"
2591
- };
2592
- }
2593
- }
2594
- function readHarnessReportApproval(cwd) {
2595
- const approvalPath = join7(cwd, HARNESS_REPORT_APPROVAL_PATH);
2596
- if (!existsSync8(approvalPath))
2597
- return null;
2598
- try {
2599
- return JSON.parse(readFileSync6(approvalPath, "utf8"));
2600
- } catch {
2601
- return {
2602
- schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
2603
- reportId: "unreadable",
2604
- reportHash: "unreadable",
2605
- approved: true,
2606
- approvedAt: "",
2607
- approvedBy: "local-human",
2608
- reportPath: HARNESS_REPORT_PATH
2609
- };
2610
- }
2611
- }
2612
2511
  function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
2613
- const found = relativePaths.filter((path) => existsSync8(join7(cwd, path)));
2512
+ const found = relativePaths.filter((path) => existsSync8(join8(cwd, path)));
2614
2513
  return {
2615
2514
  id,
2616
2515
  group,
@@ -2622,29 +2521,6 @@ function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
2622
2521
  nextCommand: found.length > 0 ? undefined : nextCommand
2623
2522
  };
2624
2523
  }
2625
- function sdkInitializationCheck(cwd) {
2626
- const candidates = ["src", "app", "agent_harness"];
2627
- let detected = false;
2628
- for (const candidate of candidates) {
2629
- const path = join7(cwd, candidate);
2630
- if (!existsSync8(path))
2631
- continue;
2632
- try {
2633
- const stat = statSync(path);
2634
- detected ||= stat.isDirectory() || stat.isFile();
2635
- } catch {}
2636
- }
2637
- return {
2638
- id: "sdk_initialization_detected",
2639
- group: "harness",
2640
- title: "SDK initialization is detectable",
2641
- status: detected ? "warning" : "skipped",
2642
- severity: "warning",
2643
- evidence: [{ label: "status", value: detected ? "code present, instrumentation not verified" : "no source candidates" }],
2644
- remediation: "Use the generated integration guide to add Moda SDK instrumentation.",
2645
- nextCommand: "moda init"
2646
- };
2647
- }
2648
2524
  function validationEventPathCheck(ingestUrl) {
2649
2525
  return {
2650
2526
  id: "validation_event_path_defined",
@@ -2950,4 +2826,4 @@ function isRecord(value) {
2950
2826
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2951
2827
  }
2952
2828
 
2953
- export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, upsertSkillManifestRecord, runSkillSync, runSkillsCommand, runDoctorCommand, runStatusCommand, buildDoctorReport };
2829
+ 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 };
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-c1vhkg8n.js";
11
+ } from "./cli-aeggte1q.js";
12
12
  import {
13
13
  ApiError,
14
14
  HARNESS_REPORT_APPROVAL_PATH,
@@ -29,7 +29,7 @@ import {
29
29
  summarizeHarness,
30
30
  terminalStyles,
31
31
  validateHarnessReport
32
- } from "./cli-ts6nhn51.js";
32
+ } from "./cli-76tsfpta.js";
33
33
  import {
34
34
  authFetch,
35
35
  clearAuthSession,
@@ -152,6 +152,29 @@ var ProblemsSchema = z.object({
152
152
  limit: z.number().min(1).max(25).default(25).optional()
153
153
  });
154
154
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
155
+ var ProblemImpactSchema = z.object({
156
+ node_id: z.string().min(1).max(256).optional(),
157
+ problem_id: z.string().regex(UUID_RE).optional(),
158
+ run_id: z.string().min(1).max(256).optional(),
159
+ days_back: z.union([z.literal("all"), z.coerce.number().refine((n) => [1, 3, 7, 30, 90].includes(n), "Use all, 1, 3, 7, 30, or 90 days")]).default(7),
160
+ mode: z.enum(["metrics", "trend", "evidence", "catalog", "overview", "affected-use-cases", "trace-use-cases"]).default("metrics"),
161
+ to: z.string().datetime({ precision: 3 }).optional(),
162
+ cursor: z.string().min(1).optional(),
163
+ conversation_ids: z.string().transform((value, ctx) => {
164
+ try {
165
+ return z.array(z.string().min(1).max(512)).min(1).max(25).parse(JSON.parse(value));
166
+ } catch {
167
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Use a JSON array of 1–25 conversation IDs" });
168
+ return z.NEVER;
169
+ }
170
+ }).optional()
171
+ }).refine((p) => p.mode !== "trace-use-cases" || p.problem_id && p.conversation_ids, {
172
+ message: "Trace use cases require --problem-id and --conversation-ids"
173
+ }).refine((p) => p.node_id || p.problem_id || ["catalog", "overview"].includes(p.mode), {
174
+ message: "Select --node-id or --problem-id, or use --mode=catalog/overview"
175
+ }).refine((p) => !p.cursor || p.mode === "evidence" && p.to && p.run_id, {
176
+ message: "An evidence cursor requires --mode=evidence, --to, and --run-id from the previous response"
177
+ });
155
178
  var PROBLEM_FAMILIES = [
156
179
  "tool_failure",
157
180
  "emotion",
@@ -5356,6 +5379,7 @@ Commands:
5356
5379
  tool-failures Get tool failure overview
5357
5380
  tool-failure-detail <tool_name> Get per-tool failure detail
5358
5381
  problems Rank cross-signal Problems by root cause (what to fix first)
5382
+ problem-impact Live problem/use-case impact (--node-id, --problem-id, --mode, --days-back)
5359
5383
  problem <problem_id> Open one Problem: dossier, or --evidence/--reports/--traces/--feedback
5360
5384
  problem-feedback <problem_id> Mark a Problem fixed, dismiss, rename, or flag a bad attribution
5361
5385
  step-scores <conversation_id> Graph-PRM step scores (per-segment curves, first bad step, rollup)
@@ -5573,7 +5597,7 @@ Examples:
5573
5597
  moda --agent doctor
5574
5598
  moda agent context
5575
5599
  moda agent next-action
5576
- moda agent evidence doctor:harness_report_approved_synced
5600
+ moda agent evidence doctor:api_key_resolves
5577
5601
  moda investigate
5578
5602
  moda failures
5579
5603
  moda ask "what should I fix first?"
@@ -6221,6 +6245,35 @@ async function runCommand(command, positional, flags, positionals = positional ?
6221
6245
  await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings);
6222
6246
  break;
6223
6247
  }
6248
+ case "problem-impact": {
6249
+ const params = ProblemImpactSchema.parse(Object.fromEntries(Object.entries(flags).map(([key, value]) => [key.replace(/-/g, "_"), value])));
6250
+ const query = new URLSearchParams({ days: String(params.days_back), mode: params.mode });
6251
+ for (const [key, value] of Object.entries({
6252
+ nodeId: params.node_id,
6253
+ problemId: params.problem_id,
6254
+ runId: params.run_id,
6255
+ to: params.to,
6256
+ cursor: params.cursor,
6257
+ conversationIds: params.conversation_ids ? JSON.stringify(params.conversation_ids) : undefined
6258
+ }))
6259
+ if (value !== undefined)
6260
+ query.set(key, value);
6261
+ const data = asRecord(await callDataAPI(`/problem-impact?${query}`)) ?? {};
6262
+ const link = new URLSearchParams({ impactDays: String(params.days_back) });
6263
+ const window = asRecord(data.window);
6264
+ const resolvedRun = asString(data.runId);
6265
+ const windowEnd = asString(window?.to);
6266
+ if (resolvedRun)
6267
+ link.set("run", resolvedRun);
6268
+ if (windowEnd)
6269
+ link.set("impactTo", windowEnd);
6270
+ if (params.node_id)
6271
+ link.set("useCase", params.node_id);
6272
+ const problem = asString(data.problemId) || params.problem_id;
6273
+ const path = problem ? `/dashboard/problems/${encodeURIComponent(problem)}` : params.node_id ? `/dashboard/use-cases/${encodeURIComponent(params.node_id)}` : "/dashboard/use-cases";
6274
+ context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` });
6275
+ break;
6276
+ }
6224
6277
  case "problems": {
6225
6278
  const args = flagsToArgs(flags);
6226
6279
  const params = ProblemsSchema.parse(args);
@@ -6526,7 +6579,7 @@ var commandRegistry = createCommandRegistry([
6526
6579
  resetApiRequestCountBeforeRun: true,
6527
6580
  telemetry: "result",
6528
6581
  handler: async (context) => {
6529
- const { runInit } = await import("./index-v3j2848b.js");
6582
+ const { runInit } = await import("./index-kx1t9k2w.js");
6530
6583
  if (context.outputMode === "agent-stream") {
6531
6584
  context.output.writeEvent({
6532
6585
  event: "started",
@@ -6769,7 +6822,7 @@ var commandRegistry = createCommandRegistry([
6769
6822
  "moda agent context",
6770
6823
  "moda agent investigate",
6771
6824
  "moda agent next-action",
6772
- "moda agent evidence doctor:harness_report_approved_synced"
6825
+ "moda agent evidence doctor:api_key_resolves"
6773
6826
  ],
6774
6827
  auth: "none",
6775
6828
  outputModes: ["agent", "agent-stream", "json"],
@@ -6908,6 +6961,21 @@ var commandRegistry = createCommandRegistry([
6908
6961
  examples: ["moda problems", "moda problems --days-back=7 --limit=10"],
6909
6962
  ...dataApiDefaults
6910
6963
  }),
6964
+ legacyCommand({
6965
+ name: "problem-impact",
6966
+ description: "Live problem/use-case impact, full analysis denominators, daily recurrence, and paginated evidence",
6967
+ examples: [
6968
+ "moda problem-impact --mode=catalog",
6969
+ "moda problem-impact --node-id=node_0 --days-back=all",
6970
+ "moda problem-impact --problem-id=<uuid> --mode=metrics",
6971
+ "moda problem-impact --node-id=node_0 --problem-id=<uuid> --mode=evidence",
6972
+ "moda problem-impact --node-id=node_0 --mode=trend --days-back=30",
6973
+ "moda problem-impact --mode=overview --days-back=7",
6974
+ "moda problem-impact --problem-id=<uuid> --mode=affected-use-cases",
6975
+ `moda problem-impact --problem-id=<uuid> --mode=trace-use-cases --conversation-ids='["trace_id"]'`
6976
+ ],
6977
+ ...dataApiDefaults
6978
+ }),
6911
6979
  legacyCommand({
6912
6980
  name: "problem",
6913
6981
  description: "Open one Problem: dossier, or --evidence/--reports/--traces/--feedback pages (--conversations is a legacy alias of --traces)",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildSourceSnapshot
3
- } from "./cli-ts6nhn51.js";
3
+ } from "./cli-76tsfpta.js";
4
4
  import"./cli-59yacef3.js";
5
5
 
6
6
  // src/harness-github-actions.ts
@@ -5,11 +5,12 @@ import {
5
5
  detectAgents,
6
6
  initPrompts,
7
7
  installRules,
8
+ packageAssetPath,
8
9
  readSkillsManifest,
9
10
  runPromptSync,
10
11
  runSkillSync,
11
12
  upsertSkillManifestRecord
12
- } from "./cli-c1vhkg8n.js";
13
+ } from "./cli-aeggte1q.js";
13
14
  import {
14
15
  codingAgentDisplayName,
15
16
  describeCodingAgentEvent,
@@ -27,7 +28,7 @@ import {
27
28
  runHarnessCommand,
28
29
  startRemoteAnalyze,
29
30
  stripAnsi
30
- } from "./cli-ts6nhn51.js";
31
+ } from "./cli-76tsfpta.js";
31
32
  import {
32
33
  selectTenantAndCreateKey
33
34
  } from "./cli-pq6rte0w.js";
@@ -57,9 +58,7 @@ import { dirname as dirname2, join as join3 } from "node:path";
57
58
  import { createHash } from "node:crypto";
58
59
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
59
60
  import { dirname, join, relative } from "node:path";
60
- import { fileURLToPath } from "node:url";
61
- var PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
62
- var BUNDLED_SKILLS_DIR = join(PACKAGE_ROOT, "skills", "integration");
61
+ var BUNDLED_SKILLS_DIR = packageAssetPath("skills", "integration");
63
62
  function bundledSkillsDir() {
64
63
  return process.env.MODA_BUNDLED_SKILLS_DIR || BUNDLED_SKILLS_DIR;
65
64
  }
@@ -617,7 +616,7 @@ async function verifyExactTrace(opts) {
617
616
  const parsed = readAuditCounts(response);
618
617
  if (parsed) {
619
618
  counts = parsed;
620
- if (counts.total_spans >= 1 && counts.llm_spans >= 1) {
619
+ if (counts.total_spans >= 1 && counts.llm_spans >= 1 && counts.orphan_count === 0 && !hasUnexpectedAuditDuplicates(response) && completeAI7Projection(response)) {
621
620
  return buildEvidence(true);
622
621
  }
623
622
  }
@@ -671,6 +670,62 @@ function isRecord2(value) {
671
670
  function isNonNegativeNumber(value) {
672
671
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
673
672
  }
673
+ function isAI7Audit(value) {
674
+ return isRecord2(value) && Array.isArray(value.spans) && value.spans.some((span) => isRecord2(span) && isRecord2(span.attributes) && ["cloudflare-think", "vercel-ai7"].includes(String(span.attributes["moda.integration.name"])));
675
+ }
676
+ function hasUnexpectedAuditDuplicates(value) {
677
+ if (!isRecord2(value) || !isRecord2(value.summary))
678
+ return true;
679
+ if (value.summary.duplicate_count === 0 || value.summary.duplicate_count === undefined)
680
+ return false;
681
+ if (!isAI7Audit(value) || !Array.isArray(value.duplicates) || !Array.isArray(value.spans) || value.duplicates.length !== value.summary.duplicate_count)
682
+ return true;
683
+ const spans = value.spans.filter(isRecord2);
684
+ return value.duplicates.some((group) => {
685
+ if (!isRecord2(group) || group.type !== "duplicate_operation" || !Array.isArray(group.span_ids) || group.span_ids.length < 2 || new Set(group.span_ids).size !== group.span_ids.length)
686
+ return true;
687
+ const calls = [];
688
+ for (const id of group.span_ids) {
689
+ const matches = spans.filter((span2) => span2.span_id === id);
690
+ if (matches.length !== 1)
691
+ return true;
692
+ const span = matches[0];
693
+ if (span.type !== "tool" || !isRecord2(span.tool) || typeof span.tool.call_id !== "string" || !span.tool.call_id)
694
+ return true;
695
+ if (!isRecord2(span.attributes) || !["cloudflare-think", "vercel-ai7"].includes(String(span.attributes["moda.integration.name"])))
696
+ return true;
697
+ calls.push(span.tool.call_id);
698
+ }
699
+ return new Set(calls).size !== calls.length;
700
+ });
701
+ }
702
+ function completeAI7Projection(value) {
703
+ if (!isAI7Audit(value))
704
+ return true;
705
+ if (!isRecord2(value) || !Array.isArray(value.spans) || value.parse_errors !== 0 || !isRecord2(value.summary) || value.summary.truncated !== false)
706
+ return false;
707
+ const spans = value.spans.filter(isRecord2);
708
+ for (const span of spans.filter((span2) => span2.type === "llm")) {
709
+ if (isRecord2(span.attributes) && span.attributes["gen_ai.system_instructions"]) {
710
+ if (!Array.isArray(span.prompt) || !span.prompt.some((message) => isRecord2(message) && message.role === "system"))
711
+ return false;
712
+ }
713
+ }
714
+ const transcript = value.conversationContext;
715
+ if (!isRecord2(transcript) || !isRecord2(transcript.context) || !Array.isArray(transcript.context.messages))
716
+ return false;
717
+ if (transcript.total_messages !== transcript.context.messages.length)
718
+ return false;
719
+ const expected = spans.filter((span) => span.type === "tool").map((span) => isRecord2(span.tool) ? span.tool.call_id : undefined);
720
+ const actual = [];
721
+ for (const message of transcript.context.messages) {
722
+ if (!isRecord2(message) || !Array.isArray(message.tool_calls))
723
+ return false;
724
+ for (const tool of message.tool_calls)
725
+ actual.push(isRecord2(tool) ? tool.id : undefined);
726
+ }
727
+ return expected.every((id) => typeof id === "string") && actual.every((id) => typeof id === "string") && new Set(actual).size === actual.length && expected.length === actual.length && expected.every((id) => actual.includes(id));
728
+ }
674
729
  async function fetchAuditWithApiKey(id, apiKey, baseUrl) {
675
730
  const url = `${baseUrl}/api/v1/data/audit/${encodeURIComponent(id)}?kind=conversation`;
676
731
  const response = await fetch(url, {
@@ -682,7 +737,17 @@ async function fetchAuditWithApiKey(id, apiKey, baseUrl) {
682
737
  if (!response.ok) {
683
738
  throw new Error(`Audit fetch failed (${response.status}): ${body.slice(0, 200)}`);
684
739
  }
685
- return JSON.parse(body);
740
+ const audit = JSON.parse(body);
741
+ if (isAI7Audit(audit) && isRecord2(audit)) {
742
+ const contextResponse = await fetch(`${baseUrl}/api/v1/data/conversations/${encodeURIComponent(id)}/context?window=20`, {
743
+ headers: { "x-api-key": apiKey },
744
+ signal: AbortSignal.timeout(1e4)
745
+ });
746
+ if (!contextResponse.ok)
747
+ throw new Error(`AI7 conversation verification failed (${contextResponse.status})`);
748
+ return { ...audit, conversationContext: await contextResponse.json() };
749
+ }
750
+ return audit;
686
751
  }
687
752
 
688
753
  // src/init/sdk-integration.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.30.2",
3
+ "version": "1.31.3",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,20 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-09-03T05:01:51.741Z",
4
- "cli_version": "1.30.2",
3
+ "bundled_at": "2026-09-15T22:29:26.871Z",
4
+ "cli_version": "1.31.3",
5
5
  "skills": [
6
+ {
7
+ "id": "integration-cloudflare-think",
8
+ "version": "1.0.0",
9
+ "summary": "Instrument Think on Workers with AI SDK 7, prompt versions, and acknowledged JSON export.",
10
+ "targets": {
11
+ "language": "node",
12
+ "frameworks": [
13
+ "cloudflare-think"
14
+ ],
15
+ "providers": []
16
+ }
17
+ },
6
18
  {
7
19
  "id": "integration-node-anthropic",
8
20
  "version": "1.0.0",
@@ -51,7 +63,7 @@
51
63
  },
52
64
  {
53
65
  "id": "integration-node-vercel-ai-sdk",
54
- "version": "1.0.0",
66
+ "version": "1.0.2",
55
67
  "summary": "Instrument Vercel AI SDK calls (generateText, streamText, generateObject) with Moda telemetry in a Node.js or TypeScript app.",
56
68
  "targets": {
57
69
  "language": "node",
@@ -0,0 +1,75 @@
1
+ # Think with AI SDK 7
2
+
3
+ Merge this hook into the existing Think class, preserving its model, tools,
4
+ routes and hook overrides. Requires SDK 1.16.0's `moda-ai/think` entry point.
5
+
6
+ ```typescript
7
+ import { Think, type TurnContext } from '@cloudflare/think';
8
+ import { createThinkTelemetry } from 'moda-ai/think';
9
+
10
+ interface Env { MODA_API_KEY: string; MODA_INGEST_URL?: string; AI: Ai }
11
+
12
+ export class TaskAgent extends Think<Env> {
13
+ getModel() { return '@cf/moonshotai/kimi-k2.7-code' as const; }
14
+
15
+ async beforeTurn(_context: TurnContext) {
16
+ const moda = createThinkTelemetry({
17
+ apiKey: this.env.MODA_API_KEY,
18
+ endpoint: this.env.MODA_INGEST_URL,
19
+ recordInputs: true,
20
+ recordOutputs: true,
21
+ onExportError: error => console.error('Moda export failed', error.message),
22
+ });
23
+ return { telemetry: moda.telemetry };
24
+ }
25
+ }
26
+ ```
27
+
28
+ The helper delegates tracing, export and flushing to the shared AI7 integration;
29
+ non-Think apps use `createAI7Telemetry` from `moda-ai/ai7`. Think's adapter includes
30
+ only its session and turn identity keys from runtime context.
31
+ The helper uses Think's runtime conversation ID by default. To attribute
32
+ managed prompts, import the synced `.moda/prompts.lock.json` at build time
33
+ and add `prompt: lock.prompts['your.prompt.key']` to the options. Keep the
34
+ app's own prompt rendering. The helper flushes on completion/abort and
35
+ retains failed batches for an explicit retry while the object is alive.
36
+
37
+ ## Smoke through an existing callable
38
+
39
+ For the upstream `think-submissions` app, the public API is `submitTask` and
40
+ `inspectTask`. Adapt these names to the application's existing callables.
41
+ Use a temporary script, with the CLI's verification ID as its argument:
42
+
43
+ ```javascript
44
+ import { AgentClient } from 'agents/client';
45
+
46
+ const id = process.argv[2];
47
+ if (!id) throw new Error('Pass the CLI verification ID');
48
+ const client = new AgentClient({
49
+ host: '127.0.0.1:4190', protocol: 'ws', agent: 'TaskAgent', name: id,
50
+ });
51
+ try {
52
+ const task = await client.call('submitTask', ['Reply with OK.', crypto.randomUUID()]);
53
+ let completed = false;
54
+ for (let attempt = 0; attempt < 60; attempt++) {
55
+ const status = await client.call('inspectTask', [task.submissionId]);
56
+ if (status.status === 'completed') { completed = true; break; }
57
+ if (['error', 'aborted', 'skipped'].includes(status.status)) {
58
+ throw new Error(`Submission failed: ${status.status}`);
59
+ }
60
+ await new Promise(resolve => setTimeout(resolve, 2000));
61
+ }
62
+ if (!completed) throw new Error('Submission timed out');
63
+ } finally {
64
+ client.close();
65
+ }
66
+ ```
67
+
68
+ This example has one conversation per named Agent: set the telemetry option
69
+ `conversationId: this.name` so the smoke's Agent name becomes the verification
70
+ ID. Keep multi-session apps' own session identity instead. Never hard-code the
71
+ verification ID in the application's source. Confirm delivery with the CLI's
72
+ trace check after the submission completes.
73
+ Use a new submission idempotency key on each smoke attempt, while keeping the
74
+ same verification conversation ID. Reusing a completed submission's key only
75
+ returns its old acknowledgment and does not exercise repaired instrumentation.
@@ -0,0 +1,123 @@
1
+ ---
2
+ id: integration-cloudflare-think
3
+ name: "Integrate Moda with Cloudflare Think"
4
+ version: 1.0.0
5
+ category: integration
6
+ targets:
7
+ language: node
8
+ frameworks:
9
+ - cloudflare-think
10
+ providers: []
11
+ sdk_package: moda-ai
12
+ summary: "Instrument Think on Workers with AI SDK 7, prompt versions, and acknowledged JSON export."
13
+ ---
14
+
15
+ # Integrate Moda with Cloudflare Think
16
+
17
+ ## Safety rules (apply to every step)
18
+
19
+ - Only touch the repository you were launched in.
20
+ - Never write the `MODA_API_KEY` value (or any secret) into source files, and
21
+ never create or edit `.env` files. The key belongs in the runtime
22
+ environment or a secret manager.
23
+ - Make the smallest correct change. Match the existing code style and imports.
24
+ - Do not commit unless the user asks.
25
+ - If a step is ambiguous in this repository, stop and report rather than
26
+ guessing.
27
+
28
+ ## 1. Find the real Think entry point
29
+
30
+ Find classes extending `Think` from `@cloudflare/think`, their `beforeTurn`
31
+ hooks, session identity, and submission/chat route. Read installed versions.
32
+ This recipe supports Think 0.18, AI SDK 7 and `@ai-sdk/otel` 1.x. For a
33
+ different major, report the compatibility gap instead of upgrading the app.
34
+
35
+ ## 2. Install the Workers integration
36
+
37
+ With the repository's package manager, install `moda-ai` with the
38
+ `moda-ai/think` export (1.16.0 or later) and `@ai-sdk/otel` 1.x. Confirm the
39
+ export exists before editing call sites. If it is not published yet, report
40
+ the release prerequisite. Retain Workers' `nodejs_compat` configuration.
41
+
42
+ The dedicated entry point owns a per-turn tracer and fetch-based JSON
43
+ exporter. Instrument the loop once: reconcile existing instrumentation or
44
+ native export to Moda before adding a second telemetry path.
45
+
46
+ ## 3. Wire the existing hook
47
+
48
+ Follow [EXAMPLE.md](EXAMPLE.md). Create telemetry per invocation and merge
49
+ it with existing hook return values. Preserve unrelated integrations.
50
+ Supply `env.MODA_API_KEY` from runtime secrets.
51
+ Pass `env.MODA_INGEST_URL` as `endpoint` when an ingest override is provided
52
+ (for example, an isolated test deployment). Make both values available in
53
+ the Worker runtime; shell variables alone are not automatically Worker bindings.
54
+ Use the app's session ID or
55
+ let the helper use Think's runtime conversation ID; agent name is suitable
56
+ only for an agent with one conversation.
57
+
58
+ Message and tool payload capture is opt-in via `recordInputs` and
59
+ `recordOutputs`. The helper flushes on completion/abort. Keep its `flush()`
60
+ available for explicit delivery verification and report `onExportError`.
61
+ Retries preserve span IDs. Pending exports live in memory, so abrupt
62
+ runtime termination does not have durable archival guarantees.
63
+
64
+ ## 4. Ingest reusable prompts when requested
65
+
66
+ Find the source prompt (`getSystemPrompt` or context configuration). Inline
67
+ TypeScript is not discovered by `moda prompts`. Extract the reusable
68
+ definition into a supported `.prompt.json` file and have the original hook
69
+ read it. Run `moda prompts init` then `moda prompts sync`; require the
70
+ expected definition to be present. Import `.moda/prompts.lock.json` at
71
+ build time and pass its `{ key, promptId, versionId }` entry as `prompt`.
72
+ Re-sync changed definitions before building.
73
+
74
+ Use the CLI's actual JSON fields, for example:
75
+
76
+ ```json
77
+ {
78
+ "key": "task-agent.system",
79
+ "name": "Task agent system prompt",
80
+ "systemPrompt": "The exact existing getSystemPrompt text goes here."
81
+ }
82
+ ```
83
+
84
+ Existing JSON definitions with a plain-text `template` are also accepted;
85
+ the CLI maps that text to registered content without rewriting the file.
86
+ Keep the application's existing prompt field and have `getSystemPrompt` read
87
+ that same definition so runtime and registry cannot drift.
88
+ Check the sync payload includes the actual text; a lock entry or
89
+ version ID alone does not prove that a nonempty definition was registered.
90
+
91
+ Keep memory and variable substitutions separate from reusable template
92
+ versions. Think adds instructions/tools: verify actual system instructions
93
+ on each model span as well as the registry definition.
94
+ Reference: https://docs.moda.dev/prompt-management/attribution
95
+
96
+ ## 5. Validate through the original application route
97
+
98
+ Build and typecheck. Exercise a fresh session through the original route:
99
+ an exact response, a real tool invocation, and a second turn. Wait for
100
+ export acknowledgment. Use `moda audit` on that exact conversation to check
101
+ expected model/tool counts, system/user inputs, output, prompt version,
102
+ and zero duplicates/orphans. Check conversation records too: step spans
103
+ must not become tools, real call IDs must occur once, and successful tools
104
+ must not report failures. Re-running onboarding must reuse the integration.
105
+
106
+ For callable submission methods, use the installed `AgentClient` from
107
+ `agents/client` (the same protocol as `useAgent().call()`), rather than
108
+ reimplementing its wire protocol. A plain HTTP GET to a WebSocket route may
109
+ return 404 even when the WebSocket works. A submission acknowledgment only
110
+ means queued: poll the app's inspection method until completion before
111
+ checking telemetry.
112
+
113
+ The CLI supplies an exact verification conversation ID. In a single-conversation
114
+ per-Agent app such as `think-submissions`, `conversationId: this.name` lets a
115
+ fresh named Agent use that ID. For apps with multiple sessions per Agent, use
116
+ their actual session ID instead. Never hard-code the verification ID in source.
117
+
118
+ Report missing evidence explicitly; connectivity alone is not a complete
119
+ integration verdict. Finish with:
120
+
121
+ ```bash
122
+ moda doctor --online --json
123
+ ```
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: integration-node-vercel-ai-sdk
3
3
  name: "Integrate Moda with the Vercel AI SDK (Node.js / TypeScript)"
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  category: integration
6
6
  targets:
7
7
  language: node
@@ -37,6 +37,11 @@ prefer those; they match the installed SDK version).
37
37
  Find the real LLM call sites: search for `generateText`, `streamText`,
38
38
  `generateObject`, or `streamObject` imported from the `ai` package.
39
39
 
40
+ Check the installed `ai` major version before selecting an API. For
41
+ `@cloudflare/think` with AI SDK 7, use `integration-cloudflare-think`.
42
+ For other AI SDK 7 apps, follow `references/ai7.md` instead of Steps 2 onward
43
+ and the legacy `EXAMPLE.md`. The remaining steps here target AI SDK through v6.
44
+
40
45
  - If you find none, stop and report: this skill only applies to apps that
41
46
  call the Vercel AI SDK directly.
42
47
  - Instrument the Vercel AI SDK layer only. Do NOT also instrument raw
@@ -140,9 +145,8 @@ identifier exists anywhere, stop and report; do not invent one.
140
145
 
141
146
  ## Step 6: Attribute managed prompts (only if the repo uses them)
142
147
 
143
- Only if this repository has a `.moda/prompts.yml` manifest: render managed
144
- prompts through the SDK before creating the telemetry config, so traces point
145
- at exact prompt versions. See
148
+ Only if this repository has a `.moda/prompts.yml` manifest: read synced prompt
149
+ IDs from the lockfile and attach them explicitly to telemetry metadata. See
146
150
  [references/prompt-attribution.md](references/prompt-attribution.md).
147
151
  If there is no prompt manifest, skip this step.
148
152
 
@@ -0,0 +1,52 @@
1
+ # AI SDK 7: shared integration
2
+
3
+ Apply the parent skill's safety rules. This recipe supports direct
4
+ `generateText` and `streamText` calls on AI SDK 7, in Node or Workers.
5
+ Think apps use the `integration-cloudflare-think` entry because their framework
6
+ supplies session/turn identity.
7
+
8
+ 1. Confirm the installed `ai` major is 7. Install `moda-ai` >=1.16.0 and
9
+ `@ai-sdk/otel` 1.x using the repository's package manager. Verify
10
+ `moda-ai/ai7` exports `createAI7Telemetry`. If unavailable, stop and report
11
+ that the SDK release is required; do not substitute the older tracer helper.
12
+ 2. Keep the app's model, tools, routes, prompts and streaming behavior. Create
13
+ the helper using the provisioned key from the runtime environment. Set
14
+ `conversationId` to the existing app session ID; without it each AI7 call is
15
+ a separate conversation. Set `userId` when the app has a user identifier.
16
+ 3. Set the existing call's `telemetry` to the returned configuration. Preserve
17
+ existing integrations when merging: retain their order and append Moda's
18
+ integration array. Avoid also tracing the underlying provider client.
19
+ 4. Prompt/tool payload capture defaults off. Enable `recordInputs` and
20
+ `recordOutputs` when prompt/content ingestion is required and authorized.
21
+ After source prompt extraction and `moda prompts sync`, pass the matching
22
+ lockfile `{key, promptId, versionId}` as `prompt`; keep the app's rendering.
23
+ 5. Await `flush()` after a completed generation (or a fully consumed stream)
24
+ for onboarding verification. Supply `onExportError` for runtime visibility;
25
+ AI7 may swallow hook exceptions. In Workers retain `nodejs_compat` and use
26
+ the runtime's request/stream lifecycle to finish asynchronous work.
27
+
28
+ For same-zone Worker-to-Worker routing on `workers.dev`, configure a service
29
+ binding and pass `fetch: (url, init) => env.MODA_INGEST.fetch(new Request(url, init))`.
30
+ Other deployments use the default native fetch. This changes transport only;
31
+ the SDK still serializes, authenticates, retries and verifies the export response.
32
+
33
+ ```typescript
34
+ import { createAI7Telemetry } from 'moda-ai/ai7';
35
+
36
+ const moda = createAI7Telemetry({
37
+ apiKey: process.env.MODA_API_KEY!, // Workers: use the request's env binding.
38
+ conversationId: sessionId,
39
+ recordInputs: true,
40
+ recordOutputs: true,
41
+ onExportError: error => console.error('Moda export failed', error.message),
42
+ });
43
+ const result = await generateText({ ...existingOptions, telemetry: moda.telemetry });
44
+ await moda.flush();
45
+ ```
46
+
47
+ Validate using a fresh verification conversation ID and the provisioned key.
48
+ Require complete spans, normalized system prompts when captured, actual tool
49
+ calls/results without duplicate summary records, and a complete conversation
50
+ window. The CLI verifier checks this for `vercel-ai7` and `cloudflare-think`.
51
+ An HTTP 200 alone is not acceptance. Retries retain span IDs, but the shared
52
+ export buffer is in memory and cannot guarantee delivery across isolate eviction.
@@ -3,29 +3,31 @@
3
3
  Only relevant when the repository manages prompts with Moda
4
4
  (`.moda/prompts.yml` exists and prompts are synced with `moda prompts sync`).
5
5
 
6
- Render the managed prompt through the SDK, then create the telemetry config.
7
- `Moda.getVercelAITelemetry()` includes the rendered prompt's metadata
8
- (`moda.prompt_key`, `moda.prompt_id`, `moda.prompt_version`,
9
- `moda.prompt_version_id`) in `experimental_telemetry.metadata`, so production
10
- traces point at the exact prompt version:
6
+ Run `moda prompts sync`, read `.moda/prompts.lock.json`, and pass its registry
7
+ IDs explicitly in telemetry metadata. Keep the application's own rendering.
8
+ This example uses the tracer-based AI SDK API through v6. Think with AI SDK
9
+ 7 uses `moda-ai/think` and `telemetry.integrations`; see its catalog entry.
11
10
 
12
11
  ```typescript
13
12
  import { Moda } from 'moda-ai';
14
13
  import { generateText } from 'ai';
15
14
  import { openai } from '@ai-sdk/openai';
15
+ import prompts from './.moda/prompts.lock.json';
16
16
 
17
- const rendered = Moda.prompt('support.triage').render({
18
- ticket: { text: userMessage },
19
- });
17
+ const version = prompts.prompts['support.triage'];
20
18
 
21
19
  const result = await generateText({
22
20
  model: openai('gpt-4o'),
23
- messages: rendered.messages,
24
- experimental_telemetry: Moda.getVercelAITelemetry(),
21
+ prompt: 'Triage this support request.',
22
+ experimental_telemetry: Moda.getVercelAITelemetry({ metadata: {
23
+ 'moda.prompt_key': version.key,
24
+ 'moda.prompt_id': version.promptId,
25
+ 'moda.prompt_version_id': version.versionId,
26
+ }}),
25
27
  });
26
28
  ```
27
29
 
28
- Order matters: render the prompt before calling
29
- `Moda.getVercelAITelemetry()` so prompt metadata is attached.
30
+ Re-sync a changed definition before building. Different variable values can
31
+ share a template version; full model inputs are recorded separately.
30
32
 
31
33
  Full reference: https://docs.moda.dev/ingestion/vercel-ai-sdk