@imenam/mcp-documentor 1.0.84 → 1.0.86

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.
Files changed (78) hide show
  1. package/README.md +323 -329
  2. package/dist/shared/read-only.js +1 -0
  3. package/dist/src/cli/setup-mcp.js +2 -5
  4. package/dist/src/core/code-source.d.ts +33 -22
  5. package/dist/src/core/code-source.js +92 -69
  6. package/dist/src/core/github-code-api.d.ts +28 -0
  7. package/dist/src/core/github-code-api.js +92 -31
  8. package/dist/src/core/local-state.d.ts +49 -0
  9. package/dist/src/core/local-state.js +78 -0
  10. package/dist/src/core/migrate-code-sources.d.ts +17 -0
  11. package/dist/src/core/migrate-code-sources.js +157 -0
  12. package/dist/src/core/path-resolver.d.ts +29 -9
  13. package/dist/src/core/path-resolver.js +18 -42
  14. package/dist/src/core/project-settings.d.ts +21 -0
  15. package/dist/src/core/project-settings.js +63 -0
  16. package/dist/src/gui-worker.js +72 -3
  17. package/dist/src/index.js +160 -7
  18. package/dist/src/tools/check-freshness.d.ts +2 -1
  19. package/dist/src/tools/check-freshness.js +1 -2
  20. package/dist/src/tools/code-sources.d.ts +102 -0
  21. package/dist/src/tools/code-sources.js +260 -0
  22. package/dist/src/version.d.ts +1 -1
  23. package/dist/src/version.js +16 -2
  24. package/gui/dist/assets/{arc-CdXRzMBT.js → arc-DBgCQU2O.js} +1 -1
  25. package/gui/dist/assets/{architectureDiagram-3BPJPVTR-CGUnwe_I.js → architectureDiagram-3BPJPVTR-BIPBAzZr.js} +1 -1
  26. package/gui/dist/assets/{blockDiagram-GPEHLZMM-BCI5_vQE.js → blockDiagram-GPEHLZMM-BE9WwuyF.js} +1 -1
  27. package/gui/dist/assets/{c4Diagram-AAUBKEIU-Bm9a6HGg.js → c4Diagram-AAUBKEIU-V1_4ktIt.js} +1 -1
  28. package/gui/dist/assets/channel-B_cci0fU.js +1 -0
  29. package/gui/dist/assets/{chunk-2J33WTMH-c7xNwExL.js → chunk-2J33WTMH-DskG-Kmf.js} +1 -1
  30. package/gui/dist/assets/{chunk-4BX2VUAB-CmcU4Q0o.js → chunk-4BX2VUAB-DaPhidEI.js} +1 -1
  31. package/gui/dist/assets/{chunk-55IACEB6-K8rRjNKN.js → chunk-55IACEB6-C9r6WDFM.js} +1 -1
  32. package/gui/dist/assets/{chunk-727SXJPM-DMPnL1bv.js → chunk-727SXJPM-BA-aC8US.js} +1 -1
  33. package/gui/dist/assets/{chunk-AQP2D5EJ-BeRIIft8.js → chunk-AQP2D5EJ-ByBp3whV.js} +1 -1
  34. package/gui/dist/assets/{chunk-FMBD7UC4-CN4It-fp.js → chunk-FMBD7UC4-CzqUByx7.js} +1 -1
  35. package/gui/dist/assets/{chunk-ND2GUHAM-Dy-ZqPua.js → chunk-ND2GUHAM-C0_B2ryQ.js} +1 -1
  36. package/gui/dist/assets/{chunk-QZHKN3VN-CaV4seJw.js → chunk-QZHKN3VN-B3eEwjDh.js} +1 -1
  37. package/gui/dist/assets/classDiagram-4FO5ZUOK-BqlcJ5BP.js +1 -0
  38. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-BqlcJ5BP.js +1 -0
  39. package/gui/dist/assets/{cose-bilkent-S5V4N54A-B1qt_j1Z.js → cose-bilkent-S5V4N54A-D_i0I2pU.js} +1 -1
  40. package/gui/dist/assets/{dagre-BM42HDAG-4x5JEBQG.js → dagre-BM42HDAG-DtEVPK_z.js} +1 -1
  41. package/gui/dist/assets/{diagram-2AECGRRQ-Q7TvjRjv.js → diagram-2AECGRRQ-BftnXa5w.js} +1 -1
  42. package/gui/dist/assets/{diagram-5GNKFQAL-Dguj26bf.js → diagram-5GNKFQAL-Dh9-kl_u.js} +1 -1
  43. package/gui/dist/assets/{diagram-KO2AKTUF-DBv6MoZF.js → diagram-KO2AKTUF-CvB0BWCa.js} +1 -1
  44. package/gui/dist/assets/{diagram-LMA3HP47-mfVXwL5q.js → diagram-LMA3HP47-CDy-J1uy.js} +1 -1
  45. package/gui/dist/assets/{diagram-OG6HWLK6-Bn3_yQWG.js → diagram-OG6HWLK6-iY9FwkIF.js} +1 -1
  46. package/gui/dist/assets/{erDiagram-TEJ5UH35-Bp4wnBw7.js → erDiagram-TEJ5UH35-BT2_IX8D.js} +1 -1
  47. package/gui/dist/assets/{flowDiagram-I6XJVG4X-CGk72E1Q.js → flowDiagram-I6XJVG4X-DaixWyMx.js} +1 -1
  48. package/gui/dist/assets/{ganttDiagram-6RSMTGT7-C1WQIgpu.js → ganttDiagram-6RSMTGT7-DSo3cQPH.js} +1 -1
  49. package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-DSIs1ZqI.js → gitGraphDiagram-PVQCEYII-BfFT7Lds.js} +1 -1
  50. package/gui/dist/assets/index-CxCC1gxu.css +1 -0
  51. package/gui/dist/assets/{index-T7ZLHNgs.js → index-DPGmAc6z.js} +107 -105
  52. package/gui/dist/assets/{infoDiagram-5YYISTIA-DzHdfk5Y.js → infoDiagram-5YYISTIA-BxbxNxe2.js} +1 -1
  53. package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DLxqTmDU.js → ishikawaDiagram-YF4QCWOH-DpopzqZr.js} +1 -1
  54. package/gui/dist/assets/{journeyDiagram-JHISSGLW-DYUPmZnf.js → journeyDiagram-JHISSGLW-DrOlFzIk.js} +1 -1
  55. package/gui/dist/assets/{kanban-definition-UN3LZRKU-f6km2MDT.js → kanban-definition-UN3LZRKU-vH81lYXy.js} +1 -1
  56. package/gui/dist/assets/{linear-DHhAaAXH.js → linear-BipFpFah.js} +1 -1
  57. package/gui/dist/assets/{mindmap-definition-RKZ34NQL-nergVxV3.js → mindmap-definition-RKZ34NQL-DsTxCLgj.js} +1 -1
  58. package/gui/dist/assets/{pieDiagram-4H26LBE5-D4IJAdGz.js → pieDiagram-4H26LBE5-Bbq2ZWQU.js} +1 -1
  59. package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-B5yYdOsV.js → quadrantDiagram-W4KKPZXB-BdTSaEo4.js} +1 -1
  60. package/gui/dist/assets/{requirementDiagram-4Y6WPE33-BrhJp07G.js → requirementDiagram-4Y6WPE33-BndI8YZy.js} +1 -1
  61. package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-CUBA4e7F.js → sankeyDiagram-5OEKKPKP-ofBkVITf.js} +1 -1
  62. package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-DP0-rUie.js → sequenceDiagram-3UESZ5HK-Cjj2UrN9.js} +1 -1
  63. package/gui/dist/assets/{stateDiagram-AJRCARHV-_BnVcpam.js → stateDiagram-AJRCARHV-CzPrXUuz.js} +1 -1
  64. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DAq4ObN9.js +1 -0
  65. package/gui/dist/assets/{timeline-definition-PNZ67QCA-crbYjsrq.js → timeline-definition-PNZ67QCA-Cz4X-IOO.js} +1 -1
  66. package/gui/dist/assets/{vennDiagram-CIIHVFJN-EOqENtE1.js → vennDiagram-CIIHVFJN-BwIgXJcZ.js} +1 -1
  67. package/gui/dist/assets/{wardley-L42UT6IY-B1FVq-_Y.js → wardley-L42UT6IY-keQS0aNZ.js} +1 -1
  68. package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-D1DZwIH_.js → wardleyDiagram-YWT4CUSO-DhykSgbD.js} +1 -1
  69. package/gui/dist/assets/{xychartDiagram-2RQKCTM6-B6Wr8_OJ.js → xychartDiagram-2RQKCTM6-DFTqc4cj.js} +1 -1
  70. package/gui/dist/index.html +2 -2
  71. package/package.json +3 -4
  72. package/scripts/migrate-paths-to-relative.js +196 -192
  73. package/gui/dist/assets/channel-f0BgDIcS.js +0 -1
  74. package/gui/dist/assets/classDiagram-4FO5ZUOK-tGvt4n1B.js +0 -1
  75. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-tGvt4n1B.js +0 -1
  76. package/gui/dist/assets/index-QkCDSQ2A.css +0 -1
  77. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-B5Vwq56z.js +0 -1
  78. package/scripts/generate-version.js +0 -20
