@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 +5 -0
- package/dist/{cli-c1vhkg8n.js → cli-j3rr0nh7.js} +57 -38
- package/dist/{cli-ts6nhn51.js → cli-p3271yp2.js} +19 -6
- package/dist/cli.js +71 -3
- package/dist/{harness-github-actions-ze17s10y.js → harness-github-actions-kbw4z7q4.js} +1 -1
- package/dist/{index-v3j2848b.js → index-4enh6fq6.js} +72 -7
- package/package.json +1 -1
- package/skills/integration/index.json +15 -3
- package/skills/integration/integration-cloudflare-think/EXAMPLE.md +75 -0
- package/skills/integration/integration-cloudflare-think/SKILL.md +123 -0
- package/skills/integration/integration-node-vercel-ai-sdk/SKILL.md +8 -4
- package/skills/integration/integration-node-vercel-ai-sdk/references/ai7.md +52 -0
- package/skills/integration/integration-node-vercel-ai-sdk/references/prompt-attribution.md +14 -12
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-
|
|
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
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
991
|
-
const filePath =
|
|
992
|
-
const relativePath =
|
|
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
|
|
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(
|
|
1110
|
+
if (existsSync4(join4(cwd, ".claude")) || isOnPath("claude")) {
|
|
1092
1111
|
agents.push("Claude Code");
|
|
1093
1112
|
}
|
|
1094
|
-
if (existsSync4(
|
|
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
|
|
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
|
|
1124
|
-
var SKILLS_MANIFEST_PATH =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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()
|
|
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-
|
|
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
|
|
4241
|
-
if (
|
|
4242
|
-
|
|
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,
|
|
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-
|
|
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-
|
|
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-
|
|
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)",
|
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "moda.skill_index.v1",
|
|
3
|
-
"bundled_at": "2026-09-
|
|
4
|
-
"cli_version": "1.
|
|
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.
|
|
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.
|
|
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:
|
|
144
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`moda
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|