@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
@@ -11,4 +11,5 @@ export const WRITE_TOOLS = new Set([
11
11
  // Sync: pull_docs and sync_status stay allowed (read-only consumer machines
12
12
  // still pull updates); these mutate local/remote state and are blocked.
13
13
  "push_docs", "sync_docs", "resolve_sync_conflict",
14
+ "set_code_source", "set_code_root",
14
15
  ]);
@@ -58,7 +58,7 @@ const GENERATED_VARS = [
58
58
  ["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
59
59
  ["MCP_DATA_DIR", "répertoire de données (défaut : celui du package)"],
60
60
  ["MCP_LOG_DIR", "répertoire des logs"],
61
- ["MCP_DOCUMENTOR_CODE_ROOT", "racine du code (pré-remplie avec le répertoire courant)"],
61
+ ["MCP_DOCUMENTOR_CODE_ROOT", "racine du code par défaut (pré-remplie avec le répertoire courant), reprise au premier démarrage puis réglable dans la GUI"],
62
62
  ["MCP_DOCUMENTOR_OPENAI_API_KEY", "clé OpenAI (active search_docs)"],
63
63
  ["MCP_DOCUMENTOR_OPENAI_MODEL", "modèle d'embedding"],
64
64
  ["MCP_DOCUMENTOR_SYNC_REMOTE", "URL HTTPS du repo privé de sync GitHub"],
@@ -66,10 +66,7 @@ const GENERATED_VARS = [
66
66
  ["MCP_DOCUMENTOR_SYNC_TOKEN", "PAT GitHub fine-grained pour la sync"],
67
67
  ];
68
68
  const OPTIONAL_VARS = [
69
- ["MCP_DOCUMENTOR_CODE_ROOTS", 'code roots par projet, ex: {"my-proj":"/srv/my-proj"}'],
70
- ["MCP_DOCUMENTOR_CODE_REPO", 'repo GitHub suivi au lieu de la copie locale : "owner/repo#main", ou une map JSON par projet {"my-proj":"owner/repo#main","legacy":"local"}'],
71
- ["MCP_DOCUMENTOR_CODE_BRANCH", "branche suivie quand le repo n'a pas de suffixe #branche"],
72
- ["MCP_DOCUMENTOR_CODE_TOKEN", "PAT GitHub pour lire le code (défaut : MCP_DOCUMENTOR_SYNC_TOKEN)"],
69
+ ["MCP_DOCUMENTOR_CODE_TOKEN", "PAT GitHub pour lire le code des projets suivis sur GitHub (réglable uniquement ici)"],
73
70
  ["READ_ONLY", "1/true pour désactiver toutes les écritures"],
74
71
  ["SYNC_PULL_ON_START", "false pour désactiver le pull au démarrage"],
75
72
  ];
@@ -1,31 +1,23 @@
1
1
  import type { TrackedSection } from "../../shared/schemas/doc.schema.js";
2
2
  import { type FileContent } from "./section-tracker.js";
3
- import { GitHubCodeApi } from "./github-code-api.js";
3
+ import { GitHubCodeApi, type RepoSpec } from "./github-code-api.js";
4
+ import type { CodeSourceConfig } from "./project-settings.js";
4
5
  /**
5
6
  * Where a project's source code is read from when checking or snapshotting documentation.
6
7
  *
7
8
  * Two backends:
8
9
  *
9
- * - **local** — the working copy on this machine, resolved through the code roots. This is the
10
- * original behaviour and stays the default: it sees uncommitted work, and it sees whichever
11
- * branch happens to be checked out.
10
+ * - **local** — the working copy on this machine, resolved through the code roots. It sees
11
+ * uncommitted work, and whichever branch happens to be checked out.
12
12
  * - **github** — one branch of one repository, read over the GitHub API. The documentation then
13
- * tracks *published* code: switching branches locally, or editing without committing, no longer
14
- * makes docs look stale, and a server that has no copy of the codebase at all (an MCP gateway)
13
+ * tracks *published* code: switching branches locally, or editing without committing, does not
14
+ * make docs look stale, and a server that has no copy of the codebase at all (an MCP gateway)
15
15
  * can still verify freshness.
16
16
  *
17
- * A project uses the GitHub backend only when a repository is configured for it. With no
18
- * repository set, everything behaves exactly as before.
19
- *
20
- * MCP_DOCUMENTOR_CODE_REPO the repository to read code from, in either of two shapes:
21
- * a single spec — "owner/repo", a GitHub URL, with an optional
22
- * "#branch" suffix — applied to every project, or a JSON map of
23
- * per-project specs, e.g.
24
- * {"web":"Imena/web#main","legacy":"local"}
25
- * — the value "local" opts a project back out to its working copy
26
- * MCP_DOCUMENTOR_CODE_BRANCH branch used when a repository carries no "#branch" suffix
27
- * (default: the repository's own default branch)
28
- * MCP_DOCUMENTOR_CODE_TOKEN GitHub token; falls back to MCP_DOCUMENTOR_SYNC_TOKEN
17
+ * Each project's choice is read from its settings (see `project-settings`) and the GitHub token
18
+ * from the machine-local state, both through the provider installed with `setCodeSourceProvider`.
19
+ * Sources are memoised per project; `invalidateCodeSources` drops them when a setting changes, so
20
+ * the next operation reads the new configuration.
29
21
  */
30
22
  /** One file as it exists at the source. */
31
23
  export interface SourceFile {
@@ -38,7 +30,7 @@ export interface SourceFile {
38
30
  blobId?: string;
39
31
  }
40
32
  export interface CodeSource {
41
- readonly kind: "local" | "github";
33
+ readonly kind: "local" | "github" | "unavailable";
42
34
  /** Short description of where this source reads from, for reports and error messages. */
43
35
  readonly label: string;
44
36
  /**
@@ -56,9 +48,28 @@ export interface CodeSource {
56
48
  /** Converts a path supplied by an agent into the portable form persisted in a doc. */
57
49
  toStoredPath(inputPath: string): Promise<string>;
58
50
  }
59
- /** Test-only / config-reload: drops the memoised environment snapshot and every cached source. */
60
- export declare function resetCodeSourceCache(): void;
61
- /** The code source a project reads from. Memoised — the configuration is environment-fixed. */
51
+ /** Where the code sources read their configuration. */
52
+ export interface CodeSourceProvider {
53
+ /** The project's code source setting. Throws when the settings cannot be read. */
54
+ codeSourceConfig(project: string): CodeSourceConfig;
55
+ /** The GitHub token used to read repositories, if one is set. */
56
+ githubToken(): string | undefined;
57
+ }
58
+ export declare function setCodeSourceProvider(value: CodeSourceProvider): void;
59
+ /** The GitHub token used to read repositories, if one is set. */
60
+ export declare function getGitHubToken(): string | undefined;
61
+ /** Drops the memoised source of one project, or of every project and every GitHub client. */
62
+ export declare function invalidateCodeSources(project?: string): void;
63
+ /** The GitHub repository spec a configuration reads, or undefined for the local working copy. */
64
+ export declare function toRepoSpec(config: CodeSourceConfig): RepoSpec | undefined;
65
+ /**
66
+ * Builds the source a configuration describes, without memoising it. Used to preview a setting
67
+ * before it is saved; `codeRoot` then pins the root a local source resolves paths against.
68
+ */
69
+ export declare function createCodeSource(project: string, config: CodeSourceConfig, codeRoot?: {
70
+ root: string | undefined;
71
+ }): CodeSource;
72
+ /** The code source a project reads from. Memoised until `invalidateCodeSources`. */
62
73
  export declare function getCodeSource(project: string): CodeSource;
63
74
  /**
64
75
  * Where each tracked section of a file currently sits, as 1-indexed line ranges — `undefined`
@@ -2,21 +2,30 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { createLogger } from "@imenam/mcp-gui-interface";
4
4
  import { bytesToFileContent, findAndHashSection } from "./section-tracker.js";
5
- import { getCodeRoot, toAbsolutePath, toStoredPath as toStoredPathLocal } from "./path-resolver.js";
6
- import { GitHubCodeApi, parseRepoSpec, formatRepoSpec } from "./github-code-api.js";
5
+ import { getCodeRoot, resolveAgainstRoot, toStoredPath as toStoredPathLocal } from "./path-resolver.js";
6
+ import { GitHubCodeApi, formatRepoSpec } from "./github-code-api.js";
7
7
  const logger = createLogger("CodeSource");
8
8
  class LocalCodeSource {
9
9
  project;
10
+ fixedRoot;
10
11
  kind = "local";
11
- constructor(project) {
12
+ /**
13
+ * `fixedRoot` pins the code root instead of reading the project's current one — used to preview
14
+ * a root before it is saved.
15
+ */
16
+ constructor(project, fixedRoot) {
12
17
  this.project = project;
18
+ this.fixedRoot = fixedRoot;
19
+ }
20
+ get root() {
21
+ return this.fixedRoot ? this.fixedRoot.root : getCodeRoot(this.project);
13
22
  }
14
23
  get label() {
15
- const root = getCodeRoot(this.project);
24
+ const root = this.root;
16
25
  return root ? `local working copy (${root})` : "local working copy";
17
26
  }
18
27
  async read(storedPath) {
19
- const absolutePath = toAbsolutePath(storedPath, this.project);
28
+ const absolutePath = resolveAgainstRoot(storedPath, this.root);
20
29
  if (!absolutePath || !fs.existsSync(absolutePath))
21
30
  return undefined;
22
31
  return {
@@ -31,10 +40,10 @@ class LocalCodeSource {
31
40
  async toStoredPath(inputPath) {
32
41
  // A relative input means "relative to the project's code root" — the same convention used
33
42
  // when a stored path is read back. Never the server's cwd.
34
- const absolute = toAbsolutePath(inputPath, this.project);
43
+ const absolute = resolveAgainstRoot(inputPath, this.root);
35
44
  if (!absolute) {
36
- throw new Error(`the path is relative but no code root is configured for project "${this.project}". ` +
37
- `Provide an absolute path or set MCP_DOCUMENTOR_CODE_ROOT.`);
45
+ throw new Error(`the path is relative but no code root is set for project "${this.project}" on this machine. ` +
46
+ `Provide an absolute path, or set a code root with set_code_root or in the project settings.`);
38
47
  }
39
48
  return toStoredPathLocal(absolute, this.project);
40
49
  }
@@ -80,7 +89,7 @@ class GitHubCodeSource {
80
89
  if (!repoPath) {
81
90
  throw new Error(`Cannot map "${inputPath}" to a path inside ${this.api.label}. ` +
82
91
  `Provide the path relative to the repository root, or set a code root for project "${this.project}" ` +
83
- `(MCP_DOCUMENTOR_CODE_ROOT / MCP_DOCUMENTOR_CODE_ROOTS) so absolute paths can be stripped down to it.`);
92
+ `(set_code_root, or the project settings) so absolute paths can be stripped down to it.`);
84
93
  }
85
94
  return repoPath;
86
95
  }
@@ -124,81 +133,95 @@ class GitHubCodeSource {
124
133
  return best;
125
134
  }
126
135
  }
127
- let settings;
128
- const sourcesByProject = new Map();
129
- const apisByRepo = new Map();
130
- function loadSettings() {
131
- if (settings)
132
- return settings;
133
- // One setting, two shapes. A repository is not a shared parent the way a code root is — it
134
- // names exactly one codebase — so an instance serving several projects needs one spec per
135
- // project, while a single-project or monorepo instance needs just the spec. A leading "{"
136
- // tells the two apart unambiguously, since no repository spec starts with one.
137
- const raw = process.env.MCP_DOCUMENTOR_CODE_REPO?.trim() || undefined;
138
- const byProject = {};
139
- let defaultSpec;
140
- if (raw?.startsWith("{")) {
141
- try {
142
- const parsed = JSON.parse(raw);
143
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
144
- for (const [proj, spec] of Object.entries(parsed)) {
145
- if (typeof spec === "string" && spec.trim() !== "")
146
- byProject[proj] = spec.trim();
147
- }
148
- }
149
- else {
150
- logger.warn("MCP_DOCUMENTOR_CODE_REPO is not a JSON object, ignoring it");
151
- }
152
- }
153
- catch {
154
- logger.warn("MCP_DOCUMENTOR_CODE_REPO looks like a JSON map but could not be parsed, ignoring it");
155
- }
136
+ /**
137
+ * A project whose settings cannot be read. Every access fails with the reason, so freshness
138
+ * reports the project as unverifiable and names what to fix.
139
+ */
140
+ class UnavailableCodeSource {
141
+ reason;
142
+ kind = "unavailable";
143
+ constructor(reason) {
144
+ this.reason = reason;
145
+ }
146
+ get label() {
147
+ return "unavailable code source";
148
+ }
149
+ async read() {
150
+ throw new Error(this.reason);
151
+ }
152
+ async contentId() {
153
+ throw new Error(this.reason);
156
154
  }
157
- else {
158
- defaultSpec = raw;
155
+ async toStoredPath() {
156
+ throw new Error(this.reason);
159
157
  }
160
- settings = {
161
- defaultSpec,
162
- byProject,
163
- defaultBranch: process.env.MCP_DOCUMENTOR_CODE_BRANCH?.trim() || undefined,
164
- token: process.env.MCP_DOCUMENTOR_CODE_TOKEN?.trim() || process.env.MCP_DOCUMENTOR_SYNC_TOKEN?.trim() || undefined,
165
- };
166
- return settings;
167
158
  }
168
- /** Test-only / config-reload: drops the memoised environment snapshot and every cached source. */
169
- export function resetCodeSourceCache() {
170
- settings = undefined;
159
+ let provider;
160
+ const sourcesByProject = new Map();
161
+ const apisByRepo = new Map();
162
+ export function setCodeSourceProvider(value) {
163
+ provider = value;
164
+ invalidateCodeSources();
165
+ }
166
+ function requireProvider() {
167
+ if (!provider)
168
+ throw new Error("code sources are read before setCodeSourceProvider was called");
169
+ return provider;
170
+ }
171
+ /** The GitHub token used to read repositories, if one is set. */
172
+ export function getGitHubToken() {
173
+ return requireProvider().githubToken();
174
+ }
175
+ /** Drops the memoised source of one project, or of every project and every GitHub client. */
176
+ export function invalidateCodeSources(project) {
177
+ if (project !== undefined) {
178
+ sourcesByProject.delete(project);
179
+ return;
180
+ }
171
181
  sourcesByProject.clear();
172
182
  apisByRepo.clear();
173
183
  }
174
- function buildSource(project) {
175
- const config = loadSettings();
176
- const raw = config.byProject[project] ?? config.defaultSpec;
177
- if (!raw || raw.toLowerCase() === "local")
178
- return new LocalCodeSource(project);
179
- let spec;
180
- try {
181
- spec = parseRepoSpec(raw);
182
- }
183
- catch (err) {
184
- const message = err instanceof Error ? err.message : String(err);
185
- logger.warn(`Code repository for project "${project}" is unusable, falling back to the local working copy: ${message}`);
186
- return new LocalCodeSource(project);
187
- }
188
- if (!spec.ref && config.defaultBranch)
189
- spec = { ...spec, ref: config.defaultBranch };
184
+ /** The GitHub repository spec a configuration reads, or undefined for the local working copy. */
185
+ export function toRepoSpec(config) {
186
+ if (config.kind !== "github")
187
+ return undefined;
188
+ const [owner, repo] = config.repo.split("/");
189
+ return { owner, repo, ref: config.branch };
190
+ }
191
+ /**
192
+ * Builds the source a configuration describes, without memoising it. Used to preview a setting
193
+ * before it is saved; `codeRoot` then pins the root a local source resolves paths against.
194
+ */
195
+ export function createCodeSource(project, config, codeRoot) {
196
+ const spec = toRepoSpec(config);
197
+ if (!spec)
198
+ return new LocalCodeSource(project, codeRoot);
190
199
  // One API client per (repo, ref): its tree and blob caches are then shared by every project
191
200
  // pointing at the same branch.
192
201
  const key = formatRepoSpec(spec);
193
202
  let api = apisByRepo.get(key);
194
203
  if (!api) {
195
- api = new GitHubCodeApi(spec, config.token);
204
+ api = new GitHubCodeApi(spec, requireProvider().githubToken());
196
205
  apisByRepo.set(key, api);
197
- logger.info(`Project "${project}" reads its code from GitHub ${key}`);
198
206
  }
199
207
  return new GitHubCodeSource(api, project);
200
208
  }
201
- /** The code source a project reads from. Memoised — the configuration is environment-fixed. */
209
+ function buildSource(project) {
210
+ let config;
211
+ try {
212
+ config = requireProvider().codeSourceConfig(project);
213
+ }
214
+ catch (err) {
215
+ const message = err instanceof Error ? err.message : String(err);
216
+ logger.error(`Code source of project "${project}" cannot be read: ${message}`);
217
+ return new UnavailableCodeSource(message);
218
+ }
219
+ const source = createCodeSource(project, config);
220
+ if (source.kind === "github")
221
+ logger.info(`Project "${project}" reads its code from ${source.label}`);
222
+ return source;
223
+ }
224
+ /** The code source a project reads from. Memoised until `invalidateCodeSources`. */
202
225
  export function getCodeSource(project) {
203
226
  let source = sourcesByProject.get(project);
204
227
  if (!source) {
@@ -24,6 +24,34 @@ export declare class GitHubApiError extends Error {
24
24
  readonly status?: number | undefined;
25
25
  constructor(message: string, status?: number | undefined);
26
26
  }
27
+ export interface RepositoryInfo {
28
+ fullName: string;
29
+ private: boolean;
30
+ defaultBranch: string;
31
+ }
32
+ export interface BranchHead {
33
+ sha: string;
34
+ message: string;
35
+ date: string;
36
+ }
37
+ /** Visibility and default branch of a repository. */
38
+ export declare function getRepositoryInfo(owner: string, repo: string, token: string | undefined): Promise<RepositoryInfo>;
39
+ /** Branch names of a repository, in GitHub's order. */
40
+ export declare function listBranches(owner: string, repo: string, token: string | undefined): Promise<string[]>;
41
+ /** The commit a branch points at, with its first message line. */
42
+ export declare function getBranchHead(owner: string, repo: string, branch: string, token: string | undefined): Promise<BranchHead>;
43
+ /** Login of the account a token belongs to. */
44
+ export declare function getTokenLogin(token: string): Promise<string>;
45
+ export interface RepositorySearchResult {
46
+ fullName: string;
47
+ private: boolean;
48
+ description: string;
49
+ }
50
+ /**
51
+ * Repositories whose name matches `term`, best matches first. "owner/name" narrows the search to
52
+ * that owner. With a token, private repositories the token can see are included.
53
+ */
54
+ export declare function searchRepositories(term: string, token: string | undefined, limit: number): Promise<RepositorySearchResult[]>;
27
55
  /**
28
56
  * Read-only access to a repository's file tree over the GitHub API.
29
57
  *
@@ -39,6 +39,96 @@ export class GitHubApiError extends Error {
39
39
  this.name = "GitHubApiError";
40
40
  }
41
41
  }
42
+ /**
43
+ * One GET against the GitHub REST API. `label` names what was asked for in error messages.
44
+ * 404 and 422 both mean "nothing at that address": 422 is what a valid repository returns for a
45
+ * ref that does not exist.
46
+ */
47
+ async function githubRequest(apiPath, token, label) {
48
+ const headers = {
49
+ Accept: "application/vnd.github+json",
50
+ "X-GitHub-Api-Version": "2022-11-28",
51
+ "User-Agent": "mcp-documentor",
52
+ };
53
+ if (token)
54
+ headers.Authorization = `Bearer ${token}`;
55
+ let response;
56
+ try {
57
+ response = await fetch(`${API_BASE}${apiPath}`, { headers });
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ throw new GitHubApiError(`network error contacting GitHub (${message})`);
62
+ }
63
+ if (response.status === 404 || response.status === 422) {
64
+ throw new GitHubApiError(`not found: ${label} (repository, branch or path missing — or the token cannot see this repository)`, response.status);
65
+ }
66
+ if (response.status === 401 || response.status === 403) {
67
+ const rateLimited = response.headers.get("x-ratelimit-remaining") === "0";
68
+ throw new GitHubApiError(rateLimited
69
+ ? `GitHub API rate limit exhausted for ${label}`
70
+ : `access denied to ${label} — check the GitHub token in the project settings`, response.status);
71
+ }
72
+ if (!response.ok) {
73
+ throw new GitHubApiError(`GitHub API returned ${response.status} for ${apiPath}`, response.status);
74
+ }
75
+ return response.json();
76
+ }
77
+ /** Visibility and default branch of a repository. */
78
+ export async function getRepositoryInfo(owner, repo, token) {
79
+ const data = await githubRequest(`/repos/${owner}/${repo}`, token, `${owner}/${repo}`);
80
+ return {
81
+ fullName: data.full_name,
82
+ private: !!data.private,
83
+ defaultBranch: data.default_branch,
84
+ };
85
+ }
86
+ /** Upper bound on listed branches: ten pages of a hundred. */
87
+ const MAX_BRANCH_PAGES = 10;
88
+ /** Branch names of a repository, in GitHub's order. */
89
+ export async function listBranches(owner, repo, token) {
90
+ const names = [];
91
+ for (let page = 1; page <= MAX_BRANCH_PAGES; page++) {
92
+ const batch = await githubRequest(`/repos/${owner}/${repo}/branches?per_page=100&page=${page}`, token, `${owner}/${repo}`);
93
+ if (!Array.isArray(batch))
94
+ break;
95
+ for (const branch of batch)
96
+ names.push(branch.name);
97
+ if (batch.length < 100)
98
+ break;
99
+ }
100
+ return names;
101
+ }
102
+ /** The commit a branch points at, with its first message line. */
103
+ export async function getBranchHead(owner, repo, branch, token) {
104
+ const commit = await githubRequest(`/repos/${owner}/${repo}/commits/${encodeURIComponent(branch)}`, token, `${owner}/${repo}@${branch}`);
105
+ return {
106
+ sha: commit.sha,
107
+ message: String(commit.commit?.message ?? "").split("\n")[0],
108
+ date: commit.commit?.committer?.date ?? commit.commit?.author?.date ?? "",
109
+ };
110
+ }
111
+ /** Login of the account a token belongs to. */
112
+ export async function getTokenLogin(token) {
113
+ const user = await githubRequest("/user", token, "the authenticated user");
114
+ return user.login;
115
+ }
116
+ /**
117
+ * Repositories whose name matches `term`, best matches first. "owner/name" narrows the search to
118
+ * that owner. With a token, private repositories the token can see are included.
119
+ */
120
+ export async function searchRepositories(term, token, limit) {
121
+ const slash = term.indexOf("/");
122
+ const owner = slash >= 0 ? term.slice(0, slash).trim() : "";
123
+ const name = slash >= 0 ? term.slice(slash + 1).trim() : term.trim();
124
+ const qualifiers = [name, "in:name", ...(owner ? [`user:${owner}`] : [])].filter(Boolean).join(" ");
125
+ const data = await githubRequest(`/search/repositories?q=${encodeURIComponent(qualifiers)}&per_page=${limit}`, token, `repositories matching "${term}"`);
126
+ return (data.items ?? []).map((item) => ({
127
+ fullName: item.full_name,
128
+ private: !!item.private,
129
+ description: item.description ?? "",
130
+ }));
131
+ }
42
132
  /**
43
133
  * Read-only access to a repository's file tree over the GitHub API.
44
134
  *
@@ -68,37 +158,8 @@ export class GitHubCodeApi {
68
158
  get label() {
69
159
  return formatRepoSpec(this.spec);
70
160
  }
71
- async request(path) {
72
- const headers = {
73
- Accept: "application/vnd.github+json",
74
- "X-GitHub-Api-Version": "2022-11-28",
75
- "User-Agent": "mcp-documentor",
76
- };
77
- if (this.token)
78
- headers.Authorization = `Bearer ${this.token}`;
79
- let response;
80
- try {
81
- response = await fetch(`${API_BASE}${path}`, { headers });
82
- }
83
- catch (err) {
84
- const message = err instanceof Error ? err.message : String(err);
85
- throw new GitHubApiError(`network error contacting GitHub (${message})`);
86
- }
87
- // 422 is what a valid repository returns for a ref that does not exist, so a mistyped
88
- // branch lands here rather than on 404 — both mean "nothing to read at that address".
89
- if (response.status === 404 || response.status === 422) {
90
- throw new GitHubApiError(`not found: ${this.label} (repository, branch or path missing — or the token cannot see this repository)`, response.status);
91
- }
92
- if (response.status === 401 || response.status === 403) {
93
- const rateLimited = response.headers.get("x-ratelimit-remaining") === "0";
94
- throw new GitHubApiError(rateLimited
95
- ? `GitHub API rate limit exhausted for ${this.label}`
96
- : `access denied to ${this.label} — check MCP_DOCUMENTOR_CODE_TOKEN / MCP_DOCUMENTOR_SYNC_TOKEN`, response.status);
97
- }
98
- if (!response.ok) {
99
- throw new GitHubApiError(`GitHub API returned ${response.status} for ${path}`, response.status);
100
- }
101
- return response.json();
161
+ request(path) {
162
+ return githubRequest(path, this.token, this.label);
102
163
  }
103
164
  /**
104
165
  * The commit the configured ref points at right now.
@@ -0,0 +1,49 @@
1
+ /** What a migration report entry describes, which sets how the GUI shows it. */
2
+ export type CodeSourceMigrationKind = "github" | "local" | "code-root";
3
+ /** One line of the code-source migration report: what a project became, and from which variables. */
4
+ export interface CodeSourceMigrationEntry {
5
+ /** Project name, or undefined for the machine-wide default code root. */
6
+ project?: string;
7
+ kind: CodeSourceMigrationKind;
8
+ status: "imported" | "skipped" | "error";
9
+ /** What the project is configured as now, or why nothing was imported. */
10
+ detail: string;
11
+ /** Environment variables the entry was read from, without the MCP_DOCUMENTOR_ prefix. */
12
+ from: string;
13
+ }
14
+ export interface CodeSourceMigrationReport {
15
+ migratedAt: string;
16
+ entries: CodeSourceMigrationEntry[];
17
+ /** Variables found in the environment, which the server no longer reads. */
18
+ variables: string[];
19
+ /** Set once the user has acknowledged the report in the GUI. */
20
+ seen: boolean;
21
+ }
22
+ /** Code roots of this machine: a default one, and per-project overrides. */
23
+ export interface CodeRoots {
24
+ default?: string;
25
+ byProject: Record<string, string>;
26
+ }
27
+ /**
28
+ * Machine-local state, in `<data dir>/local-state.json`. The sync repository's .gitignore excludes
29
+ * this file, so what it holds never leaves the machine: the code roots — where this machine keeps
30
+ * each project's repository — and the report of the one-time migration of the code-source
31
+ * environment variables.
32
+ */
33
+ export declare class LocalState {
34
+ private readonly dataDir;
35
+ constructor(dataDir: string);
36
+ private get filePath();
37
+ private read;
38
+ private write;
39
+ getCodeRoots(): CodeRoots;
40
+ /** The project's own root, else the machine default. */
41
+ getCodeRoot(project: string): string | undefined;
42
+ /** Sets the machine default root, or removes it when `root` is undefined. */
43
+ setDefaultCodeRoot(root: string | undefined): void;
44
+ /** Sets a project's own root, or removes it — the project then uses the default — when undefined. */
45
+ setProjectCodeRoot(project: string, root: string | undefined): void;
46
+ getMigrationReport(): CodeSourceMigrationReport | undefined;
47
+ setMigrationReport(report: CodeSourceMigrationReport): void;
48
+ markMigrationReportSeen(): void;
49
+ }
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ /**
4
+ * Machine-local state, in `<data dir>/local-state.json`. The sync repository's .gitignore excludes
5
+ * this file, so what it holds never leaves the machine: the code roots — where this machine keeps
6
+ * each project's repository — and the report of the one-time migration of the code-source
7
+ * environment variables.
8
+ */
9
+ export class LocalState {
10
+ dataDir;
11
+ constructor(dataDir) {
12
+ this.dataDir = dataDir;
13
+ }
14
+ get filePath() {
15
+ return path.join(this.dataDir, "local-state.json");
16
+ }
17
+ read() {
18
+ if (!fs.existsSync(this.filePath))
19
+ return {};
20
+ try {
21
+ const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
22
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
23
+ }
24
+ catch {
25
+ throw new Error(`${this.filePath} is not valid JSON — fix or delete it.`);
26
+ }
27
+ }
28
+ write(state) {
29
+ fs.mkdirSync(this.dataDir, { recursive: true });
30
+ fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf-8");
31
+ }
32
+ getCodeRoots() {
33
+ const roots = this.read().codeRoots;
34
+ return { default: roots?.default, byProject: { ...(roots?.byProject ?? {}) } };
35
+ }
36
+ /** The project's own root, else the machine default. */
37
+ getCodeRoot(project) {
38
+ const roots = this.getCodeRoots();
39
+ return roots.byProject[project] ?? roots.default;
40
+ }
41
+ /** Sets the machine default root, or removes it when `root` is undefined. */
42
+ setDefaultCodeRoot(root) {
43
+ const state = this.read();
44
+ const roots = { byProject: {}, ...state.codeRoots };
45
+ if (root)
46
+ roots.default = root;
47
+ else
48
+ delete roots.default;
49
+ state.codeRoots = roots;
50
+ this.write(state);
51
+ }
52
+ /** Sets a project's own root, or removes it — the project then uses the default — when undefined. */
53
+ setProjectCodeRoot(project, root) {
54
+ const state = this.read();
55
+ const byProject = { ...(state.codeRoots?.byProject ?? {}) };
56
+ if (root)
57
+ byProject[project] = root;
58
+ else
59
+ delete byProject[project];
60
+ state.codeRoots = { ...state.codeRoots, byProject };
61
+ this.write(state);
62
+ }
63
+ getMigrationReport() {
64
+ return this.read().codeSourceMigration;
65
+ }
66
+ setMigrationReport(report) {
67
+ const state = this.read();
68
+ state.codeSourceMigration = report;
69
+ this.write(state);
70
+ }
71
+ markMigrationReportSeen() {
72
+ const state = this.read();
73
+ if (!state.codeSourceMigration)
74
+ return;
75
+ state.codeSourceMigration.seen = true;
76
+ this.write(state);
77
+ }
78
+ }
@@ -0,0 +1,17 @@
1
+ import type { StorageEngine } from "./storage-engine.js";
2
+ import type { LocalState } from "./local-state.js";
3
+ /**
4
+ * Moves the code-source environment variables into the project settings and the machine-local
5
+ * code roots, once per machine.
6
+ *
7
+ * - MCP_DOCUMENTOR_CODE_REPO (+ MCP_DOCUMENTOR_CODE_BRANCH for specs without "#branch") becomes
8
+ * the code source of each project it names. A project that already has a code source — written
9
+ * by another machine and pulled — keeps it.
10
+ * - MCP_DOCUMENTOR_CODE_ROOTS becomes the code root of each project it names, on this machine.
11
+ * - MCP_DOCUMENTOR_CODE_ROOT seeds the default code root (see `seedDefaultCodeRoot`); it is not
12
+ * one of the variables to remove.
13
+ *
14
+ * The outcome is recorded as a report in the local state; the GUI shows it until acknowledged.
15
+ * Once recorded, the migrated variables are never read again: their presence is only logged.
16
+ */
17
+ export declare function migrateCodeSources(storageEngine: StorageEngine, localState: LocalState): void;