@moda-ai/cli 1.30.2 → 1.31.1

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
@@ -126,6 +126,11 @@ Prompt management is code-first. Keep prompts in `prompts/**/*.prompt.md` or
126
126
  `prompts/**/*.prompt.json`, sync them with the CLI, and render them through the
127
127
  SDK so LLM spans include prompt metadata.
128
128
 
129
+ JSON definitions can provide `content`, `systemPrompt`, or `messages`. Existing
130
+ files with a nonempty text `template` also work: the CLI uses it as content when
131
+ none of those fields supplies usable text. Sync preserves placeholders and
132
+ leaves the source file unchanged; empty definitions still produce an error.
133
+
129
134
  ```bash
130
135
  moda prompts init
131
136
  moda prompts status
@@ -11,7 +11,7 @@ import {
11
11
  renderStatusHuman,
12
12
  scanHarness,
13
13
  validateHarnessReport
14
- } from "./cli-ts6nhn51.js";
14
+ } from "./cli-p3271yp2.js";
15
15
  import {
16
16
  isAuthSessionValid,
17
17
  loadAuthSession
@@ -772,7 +772,19 @@ function parsePromptFile(absPath) {
772
772
  if (absPath.endsWith(".json")) {
773
773
  const parsed = JSON.parse(raw);
774
774
  const key2 = parsed.key || keyFromPath(sourcePath);
775
- const content = parsed.content || JSON.stringify(parsed.messages ?? [], null, 2);
775
+ const hasContent = typeof parsed.content === "string" && parsed.content.trim().length > 0;
776
+ const hasSystemPrompt = typeof parsed.systemPrompt === "string" && parsed.systemPrompt.trim().length > 0;
777
+ const hasTemplate = typeof parsed.template === "string" && parsed.template.trim().length > 0;
778
+ const hasMessages = Array.isArray(parsed.messages) && parsed.messages.some((message) => {
779
+ if (!message || typeof message !== "object" || !("role" in message) || !["system", "developer", "user", "assistant", "tool"].includes(String(message.role)) || !("content" in message))
780
+ return false;
781
+ const content2 = message.content;
782
+ return typeof content2 === "string" ? content2.trim().length > 0 : Array.isArray(content2) && content2.some((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
783
+ });
784
+ if (!hasContent && !hasSystemPrompt && !hasMessages && !hasTemplate) {
785
+ throw new Error(`${sourcePath}: prompt JSON requires non-empty content, systemPrompt, messages, or a text template.`);
786
+ }
787
+ const content = hasContent ? parsed.content : hasSystemPrompt ? parsed.systemPrompt : hasMessages ? JSON.stringify(parsed.messages, null, 2) : parsed.template;
776
788
  return normalizePrompt({ ...parsed, key: key2, name: parsed.name || key2, sourcePath, content });
777
789
  }
778
790
  const { frontmatter, body } = parseFrontmatter(raw);
@@ -931,11 +943,18 @@ function printJson(value) {
931
943
 
932
944
  // src/init/rules.ts
933
945
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
946
+ import { join as join3 } from "node:path";
947
+ import { confirm } from "@inquirer/prompts";
948
+
949
+ // src/package-assets.ts
934
950
  import { dirname as dirname2, join as join2 } from "node:path";
935
951
  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");
952
+ function packageAssetPath(...parts) {
953
+ return join2(dirname2(fileURLToPath(import.meta.url)), "..", ...parts);
954
+ }
955
+
956
+ // src/init/rules.ts
957
+ var SKILLS_SOURCE_DIR = packageAssetPath("skills");
939
958
  var SKILLS = [
940
959
  {
941
960
  id: "moda-cli",
@@ -946,7 +965,7 @@ var ADAPTERS = {
946
965
  "Claude Code": {
947
966
  name: "Claude Code",
948
967
  targetFor: (skillId) => ({
949
- dir: join2(".claude", "skills", skillId),
968
+ dir: join3(".claude", "skills", skillId),
950
969
  filename: "SKILL.md"
951
970
  }),
952
971
  transform: (content) => content
@@ -954,7 +973,7 @@ var ADAPTERS = {
954
973
  Cursor: {
955
974
  name: "Cursor",
956
975
  targetFor: (skillId) => ({
957
- dir: join2(".cursor", "rules"),
976
+ dir: join3(".cursor", "rules"),
958
977
  filename: `${skillId}.mdc`
959
978
  }),
960
979
  transform: claudeSkillToCursorRule
@@ -979,7 +998,7 @@ function claudeSkillToCursorRule(skillContent) {
979
998
  ${body.replace(/^\s+/, "")}`;
