@moda-ai/cli 1.30.1 → 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-mq00fkwt.js → cli-j3rr0nh7.js} +57 -38
- package/dist/{cli-js4bmw21.js → cli-p3271yp2.js} +232 -50
- package/dist/cli.js +76 -3
- package/dist/{harness-github-actions-v98snak8.js → harness-github-actions-kbw4z7q4.js} +1 -1
- package/dist/{index-14sr487g.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 };
|