@@ -0,0 +1,157 @@
1
+ import { createLogger } from "@imenam/mcp-gui-interface";
2
+ import { parseRepoSpec } from "./github-code-api.js";
3
+ import { hasCodeSourceConfig, writeCodeSourceConfig } from "./project-settings.js";
4
+ const logger = createLogger("MigrateCodeSources");
5
+ const REPO_VAR = "MCP_DOCUMENTOR_CODE_REPO";
6
+ const BRANCH_VAR = "MCP_DOCUMENTOR_CODE_BRANCH";
7
+ const ROOTS_VAR = "MCP_DOCUMENTOR_CODE_ROOTS";
8
+ const ROOT_VAR = "MCP_DOCUMENTOR_CODE_ROOT";
9
+ const short = (name) => name.replace(/^MCP_DOCUMENTOR_/, "");
10
+ function readVar(name) {
11
+ return process.env[name]?.trim() || undefined;
12
+ }
13
+ /** Entries of a JSON map variable, keeping non-empty string values only. */
14
+ function readJsonMap(name, raw) {
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ }
19
+ catch {
20
+ throw new Error(`${name} looks like a JSON map but could not be parsed`);
21
+ }
22
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
23
+ throw new Error(`${name} is not a JSON object`);
24
+ }
25
+ return Object.entries(parsed)
26
+ .filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "")
27
+ .map(([project, value]) => [project, value.trim()]);
28
+ }
29
+ /** The (project, spec) pairs MCP_DOCUMENTOR_CODE_REPO declares: one spec for all, or a JSON map. */
30
+ function readRepoSpecs(raw, projects) {
31
+ if (!raw.startsWith("{"))
32
+ return projects.map((project) => [project, raw]);
33
+ return readJsonMap(REPO_VAR, raw);
34
+ }
35
+ /**
36
+ * MCP_DOCUMENTOR_CODE_ROOT seeds the default code root of a data directory that has none — the
37
+ * setup writes it with the repository's folder. Once a default root is stored, the variable is
38
+ * not read again: a different value is only logged.
39
+ */
40
+ function seedDefaultCodeRoot(localState) {
41
+ const envRoot = readVar(ROOT_VAR);
42
+ if (!envRoot)
43
+ return undefined;
44
+ const stored = localState.getCodeRoots().default;
45
+ if (stored === undefined) {
46
+ localState.setDefaultCodeRoot(envRoot);
47
+ logger.info(`Default code root set from ${ROOT_VAR}: ${envRoot}`);
48
+ return { kind: "code-root", status: "imported", detail: `Default code root · ${envRoot}`, from: short(ROOT_VAR) };
49
+ }
50
+ if (stored !== envRoot) {
51
+ logger.info(`${ROOT_VAR} (${envRoot}) is ignored: the default code root of this machine is ${stored}.`);
52
+ }
53
+ return undefined;
54
+ }
55
+ function migrateRepos(storageEngine, projects, entries) {
56
+ const repoRaw = readVar(REPO_VAR);
57
+ if (!repoRaw)
58
+ return;
59
+ const defaultBranch = readVar(BRANCH_VAR);
60
+ let specs = [];
61
+ try {
62
+ specs = readRepoSpecs(repoRaw, projects);
63
+ }
64
+ catch (err) {
65
+ entries.push({ kind: "github", status: "error", detail: `Not imported — ${err.message}`, from: short(REPO_VAR) });
66
+ }
67
+ for (const [project, spec] of specs) {
68
+ if (!projects.includes(project)) {
69
+ entries.push({ project, kind: "github", status: "skipped", detail: "Not imported — no such project", from: short(REPO_VAR) });
70
+ continue;
71
+ }
72
+ const projectDir = storageEngine.getProjectDir(project);
73
+ if (hasCodeSourceConfig(projectDir)) {
74
+ entries.push({ project, kind: "github", status: "skipped", detail: "Already configured — kept as is", from: short(REPO_VAR) });
75
+ continue;
76
+ }
77
+ if (spec.toLowerCase() === "local") {
78
+ writeCodeSourceConfig(projectDir, { kind: "local" });
79
+ entries.push({ project, kind: "local", status: "imported", detail: "Local working copy", from: `${short(REPO_VAR)} "local"` });
80
+ continue;
81
+ }
82
+ let config;
83
+ let from = short(REPO_VAR);
84
+ try {
85
+ const parsed = parseRepoSpec(spec);
86
+ const branch = parsed.ref ?? defaultBranch;
87
+ if (!parsed.ref && defaultBranch)
88
+ from += ` + ${short(BRANCH_VAR)}`;
89
+ config = { kind: "github", repo: `${parsed.owner}/${parsed.repo}`, ...(branch && { branch }) };
90
+ }
91
+ catch (err) {
92
+ entries.push({ project, kind: "github", status: "error", detail: `Not imported — ${err.message}`, from });
93
+ continue;
94
+ }
95
+ writeCodeSourceConfig(projectDir, config);
96
+ entries.push({ project, kind: "github", status: "imported", detail: `${config.repo} · ${config.branch ?? "default branch"}`, from });
97
+ }
98
+ }
99
+ function migrateCodeRoots(localState, projects, entries) {
100
+ const rootsRaw = readVar(ROOTS_VAR);
101
+ if (!rootsRaw)
102
+ return;
103
+ let roots = [];
104
+ try {
105
+ roots = readJsonMap(ROOTS_VAR, rootsRaw);
106
+ }
107
+ catch (err) {
108
+ entries.push({ kind: "code-root", status: "error", detail: `Not imported — ${err.message}`, from: short(ROOTS_VAR) });
109
+ }
110
+ const existing = localState.getCodeRoots().byProject;
111
+ for (const [project, root] of roots) {
112
+ if (!projects.includes(project)) {
113
+ entries.push({ project, kind: "code-root", status: "skipped", detail: "Not imported — no such project", from: short(ROOTS_VAR) });
114
+ }
115
+ else if (existing[project] !== undefined) {
116
+ entries.push({ project, kind: "code-root", status: "skipped", detail: "Code root already set — kept as is", from: short(ROOTS_VAR) });
117
+ }
118
+ else {
119
+ localState.setProjectCodeRoot(project, root);
120
+ entries.push({ project, kind: "code-root", status: "imported", detail: `Code root · ${root}`, from: short(ROOTS_VAR) });
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Moves the code-source environment variables into the project settings and the machine-local
126
+ * code roots, once per machine.
127
+ *
128
+ * - MCP_DOCUMENTOR_CODE_REPO (+ MCP_DOCUMENTOR_CODE_BRANCH for specs without "#branch") becomes
129
+ * the code source of each project it names. A project that already has a code source — written
130
+ * by another machine and pulled — keeps it.
131
+ * - MCP_DOCUMENTOR_CODE_ROOTS becomes the code root of each project it names, on this machine.
132
+ * - MCP_DOCUMENTOR_CODE_ROOT seeds the default code root (see `seedDefaultCodeRoot`); it is not
133
+ * one of the variables to remove.
134
+ *
135
+ * The outcome is recorded as a report in the local state; the GUI shows it until acknowledged.
136
+ * Once recorded, the migrated variables are never read again: their presence is only logged.
137
+ */
138
+ export function migrateCodeSources(storageEngine, localState) {
139
+ const seeded = seedDefaultCodeRoot(localState);
140
+ const present = [REPO_VAR, BRANCH_VAR, ROOTS_VAR].filter((name) => readVar(name) !== undefined);
141
+ if (present.length === 0)
142
+ return;
143
+ if (localState.getMigrationReport()) {
144
+ logger.warn(`${present.join(", ")} ${present.length > 1 ? "are" : "is"} set but no longer read — code sources and code roots ` +
145
+ `are configured per project in the GUI. Remove ${present.length > 1 ? "them" : "it"} from the MCP configuration.`);
146
+ return;
147
+ }
148
+ const projects = storageEngine.getConfig().projects;
149
+ const entries = [];
150
+ migrateRepos(storageEngine, projects, entries);
151
+ migrateCodeRoots(localState, projects, entries);
152
+ if (seeded)
153
+ entries.push(seeded);
154
+ localState.setMigrationReport({ migratedAt: new Date().toISOString(), entries, variables: present, seen: false });
155
+ logger.info(`Code sources migrated from ${present.join(", ")}: ${entries.filter((e) => e.status === "imported").length} imported, ` +
156
+ `${entries.filter((e) => e.status !== "imported").length} not imported. These variables are no longer read.`);
157
+ }
@@ -1,20 +1,40 @@
1
- /** Test-only: clears the memoised env snapshot so a changed env is re-read. */
2
- export declare function resetCodeRootsCache(): void;
3
- /** The configured code root for a project, or undefined if none applies. */
1
+ /**
2
+ * Resolves tracked-file paths between two representations:
3
+ *
4
+ * - **stored** — what we persist in a doc's `tracked_files[].path`. Portable
5
+ * across machines: relative to the project's "code root" and using forward
6
+ * slashes. Absolute paths may still appear (files located outside any code
7
+ * root) and are handled transparently.
8
+ * - **absolute** — a real filesystem path on the current machine, used for any
9
+ * `fs`/`git` access.
10
+ *
11
+ * The code root is machine-local — each machine clones its repos wherever it
12
+ * wants — so it is read from the machine-local state (a default root, and
13
+ * per-project roots), through the provider installed with `setCodeRootProvider`.
14
+ *
15
+ * Without a code root, paths are stored absolute and only absolute stored paths
16
+ * can be resolved.
17
+ */
18
+ /** The code root that applies to a project on this machine, or undefined. */
19
+ export type CodeRootProvider = (project: string) => string | undefined;
20
+ export declare function setCodeRootProvider(value: CodeRootProvider): void;
21
+ /** The code root that applies to a project on this machine, or undefined if none is set. */
4
22
  export declare function getCodeRoot(project: string): string | undefined;
5
23
  /**
6
24
  * Converts an input path (typically the absolute path supplied by an agent)
7
25
  * into the portable form persisted in a doc.
8
26
  *
9
- * Falls back to an absolute path — with a warning — when there is no code root,
10
- * or when the file lives outside the root (including a different Windows drive).
27
+ * Keeps an absolute path — with a warning — when there is no code root, or when
28
+ * the file lives outside the root (including a different Windows drive).
11
29
  */
12
30
  export declare function toStoredPath(inputPath: string, project: string): string;
13
31
  /**
14
- * Resolves a stored path to an absolute filesystem path on this machine.
32
+ * Resolves a stored path against a given code root:
15
33
  *
16
- * - absolute stored path (legacy / fallback) → returned as-is
17
- * - relative stored path + configured root → joined with the root
18
- * - relative stored path + no root configured → undefined (unverifiable here)
34
+ * - absolute stored path → returned as-is
35
+ * - relative stored path + a root → joined with the root
36
+ * - relative stored path + no root → undefined (unverifiable here)
19
37
  */
38
+ export declare function resolveAgainstRoot(stored: string, root: string | undefined): string | undefined;
39
+ /** Resolves a stored path to an absolute filesystem path on this machine. */
20
40
  export declare function toAbsolutePath(stored: string, project: string): string | undefined;
@@ -1,42 +1,15 @@
1
1
  import path from "path";
2
2
  import { createLogger } from "@imenam/mcp-gui-interface";
3
3
  const logger = createLogger("PathResolver");
4
- let cached;
5
- function loadCodeRoots() {
6
- if (cached)
7
- return cached;
8
- const defaultRoot = process.env.MCP_DOCUMENTOR_CODE_ROOT?.trim() || undefined;
9
- let byProject = {};
10
- const rawMap = process.env.MCP_DOCUMENTOR_CODE_ROOTS?.trim();
11
- if (rawMap) {
12
- try {
13
- const parsed = JSON.parse(rawMap);
14
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
15
- for (const [proj, root] of Object.entries(parsed)) {
16
- if (typeof root === "string" && root.trim() !== "") {
17
- byProject[proj] = root.trim();
18
- }
19
- }
20
- }
21
- else {
22
- logger.warn("MCP_DOCUMENTOR_CODE_ROOTS is not a JSON object, ignoring it");
23
- }
24
- }
25
- catch {
26
- logger.warn("Failed to parse MCP_DOCUMENTOR_CODE_ROOTS as JSON, ignoring it");
27
- }
28
- }
29
- cached = { default: defaultRoot, byProject };
30
- return cached;
31
- }
32
- /** Test-only: clears the memoised env snapshot so a changed env is re-read. */
33
- export function resetCodeRootsCache() {
34
- cached = undefined;
4
+ let provider;
5
+ export function setCodeRootProvider(value) {
6
+ provider = value;
35
7
  }
36
- /** The configured code root for a project, or undefined if none applies. */
8
+ /** The code root that applies to a project on this machine, or undefined if none is set. */
37
9
  export function getCodeRoot(project) {
38
- const roots = loadCodeRoots();
39
- return roots.byProject[project] ?? roots.default;
10
+ if (!provider)
11
+ throw new Error("code roots are read before setCodeRootProvider was called");
12
+ return provider(project);
40
13
  }
41
14
  function toForwardSlashes(p) {
42
15
  return p.split(path.sep).join("/");
@@ -45,8 +18,8 @@ function toForwardSlashes(p) {
45
18
  * Converts an input path (typically the absolute path supplied by an agent)
46
19
  * into the portable form persisted in a doc.
47
20
  *
48
- * Falls back to an absolute path — with a warning — when there is no code root,
49
- * or when the file lives outside the root (including a different Windows drive).
21
+ * Keeps an absolute path — with a warning — when there is no code root, or when
22
+ * the file lives outside the root (including a different Windows drive).
50
23
  */
51
24
  export function toStoredPath(inputPath, project) {
52
25
  const abs = path.resolve(process.cwd(), inputPath);
@@ -64,19 +37,22 @@ export function toStoredPath(inputPath, project) {
64
37
  return toForwardSlashes(rel);
65
38
  }
66
39
  /**
67
- * Resolves a stored path to an absolute filesystem path on this machine.
40
+ * Resolves a stored path against a given code root:
68
41
  *
69
- * - absolute stored path (legacy / fallback) → returned as-is
70
- * - relative stored path + configured root → joined with the root
71
- * - relative stored path + no root configured → undefined (unverifiable here)
42
+ * - absolute stored path → returned as-is
43
+ * - relative stored path + a root → joined with the root
44
+ * - relative stored path + no root → undefined (unverifiable here)
72
45
  */
73
- export function toAbsolutePath(stored, project) {
46
+ export function resolveAgainstRoot(stored, root) {
74
47
  if (path.isAbsolute(stored)) {
75
48
  return stored;
76
49
  }
77
- const root = getCodeRoot(project);
78
50
  if (!root) {
79
51
  return undefined;
80
52
  }
81
53
  return path.resolve(root, stored);
82
54
  }
55
+ /** Resolves a stored path to an absolute filesystem path on this machine. */
56
+ export function toAbsolutePath(stored, project) {
57
+ return resolveAgainstRoot(stored, getCodeRoot(project));
58
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Where a project's source code is read from.
4
+ *
5
+ * - `local` — the working copy on this machine, resolved through the code roots.
6
+ * - `github` — one branch of one repository, read over the GitHub API. `branch` undefined means
7
+ * the repository's default branch.
8
+ */
9
+ export declare const CodeSourceConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
10
+ kind: z.ZodLiteral<"local">;
11
+ }, z.core.$strip>, z.ZodObject<{
12
+ kind: z.ZodLiteral<"github">;
13
+ repo: z.ZodString;
14
+ branch: z.ZodOptional<z.ZodString>;
15
+ }, z.core.$strip>], "kind">;
16
+ export type CodeSourceConfig = z.infer<typeof CodeSourceConfigSchema>;
17
+ /** Whether the project has a code source written in its settings. */
18
+ export declare function hasCodeSourceConfig(projectDir: string): boolean;
19
+ /** The project's code source. Throws when the settings file exists but cannot be read. */
20
+ export declare function readCodeSourceConfig(projectDir: string): CodeSourceConfig;
21
+ export declare function writeCodeSourceConfig(projectDir: string, config: CodeSourceConfig): void;
@@ -0,0 +1,63 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { z } from "zod";
4
+ /**
5
+ * Where a project's source code is read from.
6
+ *
7
+ * - `local` — the working copy on this machine, resolved through the code roots.
8
+ * - `github` — one branch of one repository, read over the GitHub API. `branch` undefined means
9
+ * the repository's default branch.
10
+ */
11
+ export const CodeSourceConfigSchema = z.discriminatedUnion("kind", [
12
+ z.object({ kind: z.literal("local") }),
13
+ z.object({
14
+ kind: z.literal("github"),
15
+ repo: z.string().regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, 'expected "owner/repo"'),
16
+ branch: z.string().min(1).optional(),
17
+ }),
18
+ ]);
19
+ const ProjectSettingsSchema = z.object({
20
+ codeSource: CodeSourceConfigSchema.optional(),
21
+ });
22
+ /**
23
+ * Per-project settings, in `projects/<project>/project.json`. The file lives next to the docs, so
24
+ * it is versioned and synchronised with them: every machine reads the same code source.
25
+ */
26
+ function settingsPath(projectDir) {
27
+ return path.join(projectDir, "project.json");
28
+ }
29
+ /** A project without settings, or without a code source in them, reads its local working copy. */
30
+ const DEFAULT_CODE_SOURCE = { kind: "local" };
31
+ function readSettings(projectDir) {
32
+ const file = settingsPath(projectDir);
33
+ if (!fs.existsSync(file))
34
+ return {};
35
+ let raw;
36
+ try {
37
+ raw = JSON.parse(fs.readFileSync(file, "utf-8"));
38
+ }
39
+ catch {
40
+ throw new Error(`${file} is not valid JSON`);
41
+ }
42
+ const parsed = ProjectSettingsSchema.safeParse(raw);
43
+ if (!parsed.success) {
44
+ throw new Error(`${file} is invalid: ${parsed.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`);
45
+ }
46
+ return parsed.data;
47
+ }
48
+ /** Whether the project has a code source written in its settings. */
49
+ export function hasCodeSourceConfig(projectDir) {
50
+ return readSettings(projectDir).codeSource !== undefined;
51
+ }
52
+ /** The project's code source. Throws when the settings file exists but cannot be read. */
53
+ export function readCodeSourceConfig(projectDir) {
54
+ return readSettings(projectDir).codeSource ?? DEFAULT_CODE_SOURCE;
55
+ }
56
+ export function writeCodeSourceConfig(projectDir, config) {
57
+ const validated = CodeSourceConfigSchema.parse(config);
58
+ const file = settingsPath(projectDir);
59
+ const settings = fs.existsSync(file) ? readSettings(projectDir) : {};
60
+ settings.codeSource = validated;
61
+ fs.mkdirSync(projectDir, { recursive: true });
62
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n", "utf-8");
63
+ }
@@ -2,7 +2,7 @@ import { serve } from "@hono/node-server";
2
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
3
  import { Hono } from "hono";
4
4
  import { streamSSE } from "hono/streaming";
5
- import { ProxyClient } from "@imenam/mcp-gui-interface";
5
+ import { GUI_LISTEN_HOST, ProxyClient, honoGuiGuard } from "@imenam/mcp-gui-interface";
6
6
  import { IPCMessageSchema } from "../shared/schemas/doc.schema.js";
7
7
  import { isReadOnly } from "../shared/read-only.js";
8
8
  import path from "path";
@@ -125,6 +125,7 @@ async function findSystemBrowser() {
125
125
  let localPort = 0;
126
126
  let guiPublicUrl = '';
127
127
  const app = new Hono();
128
+ app.use("*", honoGuiGuard());
128
129
  let proxyClient = null;
129
130
  const sseControllers = new Set();
130
131
  const pendingRequests = new Map();
@@ -196,7 +197,15 @@ function setupIpcErrorHandlers() {
196
197
  });
197
198
  return;
198
199
  }
199
- if (msg && (msg.type === "INITIAL_STATE" || msg.type === "CONFIG_UPDATED" || msg.type === "BUNDLES_UPDATED")) {
200
+ const BROADCAST_TYPES = [
201
+ "INITIAL_STATE",
202
+ "CONFIG_UPDATED",
203
+ "BUNDLES_UPDATED",
204
+ "PROJECTS_CHANGED",
205
+ "DOCS_CHANGED",
206
+ "CODE_SOURCES_CHANGED",
207
+ ];
208
+ if (msg && BROADCAST_TYPES.includes(msg.type)) {
200
209
  for (const controller of sseControllers) {
201
210
  try {
202
211
  controller.writeSSE({ data: JSON.stringify(msg), event: "message" });
@@ -216,6 +225,8 @@ const READ_ONLY_WRITE_ROUTES = [
216
225
  { method: "POST", prefix: "/api/images" },
217
226
  { method: "POST", prefix: "/api/configurations/apply" },
218
227
  { method: "POST", prefix: "/api/sync" },
228
+ { method: "POST", prefix: "/api/code-sources/apply" },
229
+ { method: "POST", prefix: "/api/code-roots/apply" },
219
230
  ];
220
231
  app.use("*", async (c, next) => {
221
232
  if (isReadOnly()) {
@@ -358,6 +369,64 @@ app.post("/api/sync", async (c) => {
358
369
  return c.json({ error: result.error }, 500);
359
370
  return c.json(result.data);
360
371
  });
372
+ // --- Code sources ---
373
+ /** Relays one IPC request and answers with its data, or with its error and `errorStatus`. */
374
+ async function relay(c, type, data, timeout, errorStatus = 500) {
375
+ const result = await ipcRequest(type, data, timeout);
376
+ if (result.error)
377
+ return c.json({ error: result.error }, errorStatus);
378
+ return c.json(result.data);
379
+ }
380
+ async function readJson(c) {
381
+ try {
382
+ return await c.req.json();
383
+ }
384
+ catch {
385
+ return {};
386
+ }
387
+ }
388
+ // GitHub projects are checked against their branch: one API round trip each.
389
+ app.get("/api/code-sources", (c) => relay(c, "LIST_CODE_SOURCES", {}, 30000));
390
+ app.post("/api/code-sources/inspect", async (c) => {
391
+ const { repo, branch } = await readJson(c);
392
+ if (!repo)
393
+ return c.json({ error: "repo is required" }, 400);
394
+ return relay(c, "INSPECT_REPOSITORY", { repo, branch }, 30000);
395
+ });
396
+ // Reads every tracked file of the project at the previewed source.
397
+ // `codeRoot`: omitted keeps the project's root, "" means the machine default, else the project's own.
398
+ app.post("/api/code-sources/preview", async (c) => {
399
+ const { project, config, codeRoot } = await readJson(c);
400
+ if (!project || !config)
401
+ return c.json({ error: "project and config are required" }, 400);
402
+ return relay(c, "PREVIEW_CODE_SOURCE", { project, config, codeRoot }, 120000, 400);
403
+ });
404
+ app.post("/api/code-sources/apply", async (c) => {
405
+ const { project, config, codeRoot } = await readJson(c);
406
+ if (!project || !config)
407
+ return c.json({ error: "project and config are required" }, 400);
408
+ return relay(c, "SAVE_CODE_SOURCE", { project, config, codeRoot }, 10000, 400);
409
+ });
410
+ app.get("/api/code-roots", (c) => relay(c, "GET_CODE_ROOTS", {}, 5000));
411
+ app.post("/api/code-sources/search", async (c) => {
412
+ const { query } = await readJson(c);
413
+ return relay(c, "SEARCH_REPOSITORIES", { query: query ?? "" }, 15000);
414
+ });
415
+ app.post("/api/code-roots/inspect", async (c) => {
416
+ const { path: root } = await readJson(c);
417
+ return relay(c, "INSPECT_CODE_ROOT", { path: root ?? "" }, 5000);
418
+ });
419
+ // Without a project, sets the machine default root. An empty path removes the root.
420
+ app.post("/api/code-roots/apply", async (c) => {
421
+ const { path: root, project } = await readJson(c);
422
+ if (typeof root !== "string")
423
+ return c.json({ error: "path is required" }, 400);
424
+ return relay(c, "SET_CODE_ROOT", { path: root, project }, 5000, 400);
425
+ });
426
+ // The token is read from MCP_DOCUMENTOR_CODE_TOKEN only: the GUI shows its status, never sets it.
427
+ app.get("/api/github-token", (c) => relay(c, "GET_GITHUB_TOKEN", {}, 15000));
428
+ app.get("/api/code-source-migration", (c) => relay(c, "GET_CODE_SOURCE_MIGRATION", {}, 5000));
429
+ app.post("/api/code-source-migration/ack", (c) => relay(c, "ACK_CODE_SOURCE_MIGRATION", {}, 5000));
361
430
  app.get("/api/projects", async (c) => {
362
431
  const result = await ipcRequest("LIST_PROJECTS");
363
432
  if (result.error)
@@ -757,7 +826,7 @@ app.get("/*", (c, next) => {
757
826
  */
758
827
  function listen(port, publicUrl, isProxied) {
759
828
  logger.info(`Starting Hono server on port ${port}...`);
760
- const serverInstance = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => {
829
+ const serverInstance = serve({ fetch: app.fetch, port, hostname: GUI_LISTEN_HOST }, (info) => {
761
830
  logger.info(`Dashboard server listening on port ${info.port}`);
762
831
  safeSend(IPCMessageSchema.parse({
763
832
  type: "READY",