980
999
  }
981
1000
  function loadSkillSource(skillId) {
982
- const path = join2(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
1001
+ const path = join3(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
983
1002
  if (!existsSync3(path)) {
984
1003
  throw new Error(`Skill source missing: ${path}`);
985
1004
  }
@@ -987,9 +1006,9 @@ function loadSkillSource(skillId) {
987
1006
  }
988
1007
  function installSkillForAgent(skill, adapter, content) {
989
1008
  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);
1009
+ const dirPath = join3(process.cwd(), dir);
1010
+ const filePath = join3(dirPath, filename);
1011
+ const relativePath = join3(dir, filename);
993
1012
  try {
994
1013
  mkdirSync2(dirPath, { recursive: true });
995
1014
  if (existsSync3(filePath)) {
@@ -1075,7 +1094,7 @@ async function installRules(detectedAgents, options = {}) {
1075
1094
 
1076
1095
  // src/init/mcp.ts
1077
1096
  import { existsSync as existsSync4 } from "node:fs";
1078
- import { join as join3 } from "node:path";
1097
+ import { join as join4 } from "node:path";
1079
1098
  import { execSync } from "node:child_process";
1080
1099
  function isOnPath(cmd) {
1081
1100
  try {
@@ -1088,10 +1107,10 @@ function isOnPath(cmd) {
1088
1107
  function detectAgents() {
1089
1108
  const cwd = process.cwd();
1090
1109
  const agents = [];
1091
- if (existsSync4(join3(cwd, ".claude")) || isOnPath("claude")) {
1110
+ if (existsSync4(join4(cwd, ".claude")) || isOnPath("claude")) {
1092
1111
  agents.push("Claude Code");
1093
1112
  }
1094
- if (existsSync4(join3(cwd, ".cursor"))) {
1113
+ if (existsSync4(join4(cwd, ".cursor"))) {
1095
1114
  agents.push("Cursor");
1096
1115
  }
1097
1116
  return agents;
@@ -1108,7 +1127,7 @@ import {
1108
1127
  readFileSync as readFileSync5,
1109
1128
  writeFileSync as writeFileSync6
1110
1129
  } from "node:fs";
1111
- import { basename, dirname as dirname5, join as join6, relative as relative3 } from "node:path";
1130
+ import { basename, dirname as dirname5, join as join7, relative as relative3 } from "node:path";
1112
1131
 
1113
1132
  // src/skills-manifest.ts
1114
1133
  import {
@@ -1120,12 +1139,12 @@ import {
1120
1139
  unlinkSync,
1121
1140
  writeFileSync as writeFileSync4
1122
1141
  } 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");
1142
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join5, normalize, relative as relative2, resolve as resolve4 } from "node:path";
1143
+ var SKILLS_MANIFEST_PATH = join5(".moda", "skills.yml");
1125
1144
  var SKILL_SOURCE_HARNESS = "moda";
1126
1145
  var SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
1127
1146
  function readSkillsManifest(cwd) {
1128
- const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
1147
+ const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
1129
1148
  if (!existsSync5(manifestPath))
1130
1149
  return null;
1131
1150
  let text = "";
@@ -1156,7 +1175,7 @@ function manifestHeaderFrom(manifest) {
1156
1175
  };
1157
1176
  }
1158
1177
  function writeSkillsManifest(cwd, header, records) {
1159
- const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
1178
+ const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
1160
1179
  const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
1161
1180
  const lines = [
1162
1181
  "version: 1",
@@ -1211,7 +1230,7 @@ function pruneManagedSkillPaths(cwd, managedPaths, log) {
1211
1230
  function pruneEmptyClaudeSkillDir(cwd, relativePath) {
1212
1231
  if (!relativePath.startsWith(".claude/skills/") || !relativePath.endsWith("/SKILL.md"))
1213
1232
  return;
1214
- const dir = dirname3(join4(cwd, relativePath));
1233
+ const dir = dirname3(join5(cwd, relativePath));
1215
1234
  try {
1216
1235
  if (existsSync5(dir) && readdirSync2(dir).length === 0) {
1217
1236
  rmdirSync(dir);
@@ -1331,7 +1350,7 @@ function yamlQuote(value) {
1331
1350
 
1332
1351
  // src/agents-md.ts
1333
1352
  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";
1353
+ import { dirname as dirname4, join as join6 } from "node:path";
1335
1354
  var AGENTS_MD_FILENAME = "AGENTS.md";
1336
1355
  var FIXES_BLOCK_BEGIN = "<!-- moda:fixes:begin -->";
1337
1356
  var FIXES_BLOCK_END = "<!-- moda:fixes:end -->";
@@ -1392,7 +1411,7 @@ function writeFileIfChanged(filePath, content) {
1392
1411
  }
1393
1412
  }
1394
1413
  function upsertFixesAgentsBlock(cwd) {
1395
- const filePath = join5(cwd, AGENTS_MD_FILENAME);
1414
+ const filePath = join6(cwd, AGENTS_MD_FILENAME);
1396
1415
  let existing = null;
1397
1416
  if (existsSync6(filePath)) {
1398
1417
  try {
@@ -1408,7 +1427,7 @@ function upsertFixesAgentsBlock(cwd) {
1408
1427
  // src/skills.ts
1409
1428
  var SKILLS_REQUEST_TIMEOUT_MS = 15000;
1410
1429
  var VALID_STATUSES = new Set(["approved", "proposed", "all"]);
1411
- var SKILL_PROPOSALS_MANIFEST_PATH = join6(".moda", "skill-proposals.yml");
1430
+ var SKILL_PROPOSALS_MANIFEST_PATH = join7(".moda", "skill-proposals.yml");
1412
1431
  async function fetchSkills(status) {
1413
1432
  const apiKey = resolveApiKey();
1414
1433
  if (!apiKey) {
@@ -1578,7 +1597,7 @@ async function pullSkills(options = {}) {
1578
1597
  }
1579
1598
  }
1580
1599
  const agents = detectAgents();
1581
- const writeCursor = agents.includes("Cursor") || existsSync7(join6(cwd, ".cursor"));
1600
+ const writeCursor = agents.includes("Cursor") || existsSync7(join7(cwd, ".cursor"));
1582
1601
  const records = [];
1583
1602
  for (const skill of skills) {
1584
1603
  if (!skill.skill_id || !skill.skill_md) {
@@ -1591,7 +1610,7 @@ async function pullSkills(options = {}) {
1591
1610
  log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
1592
1611
  continue;
1593
1612
  }
1594
- const claudePath = join6(cwd, ".claude", "skills", safeId, "SKILL.md");
1613
+ const claudePath = join7(cwd, ".claude", "skills", safeId, "SKILL.md");
1595
1614
  const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
1596
1615
  const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
1597
1616
  recordOutcome(summary, claudeStatus, log, claudeRelativePath);
@@ -1599,7 +1618,7 @@ async function pullSkills(options = {}) {
1599
1618
  if (claudeStatus !== "failed")
1600
1619
  paths.push(claudeRelativePath);
1601
1620
  if (writeCursor) {
1602
- const cursorPath = join6(cwd, ".cursor", "rules", `${safeId}.mdc`);
1621
+ const cursorPath = join7(cwd, ".cursor", "rules", `${safeId}.mdc`);
1603
1622
  const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
1604
1623
  const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
1605
1624
  recordOutcome(summary, cursorStatus, log, cursorRelativePath);
@@ -1637,7 +1656,7 @@ function recordOutcome(summary, status, log, relativePath) {
1637
1656
  break;
1638
1657
  }
1639
1658
  }
1640
- var SKILLS_DIR = join6(".claude", "skills");
1659
+ var SKILLS_DIR = join7(".claude", "skills");
1641
1660
  var SKILL_FILE = "SKILL.md";
1642
1661
  var MAX_SKILL_WALK_DEPTH = 4;
1643
1662
  async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
@@ -1668,7 +1687,7 @@ async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
1668
1687
  return { synced: response.synced ?? [], dryRun, ...agentsMd ? { agentsMd } : {} };
1669
1688
  }
1670
1689
  function discoverLocalSkills(cwd) {
1671
- const skillsRoot = join6(cwd, SKILLS_DIR);
1690
+ const skillsRoot = join7(cwd, SKILLS_DIR);
1672
1691
  if (!existsSync7(skillsRoot))
1673
1692
  return [];
1674
1693
  const managed = new Set(managedSkillPaths(cwd));
@@ -1717,7 +1736,7 @@ function walkSkillFiles(dir, depth) {
1717
1736
  }
1718
1737
  const files = [];
1719
1738
  for (const entry of entries) {
1720
- const fullPath = join6(dir, entry);
1739
+ const fullPath = join7(dir, entry);
1721
1740
  let stat;
1722
1741
  try {
1723
1742
  stat = lstatSync2(fullPath);
@@ -1791,7 +1810,7 @@ async function applySkillProposal(proposalId, flags, profileOptions = {}, option
1791
1810
  }
1792
1811
  const safeId = safeSkillId(proposal.skillId);
1793
1812
  const relativePath = `.claude/skills/${safeId}/SKILL.md`;
1794
- const writeStatus = writeIfChanged(join6(cwd, relativePath), proposal.skillMd);
1813
+ const writeStatus = writeIfChanged(join7(cwd, relativePath), proposal.skillMd);
1795
1814
  if (writeStatus === "failed") {
1796
1815
  throw new Error(`Failed to write ${relativePath}`);
1797
1816
  }
@@ -1824,7 +1843,7 @@ function safeSkillId(skillId) {
1824
1843
  return safeId;
1825
1844
  }
1826
1845
  function writeSkillProposalManifest(cwd, proposal, relativePath) {
1827
- const manifestPath = join6(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
1846
+ const manifestPath = join7(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
1828
1847
  mkdirSync5(dirname5(manifestPath), { recursive: true });
1829
1848
  const existing = existsSync7(manifestPath) ? readFileSync5(manifestPath, "utf8") : `applied_proposals:
1830
1849
  `;
@@ -2085,7 +2104,7 @@ function sha2562(input) {
2085
2104
  // src/doctor.ts
2086
2105
  import { createHash as createHash3 } from "node:crypto";
2087
2106
  import { existsSync as existsSync8, readFileSync as readFileSync6, statSync } from "node:fs";
2088
- import { join as join7 } from "node:path";
2107
+ import { join as join8 } from "node:path";
2089
2108
  async function runDoctorCommand(context) {
2090
2109
  const report = await buildDoctorReport(context, {
2091
2110
  offline: context.flags.offline === "true"
@@ -2208,7 +2227,7 @@ async function buildDoctorReport(context, options) {
2208
2227
  checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
2209
2228
  checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
2210
2229
  checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
2211
- const manifestPath = join7(context.cwd, ".moda", "harness.json");
2230
+ const manifestPath = join8(context.cwd, ".moda", "harness.json");
2212
2231
  const manifestExists = existsSync8(manifestPath);
2213
2232
  checks.push({
2214
2233
  id: "harness_manifest_exists",
@@ -2498,7 +2517,7 @@ function harnessSyncStateCheck(cwd) {
2498
2517
  };
2499
2518
  }
2500
2519
  function harnessReportStateCheck(cwd) {
2501
- const reportPath = join7(cwd, HARNESS_REPORT_PATH);
2520
+ const reportPath = join8(cwd, HARNESS_REPORT_PATH);
2502
2521
  if (!existsSync8(reportPath)) {
2503
2522
  return {
2504
2523
  id: "harness_report_approved_synced",
@@ -2592,7 +2611,7 @@ function harnessReportStateCheck(cwd) {
2592
2611
  }
2593
2612
  }
2594
2613
  function readHarnessReportApproval(cwd) {
2595
- const approvalPath = join7(cwd, HARNESS_REPORT_APPROVAL_PATH);
2614
+ const approvalPath = join8(cwd, HARNESS_REPORT_APPROVAL_PATH);
2596
2615
  if (!existsSync8(approvalPath))
2597
2616
  return null;
2598
2617
  try {
@@ -2610,7 +2629,7 @@ function readHarnessReportApproval(cwd) {
2610
2629
  }
2611
2630
  }
2612
2631
  function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
2613
- const found = relativePaths.filter((path) => existsSync8(join7(cwd, path)));
2632
+ const found = relativePaths.filter((path) => existsSync8(join8(cwd, path)));
2614
2633
  return {
2615
2634
  id,
2616
2635
  group,
@@ -2626,7 +2645,7 @@ function sdkInitializationCheck(cwd) {
2626
2645
  const candidates = ["src", "app", "agent_harness"];
2627
2646
  let detected = false;
2628
2647
  for (const candidate of candidates) {
2629
- const path = join7(cwd, candidate);
2648
+ const path = join8(cwd, candidate);
2630
2649
  if (!existsSync8(path))
2631
2650
  continue;
2632
2651
  try {
@@ -2950,4 +2969,4 @@ function isRecord(value) {
2950
2969
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2951
2970
  }
2952
2971
 
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 };
2972
+ export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, packageAssetPath, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, upsertSkillManifestRecord, runSkillSync, runSkillsCommand, runDoctorCommand, runStatusCommand, buildDoctorReport };
@@ -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") {
@@ -3546,7 +3546,7 @@ async function runHarnessCommand(context) {
3546
3546
  elapsed_ms: Date.now() - context.startedAt
3547
3547
  });
3548
3548
  if (context.flags["github-actions"] === "true") {
3549
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-ze17s10y.js");
3549
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-kbw4z7q4.js");
3550
3550
  const result = await runGithubActionsAnalyze(rootDir, {
3551
3551
  writeReport: (report2) => {
3552
3552
  const normalized = normalizeHarnessReport(report2);
@@ -4237,9 +4237,22 @@ function repairCitationLines(report, rootDir) {
4237
4237
  const currentLine = typeof citation.line === "number" ? lines[citation.line - 1] : undefined;
4238
4238
  if (currentLine && citationLineMatches(currentLine, excerpt))
4239
4239
  continue;
4240
- const matchedIndex = lines.findIndex((line) => citationLineMatches(line, excerpt));
4241
- if (matchedIndex >= 0)
4242
- citation.line = matchedIndex + 1;
4240
+ const needle = normalizeComparableLine(excerpt);
4241
+ if (!needle)
4242
+ continue;
4243
+ const sourceLines = lines.map((line, index) => ({ text: normalizeComparableLine(line), index })).filter((line) => line.text.length > 0);
4244
+ const source = sourceLines.map((line) => line.text).join(" ");
4245
+ const match = source.indexOf(needle);
4246
+ if (match < 0 || source.indexOf(needle, match + 1) >= 0)
4247
+ continue;
4248
+ let offset = 0;
4249
+ for (const line of sourceLines) {
4250
+ if (match >= offset && match < offset + line.text.length) {
4251
+ citation.line = line.index + 1;
4252
+ break;
4253
+ }
4254
+ offset += line.text.length + 1;
4255
+ }
4243
4256
  }
4244
4257
  }
4245
4258
  function citationLineMatches(line, excerpt) {
@@ -4745,7 +4758,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
4745
4758
  if (options.adapter === "claude") {
4746
4759
  const extraArgs = externalAnalystExtraArgs(options.adapter);
4747
4760
  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 *)`];
4761
+ 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
4762
  return {
4750
4763
  adapter: options.adapter,
4751
4764
  command,
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-j3rr0nh7.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-p3271yp2.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)
@@ -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-4enh6fq6.js");
6530
6583
  if (context.outputMode === "agent-stream") {
6531
6584
  context.output.writeEvent({
6532
6585
  event: "started",
@@ -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-p3271yp2.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-j3rr0nh7.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-p3271yp2.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.1",
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-15T20:51:36.329Z",
4
+ "cli_version": "1.31.1",
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