@imenam/mcp-documentor 1.0.86 → 1.0.88
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 +7 -5
- package/dist/shared/read-only.js +1 -1
- package/dist/shared/schemas/doc.schema.d.ts +3 -1
- package/dist/shared/schemas/doc.schema.js +13 -1
- package/dist/src/core/code-source.d.ts +32 -0
- package/dist/src/core/code-source.js +23 -4
- package/dist/src/core/embedding-engine.d.ts +1 -1
- package/dist/src/core/embedding-engine.js +3 -3
- package/dist/src/core/embedding-store.d.ts +1 -1
- package/dist/src/core/embedding-store.js +4 -4
- package/dist/src/core/git-sync.d.ts +5 -0
- package/dist/src/core/git-sync.js +29 -2
- package/dist/src/core/github-code-api.d.ts +3 -8
- package/dist/src/core/github-code-api.js +67 -36
- package/dist/src/core/github-log.d.ts +38 -0
- package/dist/src/core/github-log.js +64 -0
- package/dist/src/core/log-reader.d.ts +47 -0
- package/dist/src/core/log-reader.js +106 -0
- package/dist/src/core/storage-engine.d.ts +27 -2
- package/dist/src/core/storage-engine.js +99 -23
- package/dist/src/gui-worker.js +58 -1
- package/dist/src/index.js +78 -12
- package/dist/src/tools/code-sources.d.ts +9 -0
- package/dist/src/tools/code-sources.js +18 -16
- package/dist/src/tools/create-project.d.ts +19 -0
- package/dist/src/tools/create-project.js +42 -0
- package/dist/src/tools/delete-project.d.ts +16 -0
- package/dist/src/tools/delete-project.js +25 -0
- package/dist/src/tools/git-diff-doc.js +123 -98
- package/dist/src/tools/move-doc.js +20 -6
- package/dist/src/tools/patch-doc.js +14 -3
- package/gui/dist/assets/{arc-DBgCQU2O.js → arc-s4GL8QZ_.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BIPBAzZr.js → architectureDiagram-3BPJPVTR-CfDERg0r.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-BE9WwuyF.js → blockDiagram-GPEHLZMM-CjDoEG3-.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-V1_4ktIt.js → c4Diagram-AAUBKEIU-9o7f2UhK.js} +1 -1
- package/gui/dist/assets/channel-BsKHRH5P.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-DskG-Kmf.js → chunk-2J33WTMH-B37ouRYO.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-DaPhidEI.js → chunk-4BX2VUAB-Dcq44Skv.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-C9r6WDFM.js → chunk-55IACEB6-CLyM-RQf.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-BA-aC8US.js → chunk-727SXJPM-BjzEo2iz.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-ByBp3whV.js → chunk-AQP2D5EJ-Co03OHPi.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-CzqUByx7.js → chunk-FMBD7UC4-ClbF2rAp.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-C0_B2ryQ.js → chunk-ND2GUHAM-CN5cTCWu.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-B3eEwjDh.js → chunk-QZHKN3VN-Bg4-kEPK.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-DphzXyc8.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-DphzXyc8.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-D_i0I2pU.js → cose-bilkent-S5V4N54A-6Dkp59bE.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-DtEVPK_z.js → dagre-BM42HDAG-2dD-ID2T.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-BftnXa5w.js → diagram-2AECGRRQ-DBE2f2Lb.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-Dh9-kl_u.js → diagram-5GNKFQAL-CkKPHiH9.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-CvB0BWCa.js → diagram-KO2AKTUF-uWtGAF2N.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-CDy-J1uy.js → diagram-LMA3HP47-DyQaWg_0.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-iY9FwkIF.js → diagram-OG6HWLK6-H_DfuB5t.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-BT2_IX8D.js → erDiagram-TEJ5UH35-Cj9K5EXA.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-DaixWyMx.js → flowDiagram-I6XJVG4X-BYF24Jvt.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-DSo3cQPH.js → ganttDiagram-6RSMTGT7-D93LjmhI.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-BfFT7Lds.js → gitGraphDiagram-PVQCEYII-EVTcusDb.js} +1 -1
- package/gui/dist/assets/index-CtPuh-0k.css +1 -0
- package/gui/dist/assets/{index-DPGmAc6z.js → index-DvVxuv5N.js} +154 -152
- package/gui/dist/assets/{infoDiagram-5YYISTIA-BxbxNxe2.js → infoDiagram-5YYISTIA-DeGbaexj.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DpopzqZr.js → ishikawaDiagram-YF4QCWOH-DNdk7sPJ.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-DrOlFzIk.js → journeyDiagram-JHISSGLW-yLLfyn3W.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-vH81lYXy.js → kanban-definition-UN3LZRKU-D8yvVgr8.js} +1 -1
- package/gui/dist/assets/{linear-BipFpFah.js → linear-j3lplktA.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-DsTxCLgj.js → mindmap-definition-RKZ34NQL-t9adVeMH.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-Bbq2ZWQU.js → pieDiagram-4H26LBE5-6Yrvo8h8.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BdTSaEo4.js → quadrantDiagram-W4KKPZXB-C_3Su6rX.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-BndI8YZy.js → requirementDiagram-4Y6WPE33-B1qYSC11.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-ofBkVITf.js → sankeyDiagram-5OEKKPKP-D3Ervn-u.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-Cjj2UrN9.js → sequenceDiagram-3UESZ5HK-CKgTeWBS.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-CzPrXUuz.js → stateDiagram-AJRCARHV-D_PjEdFH.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-KPbsissC.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-Cz4X-IOO.js → timeline-definition-PNZ67QCA-HeWRiNHu.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-BwIgXJcZ.js → vennDiagram-CIIHVFJN-CREk7vzm.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-keQS0aNZ.js → wardley-L42UT6IY-BWtRQ7ZH.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-DhykSgbD.js → wardleyDiagram-YWT4CUSO-BvlqWWwp.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DFTqc4cj.js → xychartDiagram-2RQKCTM6-BCjv6pwc.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/gui/dist/assets/channel-B_cci0fU.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-BqlcJ5BP.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-BqlcJ5BP.js +0 -1
- package/gui/dist/assets/index-CxCC1gxu.css +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DAq4ObN9.js +0 -1
package/dist/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
9
9
|
import { APP_VERSION } from "./version.js";
|
|
10
10
|
import { GuiLauncher, createLogger, setupLogging } from "@imenam/mcp-gui-interface";
|
|
11
11
|
import { resolveLogDir } from "./core/log-dir.js";
|
|
12
|
+
import { initGitHubLog, runTriggeredBy } from "./core/github-log.js";
|
|
12
13
|
import { StorageEngine } from "./core/storage-engine.js";
|
|
13
14
|
import { EmbeddingStore } from "./core/embedding-store.js";
|
|
14
15
|
import { EmbeddingEngine } from "./core/embedding-engine.js";
|
|
@@ -35,6 +36,8 @@ import { gitDiffDoc } from "./tools/git-diff-doc.js";
|
|
|
35
36
|
import { checkLinks } from "./tools/check-links.js";
|
|
36
37
|
import { deleteDoc } from "./tools/delete-doc.js";
|
|
37
38
|
import { moveDoc } from "./tools/move-doc.js";
|
|
39
|
+
import { deleteProject } from "./tools/delete-project.js";
|
|
40
|
+
import { createProject } from "./tools/create-project.js";
|
|
38
41
|
import { getDocTree } from "./tools/get-doc-tree.js";
|
|
39
42
|
import { migrateFolders } from "./core/migrate-folders.js";
|
|
40
43
|
import { migrateEmbeddings } from "./core/migrate-embeddings.js";
|
|
@@ -79,6 +82,7 @@ function findProjectRoot(startDir) {
|
|
|
79
82
|
const rootDir = findProjectRoot(__dirname);
|
|
80
83
|
const packageEnvPath = path.join(rootDir, ".env");
|
|
81
84
|
setupLogging({ processLabel: "mcp-documentor", logDir: resolveLogDir(rootDir) });
|
|
85
|
+
initGitHubLog(resolveLogDir(rootDir));
|
|
82
86
|
const logger = createLogger("mcp-documentor");
|
|
83
87
|
function getConfig() {
|
|
84
88
|
if (fs.existsSync(packageEnvPath)) {
|
|
@@ -126,9 +130,13 @@ function broadcastToGUI(message) {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Each GUI message is one operation: the branch head it reads is resolved once, and freshly.
|
|
135
|
+
* GitHub calls it makes are attributed to it in github.log.
|
|
136
|
+
*/
|
|
130
137
|
function handleIPCMessage(msg) {
|
|
131
|
-
|
|
138
|
+
const trigger = `GUI · ${String(msg?.type ?? "unknown").toLowerCase().replace(/_/g, " ")}`;
|
|
139
|
+
return runInCodeOperation(() => runTriggeredBy(trigger, () => dispatchIPCMessage(msg)));
|
|
132
140
|
}
|
|
133
141
|
function dispatchIPCMessage(msg) {
|
|
134
142
|
if (msg.type === "ALREADY_RUNNING") {
|
|
@@ -1129,15 +1137,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1129
1137
|
},
|
|
1130
1138
|
{
|
|
1131
1139
|
name: "move_doc",
|
|
1132
|
-
description: "Move a documentation entry to a new path (changes its folder and/or name). All existing doc:// links and related_docs references pointing to the old
|
|
1140
|
+
description: "Move a documentation entry to a new path (changes its folder and/or name) and/or to another project. All existing doc:// links and related_docs references pointing to the old location are automatically rewritten across all projects. When the entry changes project, its own links keep their target: bare ones are qualified with the source project, and those into the target project become bare. Links are always bare within their own project. Its tracked_files are then resolved against the target project's code source. This is the only way to relocate a document — update_doc does not change the location. Provide new_path, new_project, or both.",
|
|
1133
1141
|
inputSchema: {
|
|
1134
1142
|
type: "object",
|
|
1135
1143
|
properties: {
|
|
1136
1144
|
path: { type: "string", description: "Current path of the documentation entry, e.g. 'backend/authentication'" },
|
|
1137
|
-
new_path: { type: "string", description: "New path for the documentation entry, e.g. 'backend/v2/authentication'. Can change folder, name, or both." },
|
|
1138
|
-
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + "
|
|
1145
|
+
new_path: { type: "string", description: "New path for the documentation entry, e.g. 'backend/v2/authentication'. Can change folder, name, or both. Omitted: the entry keeps its current path (new_project is then required)." },
|
|
1146
|
+
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " This is the project the entry is moved from." },
|
|
1147
|
+
new_project: { type: "string", description: "Project the entry is moved to. Must already exist (see list_projects; create it with create_project). Omitted: the entry stays in `project`." },
|
|
1139
1148
|
},
|
|
1140
|
-
required: ["path", "
|
|
1149
|
+
required: ["path", "project"],
|
|
1141
1150
|
},
|
|
1142
1151
|
},
|
|
1143
1152
|
{
|
|
@@ -1172,6 +1181,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1172
1181
|
properties: {},
|
|
1173
1182
|
},
|
|
1174
1183
|
},
|
|
1184
|
+
{
|
|
1185
|
+
name: "create_project",
|
|
1186
|
+
description: "Create an empty documentation project, optionally with its code source (as set_code_source would set it). A GitHub repository and branch are checked first: if they cannot be found, nothing is created.",
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
project: { type: "string", description: "Name of the project to create. Must not exist yet, nor contain '/', '\' or ':'." },
|
|
1191
|
+
code_source: {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
description: "Where the project's code is read from (defaults to the local working copy).",
|
|
1194
|
+
properties: {
|
|
1195
|
+
kind: { type: "string", enum: ["local", "github"], description: "'local' reads the working copy, 'github' reads a repository branch" },
|
|
1196
|
+
repo: { type: "string", description: "Repository when kind is 'github': 'owner/repo' or a GitHub URL" },
|
|
1197
|
+
branch: { type: "string", description: "Branch when kind is 'github' (defaults to the repository's default branch)" },
|
|
1198
|
+
},
|
|
1199
|
+
required: ["kind"],
|
|
1200
|
+
},
|
|
1201
|
+
},
|
|
1202
|
+
required: ["project"],
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
name: "delete_project",
|
|
1207
|
+
description: "Delete an empty documentation project. Refuses — and lists what remains — while the project holds any doc, image or other file besides its settings: move_doc (with new_project) or delete_doc its entries first. Also forgets this machine's code root for the project.",
|
|
1208
|
+
inputSchema: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
properties: {
|
|
1211
|
+
project: { type: "string", description: "Project to delete. Must exist and be empty." },
|
|
1212
|
+
},
|
|
1213
|
+
required: ["project"],
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1175
1216
|
{
|
|
1176
1217
|
name: "refresh_file_snapshot",
|
|
1177
1218
|
description: "Mark tracked files as up-to-date by updating their snapshot timestamps. Use when file changes don't affect the documentation. Accepts one or more doc paths.",
|
|
@@ -1207,7 +1248,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1207
1248
|
},
|
|
1208
1249
|
{
|
|
1209
1250
|
name: "patch_doc",
|
|
1210
|
-
description: "PREFERRED method for modifying documentation. Replaces a specific portion of a documentation entry's content using an exact search-and-replace, without touching the rest of the doc. Always use this instead of update_doc when making partial changes.",
|
|
1251
|
+
description: "PREFERRED method for modifying documentation. Replaces a specific portion of a documentation entry's content using an exact search-and-replace, without touching the rest of the doc. The search text must occur exactly once, unless replace_all is set; the answer gives the number of occurrences replaced. The replacement is inserted verbatim. Always use this instead of update_doc when making partial changes.",
|
|
1211
1252
|
inputSchema: {
|
|
1212
1253
|
type: "object",
|
|
1213
1254
|
properties: {
|
|
@@ -1216,6 +1257,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1216
1257
|
...textSourceProperties("search", "the search text"),
|
|
1217
1258
|
replacement: { type: "string", description: "Text to replace it with. Supports [copy-from-codebase|/absolute/path/to/file.ts|startLine|endLine] — the tag is replaced at save time by a fenced code block, and the file+range is automatically added to tracked_files. IMPORTANT: always read the source file first to identify the exact line numbers before writing the tag." },
|
|
1218
1259
|
...textSourceProperties("replacement", "the replacement"),
|
|
1260
|
+
replace_all: { type: "boolean", description: "Replace every occurrence of the search text. Without it, a search text that occurs more than once is refused." },
|
|
1219
1261
|
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
|
|
1220
1262
|
},
|
|
1221
1263
|
required: ["path", "project"],
|
|
@@ -1223,7 +1265,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1223
1265
|
},
|
|
1224
1266
|
{
|
|
1225
1267
|
name: "git_diff_doc",
|
|
1226
|
-
description: "Show
|
|
1268
|
+
description: "Show, for each changed tracked file of one or more docs, the diff between the content recorded at its last snapshot and its current content at the project's code source. The snapshot content is read from the recorded blob, else from the recorded commit, else from the newest commit at or before the snapshot date; it is checked against the stored hash, and the diff is reported impossible when it does not match.",
|
|
1227
1269
|
inputSchema: {
|
|
1228
1270
|
type: "object",
|
|
1229
1271
|
properties: {
|
|
@@ -1520,8 +1562,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1520
1562
|
};
|
|
1521
1563
|
});
|
|
1522
1564
|
// Each tool call is one operation: the branch head every tracked file is compared against is
|
|
1523
|
-
// resolved once for the whole call, and never carried over to the next one.
|
|
1524
|
-
|
|
1565
|
+
// resolved once for the whole call, and never carried over to the next one. GitHub calls it makes
|
|
1566
|
+
// are attributed to the tool in github.log.
|
|
1567
|
+
server.setRequestHandler(CallToolRequestSchema, (request) => runInCodeOperation(() => runTriggeredBy(request.params.name, () => handleToolCall(request))));
|
|
1525
1568
|
async function handleToolCall(request) {
|
|
1526
1569
|
const { name, arguments: args } = request.params;
|
|
1527
1570
|
if (isReadOnly() && WRITE_TOOLS.has(name)) {
|
|
@@ -1554,12 +1597,35 @@ async function handleToolCall(request) {
|
|
|
1554
1597
|
type: "text",
|
|
1555
1598
|
text: "set_project no longer exists: there is no active project. " +
|
|
1556
1599
|
"Pass the project explicitly to every tool call (use list_projects to see them). " +
|
|
1557
|
-
"
|
|
1600
|
+
"Create a project with create_project, or on demand with create_doc.",
|
|
1558
1601
|
},
|
|
1559
1602
|
],
|
|
1560
1603
|
};
|
|
1561
1604
|
case "list_projects":
|
|
1562
1605
|
return await listProjects(storageEngine);
|
|
1606
|
+
case "create_project": {
|
|
1607
|
+
const result = await createProject(storageEngine, localState, args);
|
|
1608
|
+
if (!result.isError) {
|
|
1609
|
+
broadcastToGUI({
|
|
1610
|
+
type: "PROJECTS_CHANGED",
|
|
1611
|
+
data: { projects: storageEngine.getConfig().projects },
|
|
1612
|
+
timestamp: new Date().toISOString(),
|
|
1613
|
+
});
|
|
1614
|
+
broadcastCodeSourcesChanged(args?.project);
|
|
1615
|
+
}
|
|
1616
|
+
return result;
|
|
1617
|
+
}
|
|
1618
|
+
case "delete_project": {
|
|
1619
|
+
const result = await deleteProject(storageEngine, localState, args);
|
|
1620
|
+
if (!result.isError) {
|
|
1621
|
+
broadcastToGUI({
|
|
1622
|
+
type: "PROJECTS_CHANGED",
|
|
1623
|
+
data: { projects: storageEngine.getConfig().projects },
|
|
1624
|
+
timestamp: new Date().toISOString(),
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
return result;
|
|
1628
|
+
}
|
|
1563
1629
|
case "refresh_file_snapshot":
|
|
1564
1630
|
return await refreshFileSnapshot(storageEngine, historyStore, args);
|
|
1565
1631
|
case "get_doc_metadata":
|
|
@@ -1684,7 +1750,7 @@ async function syncPullOnStartup() {
|
|
|
1684
1750
|
async function main() {
|
|
1685
1751
|
await migrateFolders(storageEngine);
|
|
1686
1752
|
await migrateEmbeddings(embeddingStore, storageEngine);
|
|
1687
|
-
await syncPullOnStartup
|
|
1753
|
+
await runTriggeredBy("startup sync", syncPullOnStartup);
|
|
1688
1754
|
migrateCodeSources(storageEngine, localState);
|
|
1689
1755
|
const transport = new StdioServerTransport();
|
|
1690
1756
|
await server.connect(transport);
|
|
@@ -88,6 +88,15 @@ export declare function getCodeSourceTool(storageEngine: StorageEngine, localSta
|
|
|
88
88
|
text: string;
|
|
89
89
|
}[];
|
|
90
90
|
}>;
|
|
91
|
+
/** A code source as an agent describes it. */
|
|
92
|
+
export interface CodeSourceInput {
|
|
93
|
+
kind?: string;
|
|
94
|
+
repo?: string;
|
|
95
|
+
branch?: string;
|
|
96
|
+
}
|
|
97
|
+
/** Turns an agent's description into a configuration, checking a GitHub repository and branch exist. */
|
|
98
|
+
export declare function resolveCodeSourceInput({ kind, repo, branch }: CodeSourceInput): Promise<CodeSourceConfig>;
|
|
99
|
+
export declare function describeCodeSourceConfig(config: CodeSourceConfig): string;
|
|
91
100
|
export declare function setCodeSourceTool(storageEngine: StorageEngine, localState: LocalState, args: unknown): Promise<{
|
|
92
101
|
content: {
|
|
93
102
|
type: string;
|
|
@@ -221,29 +221,31 @@ export async function getCodeSourceTool(storageEngine, localState, args) {
|
|
|
221
221
|
`The code source is shared by every machine (synced with the docs); code roots belong to this machine.`;
|
|
222
222
|
return { content: [{ type: "text", text }] };
|
|
223
223
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
let config;
|
|
230
|
-
if (kind === "local") {
|
|
231
|
-
config = { kind: "local" };
|
|
232
|
-
}
|
|
233
|
-
else if (kind === "github") {
|
|
224
|
+
/** Turns an agent's description into a configuration, checking a GitHub repository and branch exist. */
|
|
225
|
+
export async function resolveCodeSourceInput({ kind, repo, branch }) {
|
|
226
|
+
if (kind === "local")
|
|
227
|
+
return { kind: "local" };
|
|
228
|
+
if (kind === "github") {
|
|
234
229
|
if (!repo)
|
|
235
230
|
throw new Error('repo is required when kind is "github"');
|
|
236
231
|
const inspection = await inspectRepository({ repo, branch });
|
|
237
232
|
if (!inspection.ok)
|
|
238
233
|
throw new Error(inspection.error);
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
throw new Error('kind must be "local" or "github"');
|
|
234
|
+
return { kind: "github", repo: inspection.repo.fullName, ...(branch && { branch: inspection.branch }) };
|
|
243
235
|
}
|
|
236
|
+
throw new Error('kind must be "local" or "github"');
|
|
237
|
+
}
|
|
238
|
+
export function describeCodeSourceConfig(config) {
|
|
239
|
+
return config.kind === "github" ? `GitHub ${config.repo} · ${config.branch ?? "default branch"}` : "its local working copy";
|
|
240
|
+
}
|
|
241
|
+
export async function setCodeSourceTool(storageEngine, localState, args) {
|
|
242
|
+
const { project, ...input } = (args ?? {});
|
|
243
|
+
if (!project)
|
|
244
|
+
throw new Error("project is required");
|
|
245
|
+
assertKnownProject(storageEngine, project);
|
|
246
|
+
const config = await resolveCodeSourceInput(input);
|
|
244
247
|
saveCodeSource(storageEngine, localState, project, config);
|
|
245
|
-
|
|
246
|
-
return { content: [{ type: "text", text: `Project "${project}" now reads its code from ${where}.` }] };
|
|
248
|
+
return { content: [{ type: "text", text: `Project "${project}" now reads its code from ${describeCodeSourceConfig(config)}.` }] };
|
|
247
249
|
}
|
|
248
250
|
export function setCodeRootTool(storageEngine, localState, args) {
|
|
249
251
|
const { path: root, project } = (args ?? {});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
import type { LocalState } from "../core/local-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates an empty project, with its code source when one is given. The source is checked before
|
|
5
|
+
* anything is written, so an unknown repository or branch creates nothing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createProject(storageEngine: StorageEngine, localState: LocalState, args: unknown): Promise<{
|
|
8
|
+
isError: boolean;
|
|
9
|
+
content: {
|
|
10
|
+
type: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
} | {
|
|
14
|
+
content: {
|
|
15
|
+
type: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}[];
|
|
18
|
+
isError?: undefined;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { describeCodeSourceConfig, resolveCodeSourceInput, saveCodeSource } from "./code-sources.js";
|
|
3
|
+
const CreateProjectArgsSchema = z.object({
|
|
4
|
+
project: z
|
|
5
|
+
.string()
|
|
6
|
+
.trim()
|
|
7
|
+
.min(1)
|
|
8
|
+
.refine((name) => name !== "." && name !== ".." && !/[\\/:]/.test(name), {
|
|
9
|
+
message: 'Project names cannot be "." or "..", nor contain "/", "\\" or ":".',
|
|
10
|
+
})
|
|
11
|
+
.describe("Name of the project to create."),
|
|
12
|
+
code_source: z
|
|
13
|
+
.object({
|
|
14
|
+
kind: z.enum(["local", "github"]),
|
|
15
|
+
repo: z.string().optional(),
|
|
16
|
+
branch: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Where the project's code is read from, as with set_code_source. Omitted: the local working copy."),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Creates an empty project, with its code source when one is given. The source is checked before
|
|
23
|
+
* anything is written, so an unknown repository or branch creates nothing.
|
|
24
|
+
*/
|
|
25
|
+
export async function createProject(storageEngine, localState, args) {
|
|
26
|
+
try {
|
|
27
|
+
const { project, code_source } = CreateProjectArgsSchema.parse(args);
|
|
28
|
+
if (storageEngine.getConfig().projects.includes(project)) {
|
|
29
|
+
return { isError: true, content: [{ type: "text", text: `Project "${project}" already exists.` }] };
|
|
30
|
+
}
|
|
31
|
+
const config = code_source ? await resolveCodeSourceInput(code_source) : undefined;
|
|
32
|
+
storageEngine.ensureProject(project);
|
|
33
|
+
if (config)
|
|
34
|
+
saveCodeSource(storageEngine, localState, project, config);
|
|
35
|
+
const source = config ? ` It reads its code from ${describeCodeSourceConfig(config)}.` : "";
|
|
36
|
+
return { content: [{ type: "text", text: `Project "${project}" created.${source}` }] };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return { isError: true, content: [{ type: "text", text: `Error creating project: ${message}` }] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StorageEngine } from "../core/storage-engine.js";
|
|
2
|
+
import type { LocalState } from "../core/local-state.js";
|
|
3
|
+
/** Deletes an empty project, along with this machine's code root for it. */
|
|
4
|
+
export declare function deleteProject(storageEngine: StorageEngine, localState: LocalState, args: unknown): Promise<{
|
|
5
|
+
content: {
|
|
6
|
+
type: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
isError?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
isError: boolean;
|
|
12
|
+
content: {
|
|
13
|
+
type: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { invalidateCodeSources } from "../core/code-source.js";
|
|
3
|
+
import { assertProject, scopeErrorResult } from "../core/project-scope.js";
|
|
4
|
+
const DeleteProjectArgsSchema = z.object({
|
|
5
|
+
project: z.string().min(1).describe("Project to delete. It must be empty: no doc, no image, no other file."),
|
|
6
|
+
});
|
|
7
|
+
/** Deletes an empty project, along with this machine's code root for it. */
|
|
8
|
+
export async function deleteProject(storageEngine, localState, args) {
|
|
9
|
+
try {
|
|
10
|
+
const { project } = DeleteProjectArgsSchema.parse(args);
|
|
11
|
+
const projectError = assertProject(storageEngine, project);
|
|
12
|
+
if (projectError)
|
|
13
|
+
return scopeErrorResult(projectError);
|
|
14
|
+
storageEngine.deleteProject(project);
|
|
15
|
+
localState.setProjectCodeRoot(project, undefined);
|
|
16
|
+
invalidateCodeSources(project);
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: `Project "${project}" deleted.` }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23
|
+
return { isError: true, content: [{ type: "text", text: `Error deleting project: ${message}` }] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,117 +1,144 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { createTwoFilesPatch } from "diff";
|
|
3
4
|
import { GitDiffDocArgsSchema } from "../../shared/schemas/doc.schema.js";
|
|
4
5
|
import { computeFreshness } from "./check-freshness.js";
|
|
5
6
|
import { toAbsolutePath } from "../core/path-resolver.js";
|
|
6
|
-
import { getCodeSource,
|
|
7
|
+
import { getCodeSource, getGitHubSource, toStoredPathForMatching } from "../core/code-source.js";
|
|
8
|
+
import { bytesToFileContent, fileContentMatches, findAndHashSection } from "../core/section-tracker.js";
|
|
9
|
+
import { mapWithConcurrency } from "../core/concurrency.js";
|
|
7
10
|
import { assertProject, scopeErrorResult } from "../core/project-scope.js";
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** Tracked files of one doc diffed at once. */
|
|
12
|
+
const FILE_CONCURRENCY = 4;
|
|
13
|
+
const errorMessage = (err) => (err instanceof Error ? err.message : String(err));
|
|
14
|
+
const short = (sha) => sha.slice(0, 7);
|
|
15
|
+
/**
|
|
16
|
+
* Snapshot content on GitHub, by decreasing reliability: the recorded blob (independent of any
|
|
17
|
+
* commit, so it survives a rebase), the file at the recorded commit, or the file at the newest
|
|
18
|
+
* commit on the branch at or before the snapshot date.
|
|
19
|
+
*/
|
|
20
|
+
async function githubSnapshotBase(source, tracked) {
|
|
21
|
+
const repo = source.api.label;
|
|
22
|
+
if (tracked.sourceBlobId) {
|
|
23
|
+
try {
|
|
24
|
+
return { content: await source.readBlob(tracked.sourceBlobId, tracked.path), origin: `snapshot blob ${short(tracked.sourceBlobId)}`, inferred: false };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { error: `The snapshot content (blob ${short(tracked.sourceBlobId)}) cannot be read on ${repo}: ${errorMessage(err)}` };
|
|
28
|
+
}
|
|
13
29
|
}
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
let commit = tracked.sourceRevision;
|
|
31
|
+
const inferred = !commit;
|
|
32
|
+
if (!commit) {
|
|
33
|
+
try {
|
|
34
|
+
commit = await source.api.resolveCommitAtDate(tracked.lastModifiedAt);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return { error: `No source commit recorded, and the commit at the snapshot date (${tracked.lastModifiedAt}) could not be resolved on ${repo}: ${errorMessage(err)}` };
|
|
38
|
+
}
|
|
39
|
+
if (!commit) {
|
|
40
|
+
return { error: `No source commit recorded, and ${repo} has no commit at or before the snapshot date (${tracked.lastModifiedAt}).` };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const origin = inferred ? `commit ${short(commit)} inferred from the snapshot date` : `snapshot commit ${short(commit)}`;
|
|
16
44
|
try {
|
|
17
|
-
|
|
45
|
+
const content = await source.readAt(tracked.path, commit);
|
|
46
|
+
if (!content)
|
|
47
|
+
return { error: `The file does not exist at ${origin} on ${repo}.` };
|
|
48
|
+
return { content, origin, inferred };
|
|
18
49
|
}
|
|
19
|
-
catch {
|
|
20
|
-
return `
|
|
50
|
+
catch (err) {
|
|
51
|
+
return { error: `The ${origin} cannot be read on ${repo}: ${errorMessage(err)}` };
|
|
21
52
|
}
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
}
|
|
54
|
+
function git(repoRoot, args) {
|
|
55
|
+
return execFileSync("git", ["-C", repoRoot, ...args], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
|
|
56
|
+
}
|
|
57
|
+
/** Snapshot content in the local clone, with the same order of preference as on GitHub. */
|
|
58
|
+
function localSnapshotBase(tracked, project) {
|
|
59
|
+
const filePath = toAbsolutePath(tracked.path, project);
|
|
60
|
+
if (!filePath)
|
|
61
|
+
return { error: `Cannot resolve "${tracked.path}" on this machine (no code root configured).` };
|
|
62
|
+
let repoRoot;
|
|
24
63
|
try {
|
|
25
|
-
|
|
64
|
+
repoRoot = git(path.dirname(filePath), ["rev-parse", "--show-toplevel"]).toString("utf-8").trim();
|
|
26
65
|
}
|
|
27
66
|
catch {
|
|
28
|
-
return
|
|
67
|
+
return { error: `Not a git repository: ${filePath}` };
|
|
29
68
|
}
|
|
30
|
-
|
|
31
|
-
|
|
69
|
+
const relativeFilePath = path.relative(repoRoot, filePath).replace(/\\/g, "/");
|
|
70
|
+
if (tracked.sourceBlobId) {
|
|
71
|
+
try {
|
|
72
|
+
return { content: bytesToFileContent(git(repoRoot, ["cat-file", "blob", tracked.sourceBlobId])), origin: `snapshot blob ${short(tracked.sourceBlobId)}`, inferred: false };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { error: `The snapshot content (blob ${short(tracked.sourceBlobId)}) is not in the local repository ${repoRoot}.` };
|
|
76
|
+
}
|
|
32
77
|
}
|
|
33
|
-
let
|
|
78
|
+
let commit = tracked.sourceRevision;
|
|
79
|
+
const inferred = !commit;
|
|
80
|
+
if (!commit) {
|
|
81
|
+
try {
|
|
82
|
+
commit = git(repoRoot, ["log", `--before=${tracked.lastModifiedAt}`, "-1", "--format=%H", "--", relativeFilePath]).toString("utf-8").trim();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return { error: `Could not read the git log of ${filePath}.` };
|
|
86
|
+
}
|
|
87
|
+
if (!commit)
|
|
88
|
+
return { error: `No commit touches ${relativeFilePath} at or before the snapshot date (${tracked.lastModifiedAt}).` };
|
|
89
|
+
}
|
|
90
|
+
const origin = inferred ? `commit ${short(commit)} inferred from the snapshot date` : `snapshot commit ${short(commit)}`;
|
|
34
91
|
try {
|
|
35
|
-
|
|
92
|
+
return { content: bytesToFileContent(git(repoRoot, ["show", `${commit}:${relativeFilePath}`])), origin, inferred };
|
|
36
93
|
}
|
|
37
94
|
catch {
|
|
38
|
-
return `
|
|
39
|
-
}
|
|
40
|
-
if (!diff) {
|
|
41
|
-
return ` [INFO] No changes since snapshot (commit ${commitHash.slice(0, 7)}) for: ${filePath}`;
|
|
95
|
+
return { error: `The file does not exist at ${origin} in ${repoRoot}, or that commit is not in the local repository.` };
|
|
42
96
|
}
|
|
43
|
-
return diff;
|
|
44
97
|
}
|
|
45
98
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* A snapshot with no recorded commit — taken before branch tracking, or from a local working copy —
|
|
50
|
-
* is anchored on the newest commit at or before its date instead, so it diffs like any other rather
|
|
51
|
-
* than demanding a refresh first.
|
|
52
|
-
*
|
|
53
|
-
* Files are grouped by that base commit so one comparison covers every file snapshotted at the
|
|
54
|
-
* same revision — which, for a doc refreshed in one go, means a single API call.
|
|
99
|
+
* Whether a content is the one the snapshot recorded, judged by its stored hashes. Undefined when
|
|
100
|
+
* the entry holds no hash to judge by.
|
|
55
101
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
for (const [base, files] of byBase) {
|
|
88
|
-
// An inferred base is the branch state at the snapshot date, not a commit the snapshot was
|
|
89
|
-
// provably read from — worth saying, since the diff can then include changes already documented.
|
|
90
|
-
const note = inferredBases.has(base)
|
|
91
|
-
? ` [INFO] Base commit ${base.slice(0, 7)} inferred from the snapshot date (no source commit recorded).\n\n`
|
|
92
|
-
: "";
|
|
93
|
-
if (base === head) {
|
|
94
|
-
for (const file of files) {
|
|
95
|
-
diffs.set(file.path, `${note} [INFO] ${api.label} is still at the snapshot commit ${base.slice(0, 7)} — no changes.`);
|
|
96
|
-
}
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
let patches;
|
|
100
|
-
try {
|
|
101
|
-
patches = await api.comparePatches(base, head);
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
-
for (const file of files)
|
|
106
|
-
diffs.set(file.path, `${note} [ERROR] Could not compare ${base.slice(0, 7)}...${head.slice(0, 7)}: ${message}`);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
for (const file of files) {
|
|
110
|
-
const patch = patches.get(file.path);
|
|
111
|
-
diffs.set(file.path, note + (patch ?? ` [INFO] No changes to this file between ${base.slice(0, 7)} and ${head.slice(0, 7)}.`));
|
|
112
|
-
}
|
|
102
|
+
function matchesSnapshot(content, tracked) {
|
|
103
|
+
if (tracked.sections && tracked.sections.length > 0) {
|
|
104
|
+
return tracked.sections.every((section) => {
|
|
105
|
+
const result = findAndHashSection(content, section);
|
|
106
|
+
return result.found && result.matchesStored;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (tracked.contentHash)
|
|
110
|
+
return fileContentMatches(content, tracked.contentHash);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/** The diff section of one tracked file: snapshot content against the current one at the source. */
|
|
114
|
+
async function diffTrackedFile(tracked, project, source) {
|
|
115
|
+
const github = getGitHubSource(project);
|
|
116
|
+
const base = github ? await githubSnapshotBase(github, tracked) : localSnapshotBase(tracked, project);
|
|
117
|
+
const impossible = (reason) => ` [ERROR] Diff impossible: ${reason}\n` +
|
|
118
|
+
` → Re-read the file to check the doc against the current code, then call refresh_file_snapshot.`;
|
|
119
|
+
if ("error" in base)
|
|
120
|
+
return impossible(base.error);
|
|
121
|
+
const verdict = matchesSnapshot(base.content, tracked);
|
|
122
|
+
if (verdict === false) {
|
|
123
|
+
return impossible(`the content at ${base.origin} does not match the hash recorded at the snapshot.`);
|
|
124
|
+
}
|
|
125
|
+
const unverifiedNote = verdict === undefined ? ` [INFO] No hash recorded for this entry: ${base.origin} could not be checked against the snapshot.\n\n` : "";
|
|
126
|
+
const inferredNote = base.inferred && verdict ? ` [INFO] Base: ${base.origin} (no source commit recorded), checked against the snapshot hash.\n\n` : "";
|
|
127
|
+
let current;
|
|
128
|
+
try {
|
|
129
|
+
current = await source.read(tracked.path);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return `${unverifiedNote}${inferredNote} [ERROR] The current content cannot be read from ${source.label}: ${errorMessage(err)}`;
|
|
113
133
|
}
|
|
114
|
-
|
|
134
|
+
if (!current)
|
|
135
|
+
return `${unverifiedNote}${inferredNote} [INFO] The file no longer exists at ${source.label}.`;
|
|
136
|
+
const before = base.content.lines.join("\n");
|
|
137
|
+
const after = current.content.lines.join("\n");
|
|
138
|
+
if (before === after)
|
|
139
|
+
return `${unverifiedNote}${inferredNote} [INFO] No changes since the snapshot.`;
|
|
140
|
+
const patch = createTwoFilesPatch(`a/${tracked.path}`, `b/${tracked.path}`, before, after, base.origin, "current", { context: 3 });
|
|
141
|
+
return unverifiedNote + inferredNote + patch.replace(/^=+\n/, "").trimEnd();
|
|
115
142
|
}
|
|
116
143
|
export async function gitDiffDoc(storageEngine, args) {
|
|
117
144
|
try {
|
|
@@ -121,7 +148,6 @@ export async function gitDiffDoc(storageEngine, args) {
|
|
|
121
148
|
if (projectError)
|
|
122
149
|
return scopeErrorResult(projectError);
|
|
123
150
|
const source = getCodeSource(project);
|
|
124
|
-
const githubApi = getGitHubApi(project);
|
|
125
151
|
const wanted = validated.file_paths
|
|
126
152
|
? new Set(await Promise.all(validated.file_paths.map((p) => toStoredPathForMatching(project, p))))
|
|
127
153
|
: undefined;
|
|
@@ -165,11 +191,10 @@ export async function gitDiffDoc(storageEngine, args) {
|
|
|
165
191
|
docSections.push(`## ${doc.title} [${docPath}]\n\n[INFO] All tracked files already shown in a previous documentation section.`);
|
|
166
192
|
continue;
|
|
167
193
|
}
|
|
168
|
-
const diffs =
|
|
169
|
-
const fileSections = fresh.map((tf) => {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}${base}\n\n${diff}`;
|
|
194
|
+
const diffs = await mapWithConcurrency(fresh, FILE_CONCURRENCY, (tf) => diffTrackedFile(tf, project, source));
|
|
195
|
+
const fileSections = fresh.map((tf, i) => {
|
|
196
|
+
const base = tf.sourceRevision ? `\nSnapshot commit: ${short(tf.sourceRevision)}` : "";
|
|
197
|
+
return `### ${tf.path}\nSnapshot date: ${tf.lastModifiedAt}${base}\n\n${diffs[i]}`;
|
|
173
198
|
});
|
|
174
199
|
docSections.push(`## ${doc.title} [${docPath}]\n\nCode source: ${source.label}\n\n${fileSections.join("\n\n---\n\n")}`);
|
|
175
200
|
}
|