@imenam/mcp-documentor 1.0.87 → 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 +1 -0
- package/dist/shared/schemas/doc.schema.js +1 -0
- package/dist/src/core/code-source.d.ts +32 -0
- package/dist/src/core/code-source.js +23 -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 +21 -6
- package/dist/src/core/storage-engine.js +80 -24
- package/dist/src/gui-worker.js +58 -1
- package/dist/src/index.js +75 -10
- 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/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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { GITHUB_LOG_FILE } from "./github-log.js";
|
|
4
|
+
export const SERVER_LOG_FILE = "server.log";
|
|
5
|
+
export const LOG_SOURCES = {
|
|
6
|
+
server: SERVER_LOG_FILE,
|
|
7
|
+
github: GITHUB_LOG_FILE,
|
|
8
|
+
};
|
|
9
|
+
export function isLogSource(id) {
|
|
10
|
+
return Object.prototype.hasOwnProperty.call(LOG_SOURCES, id);
|
|
11
|
+
}
|
|
12
|
+
/** Bytes read from the end of each file: the most recent part, which is what the viewer shows. */
|
|
13
|
+
const TAIL_BYTES = {
|
|
14
|
+
server: 4 * 1024 * 1024,
|
|
15
|
+
github: 8 * 1024 * 1024,
|
|
16
|
+
};
|
|
17
|
+
export function logFileInfo(logDir, id) {
|
|
18
|
+
const filePath = path.join(logDir, LOG_SOURCES[id]);
|
|
19
|
+
const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
file: LOG_SOURCES[id],
|
|
23
|
+
path: filePath,
|
|
24
|
+
exists: !!stat,
|
|
25
|
+
size: stat?.size ?? 0,
|
|
26
|
+
mtime: stat ? stat.mtime.toISOString() : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The last `maxBytes` of a file, starting on a whole line. */
|
|
30
|
+
function readTail(filePath, size, maxBytes) {
|
|
31
|
+
const start = Math.max(0, size - maxBytes);
|
|
32
|
+
const length = size - start;
|
|
33
|
+
const buffer = Buffer.alloc(length);
|
|
34
|
+
const fd = fs.openSync(filePath, "r");
|
|
35
|
+
try {
|
|
36
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fs.closeSync(fd);
|
|
40
|
+
}
|
|
41
|
+
let text = buffer.toString("utf8");
|
|
42
|
+
if (start > 0)
|
|
43
|
+
text = text.slice(text.indexOf("\n") + 1);
|
|
44
|
+
return { text, partial: start > 0 };
|
|
45
|
+
}
|
|
46
|
+
const LINE_PREFIX = /^\[(\d{4}-\d{2}-\d{2}T[^\]]+)\] \[PID (\d+)\] \[([^\]]+)\] (.*)$/;
|
|
47
|
+
const LEVEL_AND_SCOPE = /^\[(DEBUG|INFO|WARN|ERROR)\] \[([^\]]+)\] ?(.*)$/;
|
|
48
|
+
/**
|
|
49
|
+
* Parses server.log lines as written by the shared logger:
|
|
50
|
+
* [ISO date] [PID n] [process] [LEVEL] [scope] message
|
|
51
|
+
* Text printed straight to the console carries no level nor scope: it is read as INFO from
|
|
52
|
+
* "console". A line without the date prefix continues the entry above it (stack traces).
|
|
53
|
+
*/
|
|
54
|
+
export function readServerLog(logDir) {
|
|
55
|
+
const file = logFileInfo(logDir, "server");
|
|
56
|
+
if (!file.exists)
|
|
57
|
+
return { file, partial: false, entries: [], unreadable: 0 };
|
|
58
|
+
const { text, partial } = readTail(file.path, file.size, TAIL_BYTES.server);
|
|
59
|
+
const entries = [];
|
|
60
|
+
let unreadable = 0;
|
|
61
|
+
for (const line of text.split(/\r?\n/)) {
|
|
62
|
+
if (!line)
|
|
63
|
+
continue;
|
|
64
|
+
const prefix = line.match(LINE_PREFIX);
|
|
65
|
+
if (!prefix) {
|
|
66
|
+
const previous = entries[entries.length - 1];
|
|
67
|
+
if (previous)
|
|
68
|
+
previous.continuation.push(line);
|
|
69
|
+
else
|
|
70
|
+
unreadable++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const [, ts, pid, processLabel, rest] = prefix;
|
|
74
|
+
const tagged = rest.match(LEVEL_AND_SCOPE);
|
|
75
|
+
entries.push({
|
|
76
|
+
ts,
|
|
77
|
+
pid: Number(pid),
|
|
78
|
+
process: processLabel,
|
|
79
|
+
level: tagged ? tagged[1] : "INFO",
|
|
80
|
+
scope: tagged ? tagged[2] : "console",
|
|
81
|
+
message: tagged ? tagged[3] : rest,
|
|
82
|
+
continuation: [],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { file, partial, entries, unreadable };
|
|
86
|
+
}
|
|
87
|
+
/** Parses github.log: one JSON object per line. */
|
|
88
|
+
export function readGitHubLog(logDir) {
|
|
89
|
+
const file = logFileInfo(logDir, "github");
|
|
90
|
+
if (!file.exists)
|
|
91
|
+
return { file, partial: false, entries: [], unreadable: 0 };
|
|
92
|
+
const { text, partial } = readTail(file.path, file.size, TAIL_BYTES.github);
|
|
93
|
+
const entries = [];
|
|
94
|
+
let unreadable = 0;
|
|
95
|
+
for (const line of text.split(/\r?\n/)) {
|
|
96
|
+
if (!line.trim())
|
|
97
|
+
continue;
|
|
98
|
+
try {
|
|
99
|
+
entries.push({ ...JSON.parse(line), raw: line });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
unreadable++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { file, partial, entries, unreadable };
|
|
106
|
+
}
|
|
@@ -42,6 +42,12 @@ export declare class StorageEngine {
|
|
|
42
42
|
/** Reads sync settings from environment variables (admin-controlled, not agent-settable). */
|
|
43
43
|
getSyncConfig(): SyncConfig | undefined;
|
|
44
44
|
ensureProject(project: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Removes a project that holds nothing but its settings file. Any other file —
|
|
47
|
+
* doc, asset or otherwise — makes it refuse, listing what it found.
|
|
48
|
+
*/
|
|
49
|
+
deleteProject(project: string): void;
|
|
50
|
+
private walkFiles;
|
|
45
51
|
private walkMdFiles;
|
|
46
52
|
private docIdToFilePath;
|
|
47
53
|
private cleanEmptyDirs;
|
|
@@ -58,18 +64,27 @@ export declare class StorageEngine {
|
|
|
58
64
|
*/
|
|
59
65
|
listDocsDetailed(project: string): Promise<DocListing[]>;
|
|
60
66
|
deleteDoc(id: string, project: string): Promise<boolean>;
|
|
67
|
+
/** Known project names, longest first, so a name is never matched as the prefix of a longer one. */
|
|
68
|
+
private projectsLongestFirst;
|
|
69
|
+
/**
|
|
70
|
+
* Matches every `doc://` URL: group 1 is the project of a qualified reference (project names
|
|
71
|
+
* may contain spaces), undefined for a bare one; group 2 is the doc id.
|
|
72
|
+
*/
|
|
73
|
+
private docUrlRegex;
|
|
74
|
+
/** Splits a `related_docs` entry into its project, when qualified with a known one, and its id. */
|
|
75
|
+
private parseDocRef;
|
|
61
76
|
/**
|
|
62
|
-
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
77
|
+
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own references are
|
|
78
|
+
* rewritten so they keep their target: bare ones are qualified with the source project, and any
|
|
79
|
+
* reference into the target project becomes bare. The history records a delete in the source
|
|
80
|
+
* project and a create in the target.
|
|
66
81
|
*/
|
|
67
82
|
moveDoc(oldId: string, project: string, newId: string, newProject: string): Promise<boolean>;
|
|
68
83
|
getBundles(): ExportBundle[];
|
|
69
84
|
saveBundles(bundles: ExportBundle[]): void;
|
|
70
85
|
/**
|
|
71
|
-
* Rewrites every reference to `project:oldId` so it points at `newProject:newId
|
|
72
|
-
*
|
|
86
|
+
* Rewrites every reference to `project:oldId` so it points at `newProject:newId`: bare inside
|
|
87
|
+
* `newProject`, qualified everywhere else.
|
|
73
88
|
*/
|
|
74
89
|
rewriteLinks(oldId: string, project: string, newId: string, newProject: string): Promise<string[]>;
|
|
75
90
|
}
|
|
@@ -91,6 +91,39 @@ export class StorageEngine {
|
|
|
91
91
|
}
|
|
92
92
|
logger.info(`Project "${project}" ensured at ${projectDir}`);
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Removes a project that holds nothing but its settings file. Any other file —
|
|
96
|
+
* doc, asset or otherwise — makes it refuse, listing what it found.
|
|
97
|
+
*/
|
|
98
|
+
deleteProject(project) {
|
|
99
|
+
const projectDir = this.getProjectDir(project);
|
|
100
|
+
const contents = this.walkFiles(projectDir)
|
|
101
|
+
.map((file) => path.relative(projectDir, file).split(path.sep).join("/"))
|
|
102
|
+
.filter((rel) => rel !== "project.json");
|
|
103
|
+
if (contents.length > 0) {
|
|
104
|
+
const shown = contents.slice(0, 20).map((rel) => ` - ${rel}`).join("\n");
|
|
105
|
+
const more = contents.length > 20 ? `\n … and ${contents.length - 20} more` : "";
|
|
106
|
+
throw new Error(`Project "${project}" is not empty (${contents.length} file(s)) — nothing deleted. Move or delete its content first:\n${shown}${more}`);
|
|
107
|
+
}
|
|
108
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
109
|
+
const config = this.getConfig();
|
|
110
|
+
config.projects = config.projects.filter((p) => p !== project);
|
|
111
|
+
this.saveConfig(config);
|
|
112
|
+
logger.info(`Project "${project}" deleted`);
|
|
113
|
+
}
|
|
114
|
+
walkFiles(dir) {
|
|
115
|
+
if (!fs.existsSync(dir))
|
|
116
|
+
return [];
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
const fullPath = path.join(dir, entry.name);
|
|
120
|
+
if (entry.isDirectory())
|
|
121
|
+
results.push(...this.walkFiles(fullPath));
|
|
122
|
+
else
|
|
123
|
+
results.push(fullPath);
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
94
127
|
walkMdFiles(dir) {
|
|
95
128
|
if (!fs.existsSync(dir))
|
|
96
129
|
return [];
|
|
@@ -227,11 +260,29 @@ export class StorageEngine {
|
|
|
227
260
|
logger.info(`Doc deleted: ${filePath}`);
|
|
228
261
|
return true;
|
|
229
262
|
}
|
|
263
|
+
/** Known project names, longest first, so a name is never matched as the prefix of a longer one. */
|
|
264
|
+
projectsLongestFirst() {
|
|
265
|
+
return [...this.getConfig().projects].sort((a, b) => b.length - a.length);
|
|
266
|
+
}
|
|
230
267
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
|
|
234
|
-
|
|
268
|
+
* Matches every `doc://` URL: group 1 is the project of a qualified reference (project names
|
|
269
|
+
* may contain spaces), undefined for a bare one; group 2 is the doc id.
|
|
270
|
+
*/
|
|
271
|
+
docUrlRegex() {
|
|
272
|
+
const projects = this.projectsLongestFirst().map(escapeRegex).join("|");
|
|
273
|
+
const qualifier = projects ? `(?:(${projects}):)?` : "()?";
|
|
274
|
+
return new RegExp(`doc://${qualifier}([^):#\\s]+)(?=[)#\\s]|$)`, "g");
|
|
275
|
+
}
|
|
276
|
+
/** Splits a `related_docs` entry into its project, when qualified with a known one, and its id. */
|
|
277
|
+
parseDocRef(ref) {
|
|
278
|
+
const project = this.projectsLongestFirst().find((p) => ref.startsWith(`${p}:`));
|
|
279
|
+
return project ? { project, id: ref.slice(project.length + 1) } : { id: ref };
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own references are
|
|
283
|
+
* rewritten so they keep their target: bare ones are qualified with the source project, and any
|
|
284
|
+
* reference into the target project becomes bare. The history records a delete in the source
|
|
285
|
+
* project and a create in the target.
|
|
235
286
|
*/
|
|
236
287
|
async moveDoc(oldId, project, newId, newProject) {
|
|
237
288
|
const projectDir = this.getProjectDir(project);
|
|
@@ -249,8 +300,16 @@ export class StorageEngine {
|
|
|
249
300
|
const crossProject = newProject !== project;
|
|
250
301
|
if (crossProject) {
|
|
251
302
|
this.historyStore?.record(project, doc, "delete");
|
|
252
|
-
|
|
253
|
-
|
|
303
|
+
const relocate = (ref) => {
|
|
304
|
+
const target = ref.project ?? project;
|
|
305
|
+
return target === newProject ? ref.id : `${target}:${ref.id}`;
|
|
306
|
+
};
|
|
307
|
+
doc.content = doc.content.replace(this.docUrlRegex(), (_match, refProject, id) => `doc://${relocate({ project: refProject, id })}`);
|
|
308
|
+
doc.related_docs = doc.related_docs.map((ref) => {
|
|
309
|
+
const parsed = this.parseDocRef(ref);
|
|
310
|
+
// A qualifier naming no known project is left as is: it cannot be resolved either way.
|
|
311
|
+
return !parsed.project && ref.includes(":") ? ref : relocate(parsed);
|
|
312
|
+
});
|
|
254
313
|
}
|
|
255
314
|
const newParts = newId.split("/");
|
|
256
315
|
doc.id = newId;
|
|
@@ -279,16 +338,16 @@ export class StorageEngine {
|
|
|
279
338
|
fs.writeFileSync(p, JSON.stringify(bundles, null, 2), "utf-8");
|
|
280
339
|
}
|
|
281
340
|
/**
|
|
282
|
-
* Rewrites every reference to `project:oldId` so it points at `newProject:newId
|
|
283
|
-
*
|
|
341
|
+
* Rewrites every reference to `project:oldId` so it points at `newProject:newId`: bare inside
|
|
342
|
+
* `newProject`, qualified everywhere else.
|
|
284
343
|
*/
|
|
285
344
|
async rewriteLinks(oldId, project, newId, newProject) {
|
|
286
345
|
const config = this.getConfig();
|
|
287
346
|
const proj = project;
|
|
288
347
|
const crossProject = newProject !== proj;
|
|
289
|
-
const
|
|
290
|
-
const bareTarget = crossProject ? qualifiedTarget : newId;
|
|
348
|
+
const targetFrom = (docProject) => (docProject === newProject ? newId : `${newProject}:${newId}`);
|
|
291
349
|
const boundary = "(?=[)#\\s]|$)";
|
|
350
|
+
const qualifiedOld = new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}${boundary}`, "g");
|
|
292
351
|
const updated = [];
|
|
293
352
|
// Within the source project: rewrite bare refs and explicit self-cross-project refs
|
|
294
353
|
const sameProjDocs = await this.listDocs(proj);
|
|
@@ -298,25 +357,22 @@ export class StorageEngine {
|
|
|
298
357
|
const doc = await this.getDoc(summary.id, proj);
|
|
299
358
|
if (!doc)
|
|
300
359
|
continue;
|
|
360
|
+
const target = targetFrom(proj);
|
|
301
361
|
let changed = false;
|
|
302
|
-
|
|
303
|
-
|
|
362
|
+
const newContent = doc.content
|
|
363
|
+
.replace(new RegExp(`doc://${escapeRegex(oldId)}${boundary}`, "g"), `doc://${target}`)
|
|
364
|
+
.replace(qualifiedOld, `doc://${target}`);
|
|
304
365
|
if (newContent !== doc.content) {
|
|
305
366
|
doc.content = newContent;
|
|
306
367
|
changed = true;
|
|
307
368
|
}
|
|
308
|
-
|
|
309
|
-
if (ref === oldId) {
|
|
310
|
-
changed = true;
|
|
311
|
-
return bareTarget;
|
|
312
|
-
}
|
|
313
|
-
if (ref === `${proj}:${oldId}`) {
|
|
369
|
+
doc.related_docs = doc.related_docs.map((ref) => {
|
|
370
|
+
if (ref === oldId || ref === `${proj}:${oldId}`) {
|
|
314
371
|
changed = true;
|
|
315
|
-
return
|
|
372
|
+
return target;
|
|
316
373
|
}
|
|
317
374
|
return ref;
|
|
318
375
|
});
|
|
319
|
-
doc.related_docs = newRelated;
|
|
320
376
|
if (changed) {
|
|
321
377
|
doc.updatedAt = new Date().toISOString();
|
|
322
378
|
await this.saveDoc(doc, proj);
|
|
@@ -328,24 +384,24 @@ export class StorageEngine {
|
|
|
328
384
|
if (otherProj === proj)
|
|
329
385
|
continue;
|
|
330
386
|
const otherDocs = await this.listDocs(otherProj);
|
|
387
|
+
const target = targetFrom(otherProj);
|
|
331
388
|
for (const summary of otherDocs) {
|
|
332
389
|
const doc = await this.getDoc(summary.id, otherProj);
|
|
333
390
|
if (!doc)
|
|
334
391
|
continue;
|
|
335
392
|
let changed = false;
|
|
336
|
-
const newContent = doc.content.replace(
|
|
393
|
+
const newContent = doc.content.replace(qualifiedOld, `doc://${target}`);
|
|
337
394
|
if (newContent !== doc.content) {
|
|
338
395
|
doc.content = newContent;
|
|
339
396
|
changed = true;
|
|
340
397
|
}
|
|
341
|
-
|
|
398
|
+
doc.related_docs = doc.related_docs.map((ref) => {
|
|
342
399
|
if (ref === `${proj}:${oldId}`) {
|
|
343
400
|
changed = true;
|
|
344
|
-
return
|
|
401
|
+
return target;
|
|
345
402
|
}
|
|
346
403
|
return ref;
|
|
347
404
|
});
|
|
348
|
-
doc.related_docs = newRelated;
|
|
349
405
|
if (changed) {
|
|
350
406
|
doc.updatedAt = new Date().toISOString();
|
|
351
407
|
await this.saveDoc(doc, otherProj);
|
package/dist/src/gui-worker.js
CHANGED
|
@@ -7,12 +7,14 @@ import { IPCMessageSchema } from "../shared/schemas/doc.schema.js";
|
|
|
7
7
|
import { isReadOnly } from "../shared/read-only.js";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import fs from "fs";
|
|
10
|
-
import { execFile } from "child_process";
|
|
10
|
+
import { execFile, spawn } from "child_process";
|
|
11
|
+
import { Readable } from "stream";
|
|
11
12
|
import { promisify } from "util";
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
import { fileURLToPath } from "url";
|
|
14
15
|
import { createLogger, setupLogging } from "@imenam/mcp-gui-interface";
|
|
15
16
|
import { resolveLogDir } from "./core/log-dir.js";
|
|
17
|
+
import { isLogSource, logFileInfo, readGitHubLog, readServerLog } from "./core/log-reader.js";
|
|
16
18
|
import matter from "gray-matter";
|
|
17
19
|
import JSZip from "jszip";
|
|
18
20
|
import puppeteer from "puppeteer-core";
|
|
@@ -427,6 +429,61 @@ app.post("/api/code-roots/apply", async (c) => {
|
|
|
427
429
|
app.get("/api/github-token", (c) => relay(c, "GET_GITHUB_TOKEN", {}, 15000));
|
|
428
430
|
app.get("/api/code-source-migration", (c) => relay(c, "GET_CODE_SOURCE_MIGRATION", {}, 5000));
|
|
429
431
|
app.post("/api/code-source-migration/ack", (c) => relay(c, "ACK_CODE_SOURCE_MIGRATION", {}, 5000));
|
|
432
|
+
// --- Logs ---
|
|
433
|
+
// Read straight from the log directory: both processes write there, and reading needs no IPC.
|
|
434
|
+
const logDir = resolveLogDir(rootDir);
|
|
435
|
+
app.get("/api/logs", (c) => c.json({ dir: logDir, files: [logFileInfo(logDir, "server"), logFileInfo(logDir, "github")] }));
|
|
436
|
+
// `knownSize` and `knownMtime` echo the file state the client already holds: when the file has
|
|
437
|
+
// not changed, nothing is re-read.
|
|
438
|
+
app.get("/api/logs/:source", (c) => {
|
|
439
|
+
const source = c.req.param("source");
|
|
440
|
+
if (!isLogSource(source))
|
|
441
|
+
return c.json({ error: `Unknown log "${source}"` }, 404);
|
|
442
|
+
const info = logFileInfo(logDir, source);
|
|
443
|
+
if (c.req.query("knownSize") === String(info.size) && c.req.query("knownMtime") === (info.mtime ?? "")) {
|
|
444
|
+
return c.json({ unchanged: true, file: info });
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
return c.json(source === "server" ? readServerLog(logDir) : readGitHubLog(logDir));
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
return c.json({ error: `Cannot read ${info.path}: ${err.message}` }, 500);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
app.get("/api/logs/:source/download", (c) => {
|
|
454
|
+
const source = c.req.param("source");
|
|
455
|
+
if (!isLogSource(source))
|
|
456
|
+
return c.json({ error: `Unknown log "${source}"` }, 404);
|
|
457
|
+
const info = logFileInfo(logDir, source);
|
|
458
|
+
if (!info.exists)
|
|
459
|
+
return c.json({ error: `${info.path} does not exist yet` }, 404);
|
|
460
|
+
const body = Readable.toWeb(fs.createReadStream(info.path));
|
|
461
|
+
return new Response(body, {
|
|
462
|
+
headers: {
|
|
463
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
464
|
+
"Content-Length": String(info.size),
|
|
465
|
+
"Content-Disposition": `attachment; filename="${info.file}"`,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
// Opens the log directory in the file manager of the machine running the server.
|
|
470
|
+
app.post("/api/logs/open-folder", async (c) => {
|
|
471
|
+
const command = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
472
|
+
try {
|
|
473
|
+
await new Promise((resolve, reject) => {
|
|
474
|
+
const child = spawn(command, [logDir], { detached: true, stdio: "ignore" });
|
|
475
|
+
child.once("error", reject);
|
|
476
|
+
child.once("spawn", () => {
|
|
477
|
+
child.unref();
|
|
478
|
+
resolve();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
return c.json({ ok: true });
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
return c.json({ error: `Cannot open ${logDir} with ${command}: ${err.message}` }, 500);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
430
487
|
app.get("/api/projects", async (c) => {
|
|
431
488
|
const result = await ipcRequest("LIST_PROJECTS");
|
|
432
489
|
if (result.error)
|
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,14 +1137,14 @@ 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) 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
|
|
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
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)." },
|
|
1138
1146
|
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION + " This is the project the entry is moved from." },
|
|
1139
|
-
new_project: { type: "string", description: "Project the entry is moved to. Must already exist (see list_projects). Omitted: the entry stays in `project`." },
|
|
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`." },
|
|
1140
1148
|
},
|
|
1141
1149
|
required: ["path", "project"],
|
|
1142
1150
|
},
|
|
@@ -1173,6 +1181,38 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1173
1181
|
properties: {},
|
|
1174
1182
|
},
|
|
1175
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
|
+
},
|
|
1176
1216
|
{
|
|
1177
1217
|
name: "refresh_file_snapshot",
|
|
1178
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.",
|
|
@@ -1208,7 +1248,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1208
1248
|
},
|
|
1209
1249
|
{
|
|
1210
1250
|
name: "patch_doc",
|
|
1211
|
-
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.",
|
|
1212
1252
|
inputSchema: {
|
|
1213
1253
|
type: "object",
|
|
1214
1254
|
properties: {
|
|
@@ -1217,6 +1257,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1217
1257
|
...textSourceProperties("search", "the search text"),
|
|
1218
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." },
|
|
1219
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." },
|
|
1220
1261
|
project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
|
|
1221
1262
|
},
|
|
1222
1263
|
required: ["path", "project"],
|
|
@@ -1224,7 +1265,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1224
1265
|
},
|
|
1225
1266
|
{
|
|
1226
1267
|
name: "git_diff_doc",
|
|
1227
|
-
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.",
|
|
1228
1269
|
inputSchema: {
|
|
1229
1270
|
type: "object",
|
|
1230
1271
|
properties: {
|
|
@@ -1521,8 +1562,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1521
1562
|
};
|
|
1522
1563
|
});
|
|
1523
1564
|
// Each tool call is one operation: the branch head every tracked file is compared against is
|
|
1524
|
-
// resolved once for the whole call, and never carried over to the next one.
|
|
1525
|
-
|
|
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))));
|
|
1526
1568
|
async function handleToolCall(request) {
|
|
1527
1569
|
const { name, arguments: args } = request.params;
|
|
1528
1570
|
if (isReadOnly() && WRITE_TOOLS.has(name)) {
|
|
@@ -1555,12 +1597,35 @@ async function handleToolCall(request) {
|
|
|
1555
1597
|
type: "text",
|
|
1556
1598
|
text: "set_project no longer exists: there is no active project. " +
|
|
1557
1599
|
"Pass the project explicitly to every tool call (use list_projects to see them). " +
|
|
1558
|
-
"
|
|
1600
|
+
"Create a project with create_project, or on demand with create_doc.",
|
|
1559
1601
|
},
|
|
1560
1602
|
],
|
|
1561
1603
|
};
|
|
1562
1604
|
case "list_projects":
|
|
1563
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
|
+
}
|
|
1564
1629
|
case "refresh_file_snapshot":
|
|
1565
1630
|
return await refreshFileSnapshot(storageEngine, historyStore, args);
|
|
1566
1631
|
case "get_doc_metadata":
|
|
@@ -1685,7 +1750,7 @@ async function syncPullOnStartup() {
|
|
|
1685
1750
|
async function main() {
|
|
1686
1751
|
await migrateFolders(storageEngine);
|
|
1687
1752
|
await migrateEmbeddings(embeddingStore, storageEngine);
|
|
1688
|
-
await syncPullOnStartup
|
|
1753
|
+
await runTriggeredBy("startup sync", syncPullOnStartup);
|
|
1689
1754
|
migrateCodeSources(storageEngine, localState);
|
|
1690
1755
|
const transport = new StdioServerTransport();
|
|
1691
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 ?? {});
|