@moda-ai/cli 1.30.2 → 1.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/{cli-ts6nhn51.js → cli-76tsfpta.js} +20 -8
- package/dist/{cli-c1vhkg8n.js → cli-aeggte1q.js} +56 -180
- package/dist/cli.js +73 -5
- package/dist/{harness-github-actions-ze17s10y.js → harness-github-actions-89sn0hqd.js} +1 -1
- package/dist/{index-v3j2848b.js → index-kx1t9k2w.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
|
@@ -32,6 +32,16 @@ export MODA_API_KEY="moda_sk_..."
|
|
|
32
32
|
|
|
33
33
|
Get your API key from [moda.dev/settings](https://moda.dev/settings).
|
|
34
34
|
|
|
35
|
+
## Diagnostics
|
|
36
|
+
|
|
37
|
+
`moda doctor --online` checks configuration and cloud data flow. Experimental
|
|
38
|
+
harness report citations, approval, and report sync do not affect normal doctor
|
|
39
|
+
or status health; use the explicit harness commands to validate those reports.
|
|
40
|
+
The legacy `status.harnessReport` JSON field is retained as `skipped`. Source
|
|
41
|
+
folder presence is not evidence of SDK initialization, so doctor does not warn
|
|
42
|
+
about instrumentation solely because a `src` or `app` directory exists. Verify
|
|
43
|
+
your integration with a fresh application conversation and `moda audit`.
|
|
44
|
+
|
|
35
45
|
## Usage
|
|
36
46
|
|
|
37
47
|
Find things by meaning, then drill in:
|
|
@@ -126,6 +136,11 @@ Prompt management is code-first. Keep prompts in `prompts/**/*.prompt.md` or
|
|
|
126
136
|
`prompts/**/*.prompt.json`, sync them with the CLI, and render them through the
|
|
127
137
|
SDK so LLM spans include prompt metadata.
|
|
128
138
|
|
|
139
|
+
JSON definitions can provide `content`, `systemPrompt`, or `messages`. Existing
|
|
140
|
+
files with a nonempty text `template` also work: the CLI uses it as content when
|
|
141
|
+
none of those fields supplies usable text. Sync preserves placeholders and
|
|
142
|
+
leaves the source file unchanged; empty definitions still produce an error.
|
|
143
|
+
|
|
129
144
|
```bash
|
|
130
145
|
moda prompts init
|
|
131
146
|
moda prompts status
|
|
@@ -1654,7 +1654,7 @@ function citationExcerptMatches(expected, actualLine) {
|
|
|
1654
1654
|
return expectedNormalized.includes(actualNormalized) || actualNormalized.includes(expectedNormalized);
|
|
1655
1655
|
}
|
|
1656
1656
|
function normalizeComparableText(value) {
|
|
1657
|
-
return value.replace(/\s+/g, " ").trim()
|
|
1657
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1658
1658
|
}
|
|
1659
1659
|
function validateCitation(citation, rootDir, errors, warnings, options) {
|
|
1660
1660
|
if (citation.type === "command_output" || citation.type === "production_evidence") {
|
|
@@ -3040,7 +3040,6 @@ function renderStatusHuman(status, options = {}) {
|
|
|
3040
3040
|
lines.push(` Data flow ${ui.status(status.dataFlow)}`);
|
|
3041
3041
|
lines.push(` Harness ${ui.status(status.harness)}`);
|
|
3042
3042
|
lines.push(` Harness sync ${ui.status(status.harnessSync)}`);
|
|
3043
|
-
lines.push(` Harness report ${ui.status(status.harnessReport)}`);
|
|
3044
3043
|
lines.push(` Shadow Spark ${ui.status(status.shadowSpark)}`);
|
|
3045
3044
|
lines.push(` Prompts ${ui.status(status.prompts)}`);
|
|
3046
3045
|
lines.push(` Skills ${ui.status(status.skills)}`);
|
|
@@ -3546,7 +3545,7 @@ async function runHarnessCommand(context) {
|
|
|
3546
3545
|
elapsed_ms: Date.now() - context.startedAt
|
|
3547
3546
|
});
|
|
3548
3547
|
if (context.flags["github-actions"] === "true") {
|
|
3549
|
-
const { runGithubActionsAnalyze } = await import("./harness-github-actions-
|
|
3548
|
+
const { runGithubActionsAnalyze } = await import("./harness-github-actions-89sn0hqd.js");
|
|
3550
3549
|
const result = await runGithubActionsAnalyze(rootDir, {
|
|
3551
3550
|
writeReport: (report2) => {
|
|
3552
3551
|
const normalized = normalizeHarnessReport(report2);
|
|
@@ -4237,9 +4236,22 @@ function repairCitationLines(report, rootDir) {
|
|
|
4237
4236
|
const currentLine = typeof citation.line === "number" ? lines[citation.line - 1] : undefined;
|
|
4238
4237
|
if (currentLine && citationLineMatches(currentLine, excerpt))
|
|
4239
4238
|
continue;
|
|
4240
|
-
const
|
|
4241
|
-
if (
|
|
4242
|
-
|
|
4239
|
+
const needle = normalizeComparableLine(excerpt);
|
|
4240
|
+
if (!needle)
|
|
4241
|
+
continue;
|
|
4242
|
+
const sourceLines = lines.map((line, index) => ({ text: normalizeComparableLine(line), index })).filter((line) => line.text.length > 0);
|
|
4243
|
+
const source = sourceLines.map((line) => line.text).join(" ");
|
|
4244
|
+
const match = source.indexOf(needle);
|
|
4245
|
+
if (match < 0 || source.indexOf(needle, match + 1) >= 0)
|
|
4246
|
+
continue;
|
|
4247
|
+
let offset = 0;
|
|
4248
|
+
for (const line of sourceLines) {
|
|
4249
|
+
if (match >= offset && match < offset + line.text.length) {
|
|
4250
|
+
citation.line = line.index + 1;
|
|
4251
|
+
break;
|
|
4252
|
+
}
|
|
4253
|
+
offset += line.text.length + 1;
|
|
4254
|
+
}
|
|
4243
4255
|
}
|
|
4244
4256
|
}
|
|
4245
4257
|
function citationLineMatches(line, excerpt) {
|
|
@@ -4745,7 +4757,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
4745
4757
|
if (options.adapter === "claude") {
|
|
4746
4758
|
const extraArgs = externalAnalystExtraArgs(options.adapter);
|
|
4747
4759
|
const model = resolveAnalystModel(options.adapter, options.flags);
|
|
4748
|
-
const permission = mode === "write" ? ["--permission-mode", "acceptEdits", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash"] : ["--allowedTools", `Read,Grep,Glob,
|
|
4760
|
+
const permission = mode === "write" ? ["--permission-mode", "acceptEdits", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash"] : ["--allowedTools", `Read,Grep,Glob,Edit(${HARNESS_REPORT_PATH}),Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)`];
|
|
4749
4761
|
return {
|
|
4750
4762
|
adapter: options.adapter,
|
|
4751
4763
|
command,
|
|
@@ -7773,4 +7785,4 @@ async function remoteAnalyzeFetch(url, apiKey, init, timeoutMs) {
|
|
|
7773
7785
|
return response.json();
|
|
7774
7786
|
}
|
|
7775
7787
|
|
|
7776
|
-
export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, shouldColorTerminalOutput, terminalStyles, stripAnsi, renderMarkdownForTerminal,
|
|
7788
|
+
export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, shouldColorTerminalOutput, terminalStyles, stripAnsi, renderMarkdownForTerminal, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, validateHarnessReport, readHarnessReport, hashHarnessReport, hashStableJson, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, renderDoctorHuman, renderStatusHuman, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, describeCodingAgentEvent, isDeterministicLocalAnalyst, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, scanHarness, summarizeHarness, buildSourceSnapshot, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, graphForSync };
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
HARNESS_REPORT_APPROVAL_PATH,
|
|
3
|
-
HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
|
|
4
|
-
HARNESS_REPORT_PATH,
|
|
5
2
|
callControlAPI,
|
|
6
3
|
callDataAPI,
|
|
7
|
-
hashHarnessReport,
|
|
8
|
-
readHarnessReport,
|
|
9
4
|
readHarnessSyncState,
|
|
10
5
|
renderDoctorHuman,
|
|
11
6
|
renderStatusHuman,
|
|
12
|
-
scanHarness
|
|
13
|
-
|
|
14
|
-
} from "./cli-ts6nhn51.js";
|
|
7
|
+
scanHarness
|
|
8
|
+
} from "./cli-76tsfpta.js";
|
|
15
9
|
import {
|
|
16
10
|
isAuthSessionValid,
|
|
17
11
|
loadAuthSession
|
|
@@ -772,7 +766,19 @@ function parsePromptFile(absPath) {
|
|
|
772
766
|
if (absPath.endsWith(".json")) {
|
|
773
767
|
const parsed = JSON.parse(raw);
|
|
774
768
|
const key2 = parsed.key || keyFromPath(sourcePath);
|
|
775
|
-
const
|
|
769
|
+
const hasContent = typeof parsed.content === "string" && parsed.content.trim().length > 0;
|
|
770
|
+
const hasSystemPrompt = typeof parsed.systemPrompt === "string" && parsed.systemPrompt.trim().length > 0;
|
|
771
|
+
const hasTemplate = typeof parsed.template === "string" && parsed.template.trim().length > 0;
|
|
772
|
+
const hasMessages = Array.isArray(parsed.messages) && parsed.messages.some((message) => {
|
|
773
|
+
if (!message || typeof message !== "object" || !("role" in message) || !["system", "developer", "user", "assistant", "tool"].includes(String(message.role)) || !("content" in message))
|
|
774
|
+
return false;
|
|
775
|
+
const content2 = message.content;
|
|
776
|
+
return typeof content2 === "string" ? content2.trim().length > 0 : Array.isArray(content2) && content2.some((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0);
|
|
777
|
+
});
|
|
778
|
+
if (!hasContent && !hasSystemPrompt && !hasMessages && !hasTemplate) {
|
|
779
|
+
throw new Error(`${sourcePath}: prompt JSON requires non-empty content, systemPrompt, messages, or a text template.`);
|
|
780
|
+
}
|
|
781
|
+
const content = hasContent ? parsed.content : hasSystemPrompt ? parsed.systemPrompt : hasMessages ? JSON.stringify(parsed.messages, null, 2) : parsed.template;
|
|
776
782
|
return normalizePrompt({ ...parsed, key: key2, name: parsed.name || key2, sourcePath, content });
|
|
777
783
|
}
|
|
778
784
|
const { frontmatter, body } = parseFrontmatter(raw);
|
|
@@ -931,11 +937,18 @@ function printJson(value) {
|
|
|
931
937
|
|
|
932
938
|
// src/init/rules.ts
|
|
933
939
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
940
|
+
import { join as join3 } from "node:path";
|
|
941
|
+
import { confirm } from "@inquirer/prompts";
|
|
942
|
+
|
|
943
|
+
// src/package-assets.ts
|
|
934
944
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
935
945
|
import { fileURLToPath } from "node:url";
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
946
|
+
function packageAssetPath(...parts) {
|
|
947
|
+
return join2(dirname2(fileURLToPath(import.meta.url)), "..", ...parts);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/init/rules.ts
|
|
951
|
+
var SKILLS_SOURCE_DIR = packageAssetPath("skills");
|
|
939
952
|
var SKILLS = [
|
|
940
953
|
{
|
|
941
954
|
id: "moda-cli",
|
|
@@ -946,7 +959,7 @@ var ADAPTERS = {
|
|
|
946
959
|
"Claude Code": {
|
|
947
960
|
name: "Claude Code",
|
|
948
961
|
targetFor: (skillId) => ({
|
|
949
|
-
dir:
|
|
962
|
+
dir: join3(".claude", "skills", skillId),
|
|
950
963
|
filename: "SKILL.md"
|
|
951
964
|
}),
|
|
952
965
|
transform: (content) => content
|
|
@@ -954,7 +967,7 @@ var ADAPTERS = {
|
|
|
954
967
|
Cursor: {
|
|
955
968
|
name: "Cursor",
|
|
956
969
|
targetFor: (skillId) => ({
|
|
957
|
-
dir:
|
|
970
|
+
dir: join3(".cursor", "rules"),
|
|
958
971
|
filename: `${skillId}.mdc`
|
|
959
972
|
}),
|
|
960
973
|
transform: claudeSkillToCursorRule
|
|
@@ -979,7 +992,7 @@ function claudeSkillToCursorRule(skillContent) {
|
|
|
979
992
|
${body.replace(/^\s+/, "")}`;
|
|
980
993
|
}
|
|
981
994
|
function loadSkillSource(skillId) {
|
|
982
|
-
const path =
|
|
995
|
+
const path = join3(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
|
|
983
996
|
if (!existsSync3(path)) {
|
|
984
997
|
throw new Error(`Skill source missing: ${path}`);
|
|
985
998
|
}
|
|
@@ -987,9 +1000,9 @@ function loadSkillSource(skillId) {
|
|
|
987
1000
|
}
|
|
988
1001
|
function installSkillForAgent(skill, adapter, content) {
|
|
989
1002
|
const { dir, filename } = adapter.targetFor(skill.id);
|
|
990
|
-
const dirPath =
|
|
991
|
-
const filePath =
|
|
992
|
-
const relativePath =
|
|
1003
|
+
const dirPath = join3(process.cwd(), dir);
|
|
1004
|
+
const filePath = join3(dirPath, filename);
|
|
1005
|
+
const relativePath = join3(dir, filename);
|
|
993
1006
|
try {
|
|
994
1007
|
mkdirSync2(dirPath, { recursive: true });
|
|
995
1008
|
if (existsSync3(filePath)) {
|
|
@@ -1075,7 +1088,7 @@ async function installRules(detectedAgents, options = {}) {
|
|
|
1075
1088
|
|
|
1076
1089
|
// src/init/mcp.ts
|
|
1077
1090
|
import { existsSync as existsSync4 } from "node:fs";
|
|
1078
|
-
import { join as
|
|
1091
|
+
import { join as join4 } from "node:path";
|
|
1079
1092
|
import { execSync } from "node:child_process";
|
|
1080
1093
|
function isOnPath(cmd) {
|
|
1081
1094
|
try {
|
|
@@ -1088,10 +1101,10 @@ function isOnPath(cmd) {
|
|
|
1088
1101
|
function detectAgents() {
|
|
1089
1102
|
const cwd = process.cwd();
|
|
1090
1103
|
const agents = [];
|
|
1091
|
-
if (existsSync4(
|
|
1104
|
+
if (existsSync4(join4(cwd, ".claude")) || isOnPath("claude")) {
|
|
1092
1105
|
agents.push("Claude Code");
|
|
1093
1106
|
}
|
|
1094
|
-
if (existsSync4(
|
|
1107
|
+
if (existsSync4(join4(cwd, ".cursor"))) {
|
|
1095
1108
|
agents.push("Cursor");
|
|
1096
1109
|
}
|
|
1097
1110
|
return agents;
|
|
@@ -1108,7 +1121,7 @@ import {
|
|
|
1108
1121
|
readFileSync as readFileSync5,
|
|
1109
1122
|
writeFileSync as writeFileSync6
|
|
1110
1123
|
} from "node:fs";
|
|
1111
|
-
import { basename, dirname as dirname5, join as
|
|
1124
|
+
import { basename, dirname as dirname5, join as join7, relative as relative3 } from "node:path";
|
|
1112
1125
|
|
|
1113
1126
|
// src/skills-manifest.ts
|
|
1114
1127
|
import {
|
|
@@ -1120,12 +1133,12 @@ import {
|
|
|
1120
1133
|
unlinkSync,
|
|
1121
1134
|
writeFileSync as writeFileSync4
|
|
1122
1135
|
} from "node:fs";
|
|
1123
|
-
import { dirname as dirname3, isAbsolute as isAbsolute4, join as
|
|
1124
|
-
var SKILLS_MANIFEST_PATH =
|
|
1136
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join5, normalize, relative as relative2, resolve as resolve4 } from "node:path";
|
|
1137
|
+
var SKILLS_MANIFEST_PATH = join5(".moda", "skills.yml");
|
|
1125
1138
|
var SKILL_SOURCE_HARNESS = "moda";
|
|
1126
1139
|
var SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
|
|
1127
1140
|
function readSkillsManifest(cwd) {
|
|
1128
|
-
const manifestPath =
|
|
1141
|
+
const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
|
|
1129
1142
|
if (!existsSync5(manifestPath))
|
|
1130
1143
|
return null;
|
|
1131
1144
|
let text = "";
|
|
@@ -1156,7 +1169,7 @@ function manifestHeaderFrom(manifest) {
|
|
|
1156
1169
|
};
|
|
1157
1170
|
}
|
|
1158
1171
|
function writeSkillsManifest(cwd, header, records) {
|
|
1159
|
-
const manifestPath =
|
|
1172
|
+
const manifestPath = join5(cwd, SKILLS_MANIFEST_PATH);
|
|
1160
1173
|
const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
|
|
1161
1174
|
const lines = [
|
|
1162
1175
|
"version: 1",
|
|
@@ -1211,7 +1224,7 @@ function pruneManagedSkillPaths(cwd, managedPaths, log) {
|
|
|
1211
1224
|
function pruneEmptyClaudeSkillDir(cwd, relativePath) {
|
|
1212
1225
|
if (!relativePath.startsWith(".claude/skills/") || !relativePath.endsWith("/SKILL.md"))
|
|
1213
1226
|
return;
|
|
1214
|
-
const dir = dirname3(
|
|
1227
|
+
const dir = dirname3(join5(cwd, relativePath));
|
|
1215
1228
|
try {
|
|
1216
1229
|
if (existsSync5(dir) && readdirSync2(dir).length === 0) {
|
|
1217
1230
|
rmdirSync(dir);
|
|
@@ -1331,7 +1344,7 @@ function yamlQuote(value) {
|
|
|
1331
1344
|
|
|
1332
1345
|
// src/agents-md.ts
|
|
1333
1346
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1334
|
-
import { dirname as dirname4, join as
|
|
1347
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
1335
1348
|
var AGENTS_MD_FILENAME = "AGENTS.md";
|
|
1336
1349
|
var FIXES_BLOCK_BEGIN = "<!-- moda:fixes:begin -->";
|
|
1337
1350
|
var FIXES_BLOCK_END = "<!-- moda:fixes:end -->";
|
|
@@ -1392,7 +1405,7 @@ function writeFileIfChanged(filePath, content) {
|
|
|
1392
1405
|
}
|
|
1393
1406
|
}
|
|
1394
1407
|
function upsertFixesAgentsBlock(cwd) {
|
|
1395
|
-
const filePath =
|
|
1408
|
+
const filePath = join6(cwd, AGENTS_MD_FILENAME);
|
|
1396
1409
|
let existing = null;
|
|
1397
1410
|
if (existsSync6(filePath)) {
|
|
1398
1411
|
try {
|
|
@@ -1408,7 +1421,7 @@ function upsertFixesAgentsBlock(cwd) {
|
|
|
1408
1421
|
// src/skills.ts
|
|
1409
1422
|
var SKILLS_REQUEST_TIMEOUT_MS = 15000;
|
|
1410
1423
|
var VALID_STATUSES = new Set(["approved", "proposed", "all"]);
|
|
1411
|
-
var SKILL_PROPOSALS_MANIFEST_PATH =
|
|
1424
|
+
var SKILL_PROPOSALS_MANIFEST_PATH = join7(".moda", "skill-proposals.yml");
|
|
1412
1425
|
async function fetchSkills(status) {
|
|
1413
1426
|
const apiKey = resolveApiKey();
|
|
1414
1427
|
if (!apiKey) {
|
|
@@ -1578,7 +1591,7 @@ async function pullSkills(options = {}) {
|
|
|
1578
1591
|
}
|
|
1579
1592
|
}
|
|
1580
1593
|
const agents = detectAgents();
|
|
1581
|
-
const writeCursor = agents.includes("Cursor") || existsSync7(
|
|
1594
|
+
const writeCursor = agents.includes("Cursor") || existsSync7(join7(cwd, ".cursor"));
|
|
1582
1595
|
const records = [];
|
|
1583
1596
|
for (const skill of skills) {
|
|
1584
1597
|
if (!skill.skill_id || !skill.skill_md) {
|
|
@@ -1591,7 +1604,7 @@ async function pullSkills(options = {}) {
|
|
|
1591
1604
|
log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
|
|
1592
1605
|
continue;
|
|
1593
1606
|
}
|
|
1594
|
-
const claudePath =
|
|
1607
|
+
const claudePath = join7(cwd, ".claude", "skills", safeId, "SKILL.md");
|
|
1595
1608
|
const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
|
|
1596
1609
|
const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
|
|
1597
1610
|
recordOutcome(summary, claudeStatus, log, claudeRelativePath);
|
|
@@ -1599,7 +1612,7 @@ async function pullSkills(options = {}) {
|
|
|
1599
1612
|
if (claudeStatus !== "failed")
|
|
1600
1613
|
paths.push(claudeRelativePath);
|
|
1601
1614
|
if (writeCursor) {
|
|
1602
|
-
const cursorPath =
|
|
1615
|
+
const cursorPath = join7(cwd, ".cursor", "rules", `${safeId}.mdc`);
|
|
1603
1616
|
const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
|
|
1604
1617
|
const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
|
|
1605
1618
|
recordOutcome(summary, cursorStatus, log, cursorRelativePath);
|
|
@@ -1637,7 +1650,7 @@ function recordOutcome(summary, status, log, relativePath) {
|
|
|
1637
1650
|
break;
|
|
1638
1651
|
}
|
|
1639
1652
|
}
|
|
1640
|
-
var SKILLS_DIR =
|
|
1653
|
+
var SKILLS_DIR = join7(".claude", "skills");
|
|
1641
1654
|
var SKILL_FILE = "SKILL.md";
|
|
1642
1655
|
var MAX_SKILL_WALK_DEPTH = 4;
|
|
1643
1656
|
async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
|
|
@@ -1668,7 +1681,7 @@ async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
|
|
|
1668
1681
|
return { synced: response.synced ?? [], dryRun, ...agentsMd ? { agentsMd } : {} };
|
|
1669
1682
|
}
|
|
1670
1683
|
function discoverLocalSkills(cwd) {
|
|
1671
|
-
const skillsRoot =
|
|
1684
|
+
const skillsRoot = join7(cwd, SKILLS_DIR);
|
|
1672
1685
|
if (!existsSync7(skillsRoot))
|
|
1673
1686
|
return [];
|
|
1674
1687
|
const managed = new Set(managedSkillPaths(cwd));
|
|
@@ -1717,7 +1730,7 @@ function walkSkillFiles(dir, depth) {
|
|
|
1717
1730
|
}
|
|
1718
1731
|
const files = [];
|
|
1719
1732
|
for (const entry of entries) {
|
|
1720
|
-
const fullPath =
|
|
1733
|
+
const fullPath = join7(dir, entry);
|
|
1721
1734
|
let stat;
|
|
1722
1735
|
try {
|
|
1723
1736
|
stat = lstatSync2(fullPath);
|
|
@@ -1791,7 +1804,7 @@ async function applySkillProposal(proposalId, flags, profileOptions = {}, option
|
|
|
1791
1804
|
}
|
|
1792
1805
|
const safeId = safeSkillId(proposal.skillId);
|
|
1793
1806
|
const relativePath = `.claude/skills/${safeId}/SKILL.md`;
|
|
1794
|
-
const writeStatus = writeIfChanged(
|
|
1807
|
+
const writeStatus = writeIfChanged(join7(cwd, relativePath), proposal.skillMd);
|
|
1795
1808
|
if (writeStatus === "failed") {
|
|
1796
1809
|
throw new Error(`Failed to write ${relativePath}`);
|
|
1797
1810
|
}
|
|
@@ -1824,7 +1837,7 @@ function safeSkillId(skillId) {
|
|
|
1824
1837
|
return safeId;
|
|
1825
1838
|
}
|
|
1826
1839
|
function writeSkillProposalManifest(cwd, proposal, relativePath) {
|
|
1827
|
-
const manifestPath =
|
|
1840
|
+
const manifestPath = join7(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
|
|
1828
1841
|
mkdirSync5(dirname5(manifestPath), { recursive: true });
|
|
1829
1842
|
const existing = existsSync7(manifestPath) ? readFileSync5(manifestPath, "utf8") : `applied_proposals:
|
|
1830
1843
|
`;
|
|
@@ -2085,7 +2098,7 @@ function sha2562(input) {
|
|
|
2085
2098
|
// src/doctor.ts
|
|
2086
2099
|
import { createHash as createHash3 } from "node:crypto";
|
|
2087
2100
|
import { existsSync as existsSync8, readFileSync as readFileSync6, statSync } from "node:fs";
|
|
2088
|
-
import { join as
|
|
2101
|
+
import { join as join8 } from "node:path";
|
|
2089
2102
|
async function runDoctorCommand(context) {
|
|
2090
2103
|
const report = await buildDoctorReport(context, {
|
|
2091
2104
|
offline: context.flags.offline === "true"
|
|
@@ -2122,7 +2135,7 @@ async function runStatusCommand(context) {
|
|
|
2122
2135
|
dataFlow: checkStatus(report.checks, "cloud_data_flow"),
|
|
2123
2136
|
harness: checkStatus(report.checks, "harness_manifest_current"),
|
|
2124
2137
|
harnessSync: checkStatus(report.checks, "harness_sync_state_present"),
|
|
2125
|
-
harnessReport:
|
|
2138
|
+
harnessReport: "skipped",
|
|
2126
2139
|
shadowSpark: checkStatus(report.checks, "shadow_spark_workspace_health"),
|
|
2127
2140
|
prompts: checkStatus(report.checks, "prompts_discovered"),
|
|
2128
2141
|
skills: checkStatus(report.checks, "skills_discovered"),
|
|
@@ -2208,7 +2221,7 @@ async function buildDoctorReport(context, options) {
|
|
|
2208
2221
|
checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
|
|
2209
2222
|
checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
|
|
2210
2223
|
checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
|
|
2211
|
-
const manifestPath =
|
|
2224
|
+
const manifestPath = join8(context.cwd, ".moda", "harness.json");
|
|
2212
2225
|
const manifestExists = existsSync8(manifestPath);
|
|
2213
2226
|
checks.push({
|
|
2214
2227
|
id: "harness_manifest_exists",
|
|
@@ -2222,7 +2235,6 @@ async function buildDoctorReport(context, options) {
|
|
|
2222
2235
|
});
|
|
2223
2236
|
checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
|
|
2224
2237
|
checks.push(harnessSyncStateCheck(context.cwd));
|
|
2225
|
-
checks.push(harnessReportStateCheck(context.cwd));
|
|
2226
2238
|
checks.push(discoveryCheck("prompts_discovered", "harness", "Prompts are discovered", context.cwd, [
|
|
2227
2239
|
"prompts",
|
|
2228
2240
|
".moda/prompts.yml"
|
|
@@ -2237,7 +2249,6 @@ async function buildDoctorReport(context, options) {
|
|
|
2237
2249
|
"tests",
|
|
2238
2250
|
"__tests__"
|
|
2239
2251
|
]));
|
|
2240
|
-
checks.push(sdkInitializationCheck(context.cwd));
|
|
2241
2252
|
checks.push(validationEventPathCheck(ingestUrl));
|
|
2242
2253
|
if (options.offline || !apiKey) {
|
|
2243
2254
|
checks.push(...offlineCloudChecks(options.offline));
|
|
@@ -2497,120 +2508,8 @@ function harnessSyncStateCheck(cwd) {
|
|
|
2497
2508
|
]
|
|
2498
2509
|
};
|
|
2499
2510
|
}
|
|
2500
|
-
function harnessReportStateCheck(cwd) {
|
|
2501
|
-
const reportPath = join7(cwd, HARNESS_REPORT_PATH);
|
|
2502
|
-
if (!existsSync8(reportPath)) {
|
|
2503
|
-
return {
|
|
2504
|
-
id: "harness_report_approved_synced",
|
|
2505
|
-
group: "harness",
|
|
2506
|
-
title: "Harness analyst report is approved and synced",
|
|
2507
|
-
status: "warning",
|
|
2508
|
-
severity: "warning",
|
|
2509
|
-
evidence: [{ label: "path", value: HARNESS_REPORT_PATH }],
|
|
2510
|
-
remediation: "Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.",
|
|
2511
|
-
nextCommand: "moda harness analyze --experimental --summary"
|
|
2512
|
-
};
|
|
2513
|
-
}
|
|
2514
|
-
try {
|
|
2515
|
-
const report = readHarnessReport(reportPath);
|
|
2516
|
-
const validation = validateHarnessReport(report, cwd);
|
|
2517
|
-
const reportHash = hashHarnessReport(report);
|
|
2518
|
-
const evidence = [
|
|
2519
|
-
{ label: "path", value: HARNESS_REPORT_PATH },
|
|
2520
|
-
{ label: "report_hash", value: reportHash.slice(0, 12) },
|
|
2521
|
-
{ label: "validation", value: validation.status }
|
|
2522
|
-
];
|
|
2523
|
-
if (validation.status !== "pass") {
|
|
2524
|
-
return {
|
|
2525
|
-
id: "harness_report_approved_synced",
|
|
2526
|
-
group: "harness",
|
|
2527
|
-
title: "Harness analyst report is approved and synced",
|
|
2528
|
-
status: "fail",
|
|
2529
|
-
severity: "critical",
|
|
2530
|
-
evidence: [
|
|
2531
|
-
...evidence,
|
|
2532
|
-
{ label: "errors", value: validation.errors.slice(0, 3).join(" | ") || "validation failed" }
|
|
2533
|
-
],
|
|
2534
|
-
remediation: "Regenerate or fix the harness report before approving or syncing.",
|
|
2535
|
-
nextCommand: "moda harness analyze --experimental --summary"
|
|
2536
|
-
};
|
|
2537
|
-
}
|
|
2538
|
-
const approval = readHarnessReportApproval(cwd);
|
|
2539
|
-
const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION && approval.reportId === report.reportId && approval.reportHash === reportHash && approval.approved === true;
|
|
2540
|
-
evidence.push({ label: "approval", value: approved ? "approved" : approval ? "stale_or_invalid" : "missing" });
|
|
2541
|
-
if (approval && !approved) {
|
|
2542
|
-
return {
|
|
2543
|
-
id: "harness_report_approved_synced",
|
|
2544
|
-
group: "harness",
|
|
2545
|
-
title: "Harness analyst report is approved and synced",
|
|
2546
|
-
status: "fail",
|
|
2547
|
-
severity: "critical",
|
|
2548
|
-
evidence,
|
|
2549
|
-
remediation: "Approval no longer matches the current report. Review and approve the report again.",
|
|
2550
|
-
nextCommand: "moda harness approve --yes"
|
|
2551
|
-
};
|
|
2552
|
-
}
|
|
2553
|
-
if (!approved) {
|
|
2554
|
-
return {
|
|
2555
|
-
id: "harness_report_approved_synced",
|
|
2556
|
-
group: "harness",
|
|
2557
|
-
title: "Harness analyst report is approved and synced",
|
|
2558
|
-
status: "warning",
|
|
2559
|
-
severity: "warning",
|
|
2560
|
-
evidence,
|
|
2561
|
-
remediation: "Review and approve the validated harness report before syncing.",
|
|
2562
|
-
nextCommand: "moda harness approve --yes"
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
const state = readHarnessSyncState(cwd);
|
|
2566
|
-
const synced = state?.source === "approved_report" && state.report?.reportHash === reportHash;
|
|
2567
|
-
evidence.push({ label: "sync", value: synced ? "synced" : "not_synced_to_this_report" }, ...state?.report?.reportHash ? [{ label: "sync_report_hash", value: state.report.reportHash.slice(0, 12) }] : []);
|
|
2568
|
-
return {
|
|
2569
|
-
id: "harness_report_approved_synced",
|
|
2570
|
-
group: "harness",
|
|
2571
|
-
title: "Harness analyst report is approved and synced",
|
|
2572
|
-
status: synced ? "pass" : "warning",
|
|
2573
|
-
severity: synced ? "info" : "warning",
|
|
2574
|
-
evidence,
|
|
2575
|
-
remediation: synced ? undefined : "Sync the approved harness report so Moda cloud has the same harness graph.",
|
|
2576
|
-
nextCommand: synced ? undefined : "moda harness sync --from-report .moda/harness-report.json"
|
|
2577
|
-
};
|
|
2578
|
-
} catch (err) {
|
|
2579
|
-
return {
|
|
2580
|
-
id: "harness_report_approved_synced",
|
|
2581
|
-
group: "harness",
|
|
2582
|
-
title: "Harness analyst report is approved and synced",
|
|
2583
|
-
status: "fail",
|
|
2584
|
-
severity: "critical",
|
|
2585
|
-
evidence: [
|
|
2586
|
-
{ label: "path", value: HARNESS_REPORT_PATH },
|
|
2587
|
-
{ label: "error", value: err instanceof Error ? err.message : String(err) }
|
|
2588
|
-
],
|
|
2589
|
-
remediation: "Regenerate the harness report.",
|
|
2590
|
-
nextCommand: "moda harness analyze --experimental --summary"
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
function readHarnessReportApproval(cwd) {
|
|
2595
|
-
const approvalPath = join7(cwd, HARNESS_REPORT_APPROVAL_PATH);
|
|
2596
|
-
if (!existsSync8(approvalPath))
|
|
2597
|
-
return null;
|
|
2598
|
-
try {
|
|
2599
|
-
return JSON.parse(readFileSync6(approvalPath, "utf8"));
|
|
2600
|
-
} catch {
|
|
2601
|
-
return {
|
|
2602
|
-
schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
|
|
2603
|
-
reportId: "unreadable",
|
|
2604
|
-
reportHash: "unreadable",
|
|
2605
|
-
approved: true,
|
|
2606
|
-
approvedAt: "",
|
|
2607
|
-
approvedBy: "local-human",
|
|
2608
|
-
reportPath: HARNESS_REPORT_PATH
|
|
2609
|
-
};
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
2511
|
function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
|
|
2613
|
-
const found = relativePaths.filter((path) => existsSync8(
|
|
2512
|
+
const found = relativePaths.filter((path) => existsSync8(join8(cwd, path)));
|
|
2614
2513
|
return {
|
|
2615
2514
|
id,
|
|
2616
2515
|
group,
|
|
@@ -2622,29 +2521,6 @@ function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
|
|
|
2622
2521
|
nextCommand: found.length > 0 ? undefined : nextCommand
|
|
2623
2522
|
};
|
|
2624
2523
|
}
|
|
2625
|
-
function sdkInitializationCheck(cwd) {
|
|
2626
|
-
const candidates = ["src", "app", "agent_harness"];
|
|
2627
|
-
let detected = false;
|
|
2628
|
-
for (const candidate of candidates) {
|
|
2629
|
-
const path = join7(cwd, candidate);
|
|
2630
|
-
if (!existsSync8(path))
|
|
2631
|
-
continue;
|
|
2632
|
-
try {
|
|
2633
|
-
const stat = statSync(path);
|
|
2634
|
-
detected ||= stat.isDirectory() || stat.isFile();
|
|
2635
|
-
} catch {}
|
|
2636
|
-
}
|
|
2637
|
-
return {
|
|
2638
|
-
id: "sdk_initialization_detected",
|
|
2639
|
-
group: "harness",
|
|
2640
|
-
title: "SDK initialization is detectable",
|
|
2641
|
-
status: detected ? "warning" : "skipped",
|
|
2642
|
-
severity: "warning",
|
|
2643
|
-
evidence: [{ label: "status", value: detected ? "code present, instrumentation not verified" : "no source candidates" }],
|
|
2644
|
-
remediation: "Use the generated integration guide to add Moda SDK instrumentation.",
|
|
2645
|
-
nextCommand: "moda init"
|
|
2646
|
-
};
|
|
2647
|
-
}
|
|
2648
2524
|
function validationEventPathCheck(ingestUrl) {
|
|
2649
2525
|
return {
|
|
2650
2526
|
id: "validation_event_path_defined",
|
|
@@ -2950,4 +2826,4 @@ function isRecord(value) {
|
|
|
2950
2826
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2951
2827
|
}
|
|
2952
2828
|
|
|
2953
|
-
export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, upsertSkillManifestRecord, runSkillSync, runSkillsCommand, runDoctorCommand, runStatusCommand, buildDoctorReport };
|
|
2829
|
+
export { DEFAULT_POLL_INTERVAL_MS, DEFAULT_WAIT_TIMEOUT_MS, fetchLatestRun, runPromptsCommand, initPrompts, runPromptSync, packageAssetPath, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, upsertSkillManifestRecord, runSkillSync, runSkillsCommand, runDoctorCommand, runStatusCommand, buildDoctorReport };
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runPromptsCommand,
|
|
9
9
|
runSkillsCommand,
|
|
10
10
|
runStatusCommand
|
|
11
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-aeggte1q.js";
|
|
12
12
|
import {
|
|
13
13
|
ApiError,
|
|
14
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
summarizeHarness,
|
|
30
30
|
terminalStyles,
|
|
31
31
|
validateHarnessReport
|
|
32
|
-
} from "./cli-
|
|
32
|
+
} from "./cli-76tsfpta.js";
|
|
33
33
|
import {
|
|
34
34
|
authFetch,
|
|
35
35
|
clearAuthSession,
|
|
@@ -152,6 +152,29 @@ var ProblemsSchema = z.object({
|
|
|
152
152
|
limit: z.number().min(1).max(25).default(25).optional()
|
|
153
153
|
});
|
|
154
154
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
155
|
+
var ProblemImpactSchema = z.object({
|
|
156
|
+
node_id: z.string().min(1).max(256).optional(),
|
|
157
|
+
problem_id: z.string().regex(UUID_RE).optional(),
|
|
158
|
+
run_id: z.string().min(1).max(256).optional(),
|
|
159
|
+
days_back: z.union([z.literal("all"), z.coerce.number().refine((n) => [1, 3, 7, 30, 90].includes(n), "Use all, 1, 3, 7, 30, or 90 days")]).default(7),
|
|
160
|
+
mode: z.enum(["metrics", "trend", "evidence", "catalog", "overview", "affected-use-cases", "trace-use-cases"]).default("metrics"),
|
|
161
|
+
to: z.string().datetime({ precision: 3 }).optional(),
|
|
162
|
+
cursor: z.string().min(1).optional(),
|
|
163
|
+
conversation_ids: z.string().transform((value, ctx) => {
|
|
164
|
+
try {
|
|
165
|
+
return z.array(z.string().min(1).max(512)).min(1).max(25).parse(JSON.parse(value));
|
|
166
|
+
} catch {
|
|
167
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Use a JSON array of 1–25 conversation IDs" });
|
|
168
|
+
return z.NEVER;
|
|
169
|
+
}
|
|
170
|
+
}).optional()
|
|
171
|
+
}).refine((p) => p.mode !== "trace-use-cases" || p.problem_id && p.conversation_ids, {
|
|
172
|
+
message: "Trace use cases require --problem-id and --conversation-ids"
|
|
173
|
+
}).refine((p) => p.node_id || p.problem_id || ["catalog", "overview"].includes(p.mode), {
|
|
174
|
+
message: "Select --node-id or --problem-id, or use --mode=catalog/overview"
|
|
175
|
+
}).refine((p) => !p.cursor || p.mode === "evidence" && p.to && p.run_id, {
|
|
176
|
+
message: "An evidence cursor requires --mode=evidence, --to, and --run-id from the previous response"
|
|
177
|
+
});
|
|
155
178
|
var PROBLEM_FAMILIES = [
|
|
156
179
|
"tool_failure",
|
|
157
180
|
"emotion",
|
|
@@ -5356,6 +5379,7 @@ Commands:
|
|
|
5356
5379
|
tool-failures Get tool failure overview
|
|
5357
5380
|
tool-failure-detail <tool_name> Get per-tool failure detail
|
|
5358
5381
|
problems Rank cross-signal Problems by root cause (what to fix first)
|
|
5382
|
+
problem-impact Live problem/use-case impact (--node-id, --problem-id, --mode, --days-back)
|
|
5359
5383
|
problem <problem_id> Open one Problem: dossier, or --evidence/--reports/--traces/--feedback
|
|
5360
5384
|
problem-feedback <problem_id> Mark a Problem fixed, dismiss, rename, or flag a bad attribution
|
|
5361
5385
|
step-scores <conversation_id> Graph-PRM step scores (per-segment curves, first bad step, rollup)
|
|
@@ -5573,7 +5597,7 @@ Examples:
|
|
|
5573
5597
|
moda --agent doctor
|
|
5574
5598
|
moda agent context
|
|
5575
5599
|
moda agent next-action
|
|
5576
|
-
moda agent evidence doctor:
|
|
5600
|
+
moda agent evidence doctor:api_key_resolves
|
|
5577
5601
|
moda investigate
|
|
5578
5602
|
moda failures
|
|
5579
5603
|
moda ask "what should I fix first?"
|
|
@@ -6221,6 +6245,35 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
6221
6245
|
await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings);
|
|
6222
6246
|
break;
|
|
6223
6247
|
}
|
|
6248
|
+
case "problem-impact": {
|
|
6249
|
+
const params = ProblemImpactSchema.parse(Object.fromEntries(Object.entries(flags).map(([key, value]) => [key.replace(/-/g, "_"), value])));
|
|
6250
|
+
const query = new URLSearchParams({ days: String(params.days_back), mode: params.mode });
|
|
6251
|
+
for (const [key, value] of Object.entries({
|
|
6252
|
+
nodeId: params.node_id,
|
|
6253
|
+
problemId: params.problem_id,
|
|
6254
|
+
runId: params.run_id,
|
|
6255
|
+
to: params.to,
|
|
6256
|
+
cursor: params.cursor,
|
|
6257
|
+
conversationIds: params.conversation_ids ? JSON.stringify(params.conversation_ids) : undefined
|
|
6258
|
+
}))
|
|
6259
|
+
if (value !== undefined)
|
|
6260
|
+
query.set(key, value);
|
|
6261
|
+
const data = asRecord(await callDataAPI(`/problem-impact?${query}`)) ?? {};
|
|
6262
|
+
const link = new URLSearchParams({ impactDays: String(params.days_back) });
|
|
6263
|
+
const window = asRecord(data.window);
|
|
6264
|
+
const resolvedRun = asString(data.runId);
|
|
6265
|
+
const windowEnd = asString(window?.to);
|
|
6266
|
+
if (resolvedRun)
|
|
6267
|
+
link.set("run", resolvedRun);
|
|
6268
|
+
if (windowEnd)
|
|
6269
|
+
link.set("impactTo", windowEnd);
|
|
6270
|
+
if (params.node_id)
|
|
6271
|
+
link.set("useCase", params.node_id);
|
|
6272
|
+
const problem = asString(data.problemId) || params.problem_id;
|
|
6273
|
+
const path = problem ? `/dashboard/problems/${encodeURIComponent(problem)}` : params.node_id ? `/dashboard/use-cases/${encodeURIComponent(params.node_id)}` : "/dashboard/use-cases";
|
|
6274
|
+
context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` });
|
|
6275
|
+
break;
|
|
6276
|
+
}
|
|
6224
6277
|
case "problems": {
|
|
6225
6278
|
const args = flagsToArgs(flags);
|
|
6226
6279
|
const params = ProblemsSchema.parse(args);
|
|
@@ -6526,7 +6579,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
6526
6579
|
resetApiRequestCountBeforeRun: true,
|
|
6527
6580
|
telemetry: "result",
|
|
6528
6581
|
handler: async (context) => {
|
|
6529
|
-
const { runInit } = await import("./index-
|
|
6582
|
+
const { runInit } = await import("./index-kx1t9k2w.js");
|
|
6530
6583
|
if (context.outputMode === "agent-stream") {
|
|
6531
6584
|
context.output.writeEvent({
|
|
6532
6585
|
event: "started",
|
|
@@ -6769,7 +6822,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
6769
6822
|
"moda agent context",
|
|
6770
6823
|
"moda agent investigate",
|
|
6771
6824
|
"moda agent next-action",
|
|
6772
|
-
"moda agent evidence doctor:
|
|
6825
|
+
"moda agent evidence doctor:api_key_resolves"
|
|
6773
6826
|
],
|
|
6774
6827
|
auth: "none",
|
|
6775
6828
|
outputModes: ["agent", "agent-stream", "json"],
|
|
@@ -6908,6 +6961,21 @@ var commandRegistry = createCommandRegistry([
|
|
|
6908
6961
|
examples: ["moda problems", "moda problems --days-back=7 --limit=10"],
|
|
6909
6962
|
...dataApiDefaults
|
|
6910
6963
|
}),
|
|
6964
|
+
legacyCommand({
|
|
6965
|
+
name: "problem-impact",
|
|
6966
|
+
description: "Live problem/use-case impact, full analysis denominators, daily recurrence, and paginated evidence",
|
|
6967
|
+
examples: [
|
|
6968
|
+
"moda problem-impact --mode=catalog",
|
|
6969
|
+
"moda problem-impact --node-id=node_0 --days-back=all",
|
|
6970
|
+
"moda problem-impact --problem-id=<uuid> --mode=metrics",
|
|
6971
|
+
"moda problem-impact --node-id=node_0 --problem-id=<uuid> --mode=evidence",
|
|
6972
|
+
"moda problem-impact --node-id=node_0 --mode=trend --days-back=30",
|
|
6973
|
+
"moda problem-impact --mode=overview --days-back=7",
|
|
6974
|
+
"moda problem-impact --problem-id=<uuid> --mode=affected-use-cases",
|
|
6975
|
+
`moda problem-impact --problem-id=<uuid> --mode=trace-use-cases --conversation-ids='["trace_id"]'`
|
|
6976
|
+
],
|
|
6977
|
+
...dataApiDefaults
|
|
6978
|
+
}),
|
|
6911
6979
|
legacyCommand({
|
|
6912
6980
|
name: "problem",
|
|
6913
6981
|
description: "Open one Problem: dossier, or --evidence/--reports/--traces/--feedback pages (--conversations is a legacy alias of --traces)",
|
|
@@ -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-aeggte1q.js";
|
|
13
14
|
import {
|
|
14
15
|
codingAgentDisplayName,
|
|
15
16
|
describeCodingAgentEvent,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
runHarnessCommand,
|
|
28
29
|
startRemoteAnalyze,
|
|
29
30
|
stripAnsi
|
|
30
|
-
} from "./cli-
|
|
31
|
+
} from "./cli-76tsfpta.js";
|
|
31
32
|
import {
|
|
32
33
|
selectTenantAndCreateKey
|
|
33
34
|
} from "./cli-pq6rte0w.js";
|
|
@@ -57,9 +58,7 @@ import { dirname as dirname2, join as join3 } from "node:path";
|
|
|
57
58
|
import { createHash } from "node:crypto";
|
|
58
59
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
59
60
|
import { dirname, join, relative } from "node:path";
|
|
60
|
-
|
|
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-15T22:29:26.871Z",
|
|
4
|
+
"cli_version": "1.31.3",
|
|
5
5
|
"skills": [
|
|
6
|
+
{
|
|
7
|
+
"id": "integration-cloudflare-think",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"summary": "Instrument Think on Workers with AI SDK 7, prompt versions, and acknowledged JSON export.",
|
|
10
|
+
"targets": {
|
|
11
|
+
"language": "node",
|
|
12
|
+
"frameworks": [
|
|
13
|
+
"cloudflare-think"
|
|
14
|
+
],
|
|
15
|
+
"providers": []
|
|
16
|
+
}
|
|
17
|
+
},
|
|
6
18
|
{
|
|
7
19
|
"id": "integration-node-anthropic",
|
|
8
20
|
"version": "1.0.0",
|
|
@@ -51,7 +63,7 @@
|
|
|
51
63
|
},
|
|
52
64
|
{
|
|
53
65
|
"id": "integration-node-vercel-ai-sdk",
|
|
54
|
-
"version": "1.0.
|
|
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
|