@imenam/mcp-documentor 1.0.87 → 1.0.89
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 +10 -6
- 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/blob-store.d.ts +5 -0
- package/dist/src/core/blob-store.js +49 -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 +41 -2
- package/dist/src/core/github-code-api.d.ts +7 -11
- package/dist/src/core/github-code-api.js +94 -41
- 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 +77 -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-BSPFCwLF.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-BIPBAzZr.js → architectureDiagram-3BPJPVTR-DHZcCdLS.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-BE9WwuyF.js → blockDiagram-GPEHLZMM-D9dTrlKM.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-V1_4ktIt.js → c4Diagram-AAUBKEIU-BF3-rTSW.js} +1 -1
- package/gui/dist/assets/channel-BgXnhna2.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-DskG-Kmf.js → chunk-2J33WTMH-B_rKbUyS.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-DaPhidEI.js → chunk-4BX2VUAB-C9w424bT.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-C9r6WDFM.js → chunk-55IACEB6-B2gLfiw3.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-BA-aC8US.js → chunk-727SXJPM-CXLwbQzD.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-ByBp3whV.js → chunk-AQP2D5EJ-DIEelNex.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-CzqUByx7.js → chunk-FMBD7UC4-CKDjYi8Q.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-C0_B2ryQ.js → chunk-ND2GUHAM-D5NTnoWH.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-B3eEwjDh.js → chunk-QZHKN3VN-Cc9DzsoT.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-DmPDrQWl.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-DmPDrQWl.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-D_i0I2pU.js → cose-bilkent-S5V4N54A-zK_y1Jct.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-DtEVPK_z.js → dagre-BM42HDAG-D87rXs2_.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-BftnXa5w.js → diagram-2AECGRRQ-0qYWhZfZ.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-Dh9-kl_u.js → diagram-5GNKFQAL-BvmrfVQ0.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-CvB0BWCa.js → diagram-KO2AKTUF-0DIH1FgX.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-CDy-J1uy.js → diagram-LMA3HP47-T8W429Io.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-iY9FwkIF.js → diagram-OG6HWLK6-DJMnhtDn.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-BT2_IX8D.js → erDiagram-TEJ5UH35-CyuwcTwK.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-DaixWyMx.js → flowDiagram-I6XJVG4X-xhGOn_4T.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-DSo3cQPH.js → ganttDiagram-6RSMTGT7-CmkhPjbj.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-BfFT7Lds.js → gitGraphDiagram-PVQCEYII-CyMVk2nl.js} +1 -1
- package/gui/dist/assets/{index-DPGmAc6z.js → index-CCqNIUBp.js} +154 -152
- package/gui/dist/assets/index-CtPuh-0k.css +1 -0
- package/gui/dist/assets/{infoDiagram-5YYISTIA-BxbxNxe2.js → infoDiagram-5YYISTIA-q2gOQizR.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DpopzqZr.js → ishikawaDiagram-YF4QCWOH-C_a8ouWt.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-DrOlFzIk.js → journeyDiagram-JHISSGLW-B0XgZgmz.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-vH81lYXy.js → kanban-definition-UN3LZRKU-BE1EyBT0.js} +1 -1
- package/gui/dist/assets/{linear-BipFpFah.js → linear-CJpiIHfp.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-DsTxCLgj.js → mindmap-definition-RKZ34NQL-BqrhFP-W.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-Bbq2ZWQU.js → pieDiagram-4H26LBE5-TchMhFLa.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BdTSaEo4.js → quadrantDiagram-W4KKPZXB-KjwnPHRE.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-BndI8YZy.js → requirementDiagram-4Y6WPE33-9wlFYy5z.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-ofBkVITf.js → sankeyDiagram-5OEKKPKP-3DSwfggy.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-Cjj2UrN9.js → sequenceDiagram-3UESZ5HK-CeGhZql1.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-CzPrXUuz.js → stateDiagram-AJRCARHV-DgPL6ffw.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-y8DOR9eQ.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-Cz4X-IOO.js → timeline-definition-PNZ67QCA-BUs8-FJU.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-BwIgXJcZ.js → vennDiagram-CIIHVFJN-Csa_wIyD.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-keQS0aNZ.js → wardley-L42UT6IY-Cw3ksKPO.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-DhykSgbD.js → wardleyDiagram-YWT4CUSO-C0tdxIha.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DFTqc4cj.js → xychartDiagram-2RQKCTM6-C6UgKrOd.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,38 @@
|
|
|
1
|
+
export declare const GITHUB_LOG_FILE = "github.log";
|
|
2
|
+
/**
|
|
3
|
+
* One call to GitHub, as written to github.log — one JSON object per line.
|
|
4
|
+
*
|
|
5
|
+
* kind "git" for a git command reaching the remote, "api" for a REST request
|
|
6
|
+
* action what was done, in plain words
|
|
7
|
+
* command the exact git command or HTTP request
|
|
8
|
+
* repo "owner/repo" the call targets, when it targets one
|
|
9
|
+
* trigger the tool call, GUI request or server step that caused it
|
|
10
|
+
* status HTTP status, or "ok" / "rejected" / "failed" / "network error"
|
|
11
|
+
*/
|
|
12
|
+
export interface GitHubLogEntry {
|
|
13
|
+
ts: string;
|
|
14
|
+
kind: "git" | "api";
|
|
15
|
+
action: string;
|
|
16
|
+
command: string;
|
|
17
|
+
repo: string | null;
|
|
18
|
+
trigger: string;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
status: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
rateLimit?: {
|
|
24
|
+
remaining: number;
|
|
25
|
+
limit: number;
|
|
26
|
+
reset: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Runs `fn` with `trigger` recorded as the cause of every GitHub call it makes. */
|
|
30
|
+
export declare function runTriggeredBy<T>(trigger: string, fn: () => T): T;
|
|
31
|
+
/** Opens github.log in `logDir` for appending. */
|
|
32
|
+
export declare function initGitHubLog(logDir: string): void;
|
|
33
|
+
/** Masks credentials embedded in URLs or tokens quoted verbatim. */
|
|
34
|
+
export declare function redactSecrets(text: string, secrets?: Array<string | undefined>): string;
|
|
35
|
+
/** "owner/repo" for a GitHub URL, the host and path for any other remote. */
|
|
36
|
+
export declare function describeRemote(remoteUrl: string): string;
|
|
37
|
+
/** Appends one call to github.log and mirrors it in server.log. */
|
|
38
|
+
export declare function recordGitHubCall(call: Omit<GitHubLogEntry, "ts" | "trigger">): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createLogger } from "@imenam/mcp-gui-interface";
|
|
5
|
+
const logger = createLogger("GitHub");
|
|
6
|
+
export const GITHUB_LOG_FILE = "github.log";
|
|
7
|
+
const triggerStorage = new AsyncLocalStorage();
|
|
8
|
+
/** Runs `fn` with `trigger` recorded as the cause of every GitHub call it makes. */
|
|
9
|
+
export function runTriggeredBy(trigger, fn) {
|
|
10
|
+
return triggerStorage.run(trigger, fn);
|
|
11
|
+
}
|
|
12
|
+
function currentTrigger() {
|
|
13
|
+
return triggerStorage.getStore() ?? "server";
|
|
14
|
+
}
|
|
15
|
+
let stream = null;
|
|
16
|
+
/** Opens github.log in `logDir` for appending. */
|
|
17
|
+
export function initGitHubLog(logDir) {
|
|
18
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
19
|
+
const file = path.join(logDir, GITHUB_LOG_FILE);
|
|
20
|
+
stream = fs.createWriteStream(file, { flags: "a", encoding: "utf8" });
|
|
21
|
+
stream.on("error", (err) => {
|
|
22
|
+
stream = null;
|
|
23
|
+
logger.error(`Cannot write ${file}, GitHub calls are no longer recorded there: ${err.message}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Masks credentials embedded in URLs or tokens quoted verbatim. */
|
|
27
|
+
export function redactSecrets(text, secrets = []) {
|
|
28
|
+
let out = text.replace(/(https?:\/\/)[^@\s/]+@/gi, "$1***@");
|
|
29
|
+
for (const secret of secrets) {
|
|
30
|
+
if (secret)
|
|
31
|
+
out = out.split(secret).join("***");
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** "owner/repo" for a GitHub URL, the host and path for any other remote. */
|
|
36
|
+
export function describeRemote(remoteUrl) {
|
|
37
|
+
const cleaned = redactSecrets(remoteUrl).replace(/^https?:\/\/(\*\*\*@)?/i, "").replace(/^git@/i, "").replace(/\.git$/i, "");
|
|
38
|
+
const github = cleaned.match(/^github\.com[/:](.+)$/i);
|
|
39
|
+
return github ? github[1] : cleaned.replace(":", "/");
|
|
40
|
+
}
|
|
41
|
+
/** Appends one call to github.log and mirrors it in server.log. */
|
|
42
|
+
export function recordGitHubCall(call) {
|
|
43
|
+
const entry = {
|
|
44
|
+
ts: new Date().toISOString(),
|
|
45
|
+
kind: call.kind,
|
|
46
|
+
action: call.action,
|
|
47
|
+
command: call.command,
|
|
48
|
+
repo: call.repo,
|
|
49
|
+
trigger: currentTrigger(),
|
|
50
|
+
ok: call.ok,
|
|
51
|
+
status: call.status,
|
|
52
|
+
durationMs: call.durationMs,
|
|
53
|
+
...(call.error ? { error: call.error } : {}),
|
|
54
|
+
...(call.rateLimit ? { rateLimit: call.rateLimit } : {}),
|
|
55
|
+
};
|
|
56
|
+
stream?.write(`${JSON.stringify(entry)}\n`);
|
|
57
|
+
const summary = `${entry.command} · ${entry.status} · ${entry.durationMs} ms · ${entry.trigger}`;
|
|
58
|
+
if (entry.ok)
|
|
59
|
+
logger.info(summary);
|
|
60
|
+
else if (entry.status === "404" || entry.status === "422")
|
|
61
|
+
logger.warn(`${summary} · ${entry.error ?? ""}`);
|
|
62
|
+
else
|
|
63
|
+
logger.error(`${summary} · ${entry.error ?? ""}`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type GitHubLogEntry } from "./github-log.js";
|
|
2
|
+
export declare const SERVER_LOG_FILE = "server.log";
|
|
3
|
+
export declare const LOG_SOURCES: {
|
|
4
|
+
readonly server: "server.log";
|
|
5
|
+
readonly github: "github.log";
|
|
6
|
+
};
|
|
7
|
+
export type LogSourceId = keyof typeof LOG_SOURCES;
|
|
8
|
+
export declare function isLogSource(id: string): id is LogSourceId;
|
|
9
|
+
export interface LogFileInfo {
|
|
10
|
+
id: LogSourceId;
|
|
11
|
+
file: string;
|
|
12
|
+
path: string;
|
|
13
|
+
exists: boolean;
|
|
14
|
+
size: number;
|
|
15
|
+
mtime: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** One line of server.log, with the lines that followed it without a timestamp of their own. */
|
|
18
|
+
export interface ServerLogEntry {
|
|
19
|
+
ts: string;
|
|
20
|
+
pid: number;
|
|
21
|
+
process: string;
|
|
22
|
+
level: "DEBUG" | "INFO" | "WARN" | "ERROR";
|
|
23
|
+
scope: string;
|
|
24
|
+
message: string;
|
|
25
|
+
continuation: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface GitHubLogLine extends GitHubLogEntry {
|
|
28
|
+
raw: string;
|
|
29
|
+
}
|
|
30
|
+
/** A window on the end of a log file. `partial` is true when older lines lie before it. */
|
|
31
|
+
export interface LogWindow<T> {
|
|
32
|
+
file: LogFileInfo;
|
|
33
|
+
partial: boolean;
|
|
34
|
+
entries: T[];
|
|
35
|
+
/** Lines that could not be parsed, left out of `entries`. */
|
|
36
|
+
unreadable: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function logFileInfo(logDir: string, id: LogSourceId): LogFileInfo;
|
|
39
|
+
/**
|
|
40
|
+
* Parses server.log lines as written by the shared logger:
|
|
41
|
+
* [ISO date] [PID n] [process] [LEVEL] [scope] message
|
|
42
|
+
* Text printed straight to the console carries no level nor scope: it is read as INFO from
|
|
43
|
+
* "console". A line without the date prefix continues the entry above it (stack traces).
|
|
44
|
+
*/
|
|
45
|
+
export declare function readServerLog(logDir: string): LogWindow<ServerLogEntry>;
|
|
46
|
+
/** Parses github.log: one JSON object per line. */
|
|
47
|
+
export declare function readGitHubLog(logDir: string): LogWindow<GitHubLogLine>;
|
|
@@ -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)
|