@imenam/mcp-documentor 1.0.72 → 1.0.76

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 (74) hide show
  1. package/README.md +271 -259
  2. package/dist/shared/schemas/doc.schema.d.ts +1 -4
  3. package/dist/shared/schemas/doc.schema.js +1 -4
  4. package/dist/src/cli/setup-mcp.d.ts +20 -1
  5. package/dist/src/cli/setup-mcp.js +65 -6
  6. package/dist/src/core/conflict-inspector.d.ts +13 -0
  7. package/dist/src/core/conflict-inspector.js +17 -0
  8. package/dist/src/core/git-sync.d.ts +11 -4
  9. package/dist/src/core/git-sync.js +33 -22
  10. package/dist/src/core/storage-engine.d.ts +14 -15
  11. package/dist/src/core/storage-engine.js +15 -51
  12. package/dist/src/gui-worker.js +91 -34
  13. package/dist/src/index.js +100 -110
  14. package/dist/src/tools/check-links.js +1 -1
  15. package/dist/src/tools/conflicts.js +50 -5
  16. package/dist/src/tools/list-projects.js +7 -5
  17. package/dist/src/tools/sync.js +14 -7
  18. package/dist/src/version.d.ts +1 -1
  19. package/dist/src/version.js +1 -1
  20. package/gui/dist/assets/{arc-KWKRz6LO.js → arc-CsM5OJTM.js} +1 -1
  21. package/gui/dist/assets/{architectureDiagram-3BPJPVTR-Cjfl98ZS.js → architectureDiagram-3BPJPVTR-BwIu1llT.js} +1 -1
  22. package/gui/dist/assets/{blockDiagram-GPEHLZMM-CtPUrnse.js → blockDiagram-GPEHLZMM-BVijVnPI.js} +1 -1
  23. package/gui/dist/assets/{c4Diagram-AAUBKEIU-Dh5_7IZy.js → c4Diagram-AAUBKEIU-D-rcZUZu.js} +1 -1
  24. package/gui/dist/assets/channel-DKPwwuQf.js +1 -0
  25. package/gui/dist/assets/{chunk-2J33WTMH-44T5JbXy.js → chunk-2J33WTMH-ByfTGREe.js} +1 -1
  26. package/gui/dist/assets/{chunk-4BX2VUAB-D1fEQ0Ng.js → chunk-4BX2VUAB-B_5oCXyz.js} +1 -1
  27. package/gui/dist/assets/{chunk-55IACEB6-M2lEB2Vn.js → chunk-55IACEB6-Bc2Kq9HD.js} +1 -1
  28. package/gui/dist/assets/{chunk-727SXJPM-B3A_Fp4f.js → chunk-727SXJPM-CVwmKEMQ.js} +1 -1
  29. package/gui/dist/assets/{chunk-AQP2D5EJ-CqkJXv3c.js → chunk-AQP2D5EJ-BkMuEFxD.js} +1 -1
  30. package/gui/dist/assets/{chunk-FMBD7UC4-Iyj_Ky8e.js → chunk-FMBD7UC4-CU7EDl9I.js} +1 -1
  31. package/gui/dist/assets/{chunk-ND2GUHAM-DE5R-C9U.js → chunk-ND2GUHAM-1b34Dtkn.js} +1 -1
  32. package/gui/dist/assets/{chunk-QZHKN3VN-BekBU-YP.js → chunk-QZHKN3VN-DnUfMecI.js} +1 -1
  33. package/gui/dist/assets/classDiagram-4FO5ZUOK-CnCvVGW9.js +1 -0
  34. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CnCvVGW9.js +1 -0
  35. package/gui/dist/assets/{cose-bilkent-S5V4N54A-Dw2zOlxr.js → cose-bilkent-S5V4N54A-D9OBrxKr.js} +1 -1
  36. package/gui/dist/assets/{dagre-BM42HDAG-ebVZNyxI.js → dagre-BM42HDAG-zDqwL8xs.js} +1 -1
  37. package/gui/dist/assets/{diagram-2AECGRRQ-Bh5FUKy0.js → diagram-2AECGRRQ-ejbP8WIj.js} +1 -1
  38. package/gui/dist/assets/{diagram-5GNKFQAL-YJJTJy-Q.js → diagram-5GNKFQAL-Cv9WmtFO.js} +1 -1
  39. package/gui/dist/assets/{diagram-KO2AKTUF-D161kBvB.js → diagram-KO2AKTUF-BI0ybJiN.js} +1 -1
  40. package/gui/dist/assets/{diagram-LMA3HP47-Drcmx9J8.js → diagram-LMA3HP47-DlVGttLq.js} +1 -1
  41. package/gui/dist/assets/{diagram-OG6HWLK6-DRrxnW_H.js → diagram-OG6HWLK6-DjoAH-Fq.js} +1 -1
  42. package/gui/dist/assets/{erDiagram-TEJ5UH35-FrH4iEa3.js → erDiagram-TEJ5UH35-BVQHVGK4.js} +1 -1
  43. package/gui/dist/assets/{flowDiagram-I6XJVG4X-BMxSElvH.js → flowDiagram-I6XJVG4X-oZP2Z-6T.js} +1 -1
  44. package/gui/dist/assets/{ganttDiagram-6RSMTGT7-H09FGGUr.js → ganttDiagram-6RSMTGT7-dy4QyEOP.js} +1 -1
  45. package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-DLhPfXNp.js → gitGraphDiagram-PVQCEYII-BxgLy7Ln.js} +1 -1
  46. package/gui/dist/assets/{index-CdGsMXYw.js → index-Dtv0slFQ.js} +97 -97
  47. package/gui/dist/assets/index-QkCDSQ2A.css +1 -0
  48. package/gui/dist/assets/{infoDiagram-5YYISTIA-CV8ctX8t.js → infoDiagram-5YYISTIA-DNOdinpR.js} +1 -1
  49. package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-BeuHUSPO.js → ishikawaDiagram-YF4QCWOH-C4pQ434G.js} +1 -1
  50. package/gui/dist/assets/{journeyDiagram-JHISSGLW-CfAHwYXQ.js → journeyDiagram-JHISSGLW-CBrVYW94.js} +1 -1
  51. package/gui/dist/assets/{kanban-definition-UN3LZRKU-CFF4KWbn.js → kanban-definition-UN3LZRKU-Df-rgiY8.js} +1 -1
  52. package/gui/dist/assets/{linear-DwZpGeRp.js → linear-B_6fRXVt.js} +1 -1
  53. package/gui/dist/assets/{mindmap-definition-RKZ34NQL-BW0yW4ap.js → mindmap-definition-RKZ34NQL-CRAd_VDM.js} +1 -1
  54. package/gui/dist/assets/{pieDiagram-4H26LBE5-plBCN_c7.js → pieDiagram-4H26LBE5-CJkmLrDI.js} +1 -1
  55. package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-Dmp9sOq_.js → quadrantDiagram-W4KKPZXB-D-rDEcnF.js} +1 -1
  56. package/gui/dist/assets/{requirementDiagram-4Y6WPE33-CthKC15J.js → requirementDiagram-4Y6WPE33-DE8MhSMe.js} +1 -1
  57. package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-BCAzzfPU.js → sankeyDiagram-5OEKKPKP-BwlxtrUs.js} +1 -1
  58. package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-DgwOe82k.js → sequenceDiagram-3UESZ5HK-B4LjBzWg.js} +1 -1
  59. package/gui/dist/assets/{stateDiagram-AJRCARHV-B6k-x1TC.js → stateDiagram-AJRCARHV-Bv72afPK.js} +1 -1
  60. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BKJDOkkR.js +1 -0
  61. package/gui/dist/assets/{timeline-definition-PNZ67QCA-CqwqJkZw.js → timeline-definition-PNZ67QCA-CBxgf8iW.js} +1 -1
  62. package/gui/dist/assets/{vennDiagram-CIIHVFJN-REFDrI5t.js → vennDiagram-CIIHVFJN-DPSdK06q.js} +1 -1
  63. package/gui/dist/assets/{wardley-L42UT6IY-53gLZWfR.js → wardley-L42UT6IY-JIAzuoPm.js} +1 -1
  64. package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-OPfKpNx-.js → wardleyDiagram-YWT4CUSO-YDfaF1wT.js} +1 -1
  65. package/gui/dist/assets/{xychartDiagram-2RQKCTM6-HWGySgFg.js → xychartDiagram-2RQKCTM6-DHlPryZX.js} +1 -1
  66. package/gui/dist/index.html +2 -2
  67. package/package.json +65 -65
  68. package/dist/src/tools/set-project.d.ts +0 -14
  69. package/dist/src/tools/set-project.js +0 -18
  70. package/gui/dist/assets/channel-JejxY436.js +0 -1
  71. package/gui/dist/assets/classDiagram-4FO5ZUOK-CdAqI_QA.js +0 -1
  72. package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CdAqI_QA.js +0 -1
  73. package/gui/dist/assets/index-DAzSbGj_.css +0 -1
  74. package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-bfw3SR0t.js +0 -1
@@ -4,8 +4,9 @@ const SERVER_NAME = "mcp-documentor";
4
4
  const PACKAGE_NAME = "@imenam/mcp-documentor";
5
5
  /**
6
6
  * Valeurs volontairement vides : chaque variable est inactive tant qu'elle n'est
7
- * pas remplie (PROXY_URL vide = GUI désactivée, clé vide = pas d'embeddings),
8
- * donc le fichier généré est utilisable tel quel, jamais à moitié cassé.
7
+ * pas remplie (PROXY_URL et APP_PORT vides = GUI désactivée, clé vide = pas
8
+ * d'embeddings), donc le fichier généré est utilisable tel quel, jamais à
9
+ * moitié cassé.
9
10
  */
10
11
  /** Slashes POSIX : évite les backslashes échappés dans le JSON, et Node les accepte sous Windows. */
11
12
  function toPosixPath(dir) {
@@ -23,14 +24,18 @@ function toDisplayName(folder) {
23
24
  .map(capitalize)
24
25
  .join(" ");
25
26
  }
26
- function buildServerEntry(codeRoot) {
27
+ function buildServerEntry(codeRoot, exposure) {
27
28
  // Le nom du dossier courant sert de base aux identifiants d'affichage côté proxy.
28
29
  const folder = path.basename(codeRoot) || "documentor";
30
+ // Un port explicite désactive le placeholder proxy : les deux variables ne
31
+ // doivent jamais être renseignées ensemble, PROXY_URL primant au runtime.
32
+ const proxyUrl = exposure.appPort !== undefined ? "" : exposure.proxyUrl ?? "http://localhost:";
29
33
  return {
30
34
  command: "npx",
31
35
  args: ["-y", PACKAGE_NAME],
32
36
  env: {
33
- PROXY_URL: "http://localhost:",
37
+ PROXY_URL: proxyUrl,
38
+ APP_PORT: exposure.appPort !== undefined ? String(exposure.appPort) : "",
34
39
  APP_GROUP: capitalize(folder),
35
40
  APP_NAME: `Documentor ${toDisplayName(folder)}`,
36
41
  APP_PATH: `/documentor-${folder}`,
@@ -47,6 +52,7 @@ function buildServerEntry(codeRoot) {
47
52
  }
48
53
  const GENERATED_VARS = [
49
54
  ["PROXY_URL", "URL du proxy pour exposer la GUI (compléter le port)"],
55
+ ["APP_PORT", "port local si aucun proxy — la GUI est servie sur http://localhost:<port>"],
50
56
  ["APP_GROUP", "groupe d'affichage dans le dashboard du proxy"],
51
57
  ["APP_NAME", "nom affiché de la GUI dans le dashboard du proxy"],
52
58
  ["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
@@ -64,7 +70,53 @@ const OPTIONAL_VARS = [
64
70
  ["READ_ONLY", "1/true pour désactiver toutes les écritures"],
65
71
  ["SYNC_PULL_ON_START", "false pour désactiver le pull au démarrage"],
66
72
  ];
67
- export function runSetupMcp({ cwd, force }) {
73
+ /** Lit `--flag=valeur` (tirets ou underscores acceptés). */
74
+ function readFlag(argv, name) {
75
+ const prefixes = [`--${name}=`, `--${name.replace(/_/g, "-")}=`];
76
+ for (const arg of argv) {
77
+ const prefix = prefixes.find((p) => arg.startsWith(p));
78
+ if (prefix)
79
+ return arg.slice(prefix.length).trim();
80
+ }
81
+ return undefined;
82
+ }
83
+ /**
84
+ * Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
85
+ * Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
86
+ * donc un fichier portant les deux serait silencieusement trompeur.
87
+ */
88
+ export function parseGuiExposure(argv) {
89
+ const rawProxyUrl = readFlag(argv, "proxy_url");
90
+ const rawAppPort = readFlag(argv, "app_port");
91
+ if (rawProxyUrl !== undefined && rawAppPort !== undefined) {
92
+ return { error: "--proxy_url et --app_port sont exclusifs : la GUI passe par le proxy, ou écoute sur un port local." };
93
+ }
94
+ if (rawProxyUrl !== undefined) {
95
+ if (rawProxyUrl === "")
96
+ return { error: "--proxy_url attend une URL, ex : --proxy_url=http://localhost:3000" };
97
+ let parsed;
98
+ try {
99
+ parsed = new URL(rawProxyUrl);
100
+ }
101
+ catch {
102
+ return { error: `--proxy_url invalide : "${rawProxyUrl}" n'est pas une URL absolue (ex : http://localhost:3000).` };
103
+ }
104
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
105
+ return { error: `--proxy_url invalide : protocole "${parsed.protocol}" non supporté (http ou https attendu).` };
106
+ }
107
+ // Une barre finale casserait la concaténation PROXY_URL + APP_PATH.
108
+ return { exposure: { proxyUrl: rawProxyUrl.replace(/\/+$/, "") } };
109
+ }
110
+ if (rawAppPort !== undefined) {
111
+ const port = Number(rawAppPort);
112
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
113
+ return { error: `--app_port invalide : "${rawAppPort}" n'est pas un entier entre 1 et 65535.` };
114
+ }
115
+ return { exposure: { appPort: port } };
116
+ }
117
+ return { exposure: {} };
118
+ }
119
+ export function runSetupMcp({ cwd, force, exposure = {} }) {
68
120
  const target = path.join(cwd, ".mcp.json");
69
121
  let config = {};
70
122
  const exists = fs.existsSync(target);
@@ -94,11 +146,17 @@ export function runSetupMcp({ cwd, force }) {
94
146
  console.error("Relancez avec --force pour écraser cette entrée (les autres serveurs sont préservés).");
95
147
  return 1;
96
148
  }
97
- config.mcpServers[SERVER_NAME] = buildServerEntry(cwd);
149
+ config.mcpServers[SERVER_NAME] = buildServerEntry(cwd, exposure);
98
150
  fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
99
151
  const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
100
152
  console.log(`Configuration ${SERVER_NAME} ${action} dans ${target}`);
101
153
  console.log("");
154
+ if (exposure.appPort !== undefined) {
155
+ console.log(`GUI en accès direct : http://localhost:${exposure.appPort} (sans proxy).`);
156
+ }
157
+ else if (exposure.proxyUrl !== undefined) {
158
+ console.log(`GUI exposée via le proxy ${exposure.proxyUrl}.`);
159
+ }
102
160
  console.log('À compléter dans le champ "env" :');
103
161
  for (const [name, description] of GENERATED_VARS) {
104
162
  console.log(` ${name.padEnd(30)} ${description}`);
@@ -110,5 +168,6 @@ export function runSetupMcp({ cwd, force }) {
110
168
  }
111
169
  console.log("");
112
170
  console.log("Une variable laissée vide est simplement inactive.");
171
+ console.log("PROXY_URL et APP_PORT s'excluent : PROXY_URL prime si les deux sont renseignés.");
113
172
  return 0;
114
173
  }
@@ -20,6 +20,19 @@ export interface ConflictHunk {
20
20
  remote: string[];
21
21
  /** Common ancestor, present only when the markers were written in diff3 style. */
22
22
  base?: string[];
23
+ /**
24
+ * The hunk concerns frontmatter, either because it sits inside the YAML block
25
+ * or because the doc merge driver hoisted a frontmatter disagreement to the
26
+ * top of the body. Keeping both sides of one of these would duplicate a key,
27
+ * so that choice is refused rather than silently producing broken YAML.
28
+ */
29
+ inFrontmatter: boolean;
30
+ /**
31
+ * The hunk is the block the doc merge driver hoisted out of the frontmatter.
32
+ * Its sides are `key: value` lines that belong back in the YAML, not in the
33
+ * body where the markers physically sit.
34
+ */
35
+ hoisted: boolean;
23
36
  }
24
37
  export interface ParsedFile {
25
38
  lines: string[];
@@ -34,6 +34,19 @@ export function hasConflictMarkers(text) {
34
34
  function sidesFor(operation, first, second) {
35
35
  return operation === "rebase" ? { local: second, remote: first } : { local: first, remote: second };
36
36
  }
37
+ /**
38
+ * Line range of the YAML frontmatter block, if the file opens with one.
39
+ * Returns null when there is none.
40
+ */
41
+ function frontmatterRange(lines) {
42
+ if (lines.length === 0 || lines[0].trim() !== "---")
43
+ return null;
44
+ for (let i = 1; i < lines.length; i++) {
45
+ if (lines[i].trim() === "---")
46
+ return { start: 0, end: i };
47
+ }
48
+ return null;
49
+ }
37
50
  function sectionFor(lines, hunkStart) {
38
51
  for (let i = hunkStart - 1; i >= 0; i--) {
39
52
  const match = lines[i].match(HEADING);
@@ -45,6 +58,7 @@ function sectionFor(lines, hunkStart) {
45
58
  /** Splits a conflicted file into hunks. Handles both diff3 and plain markers. */
46
59
  export function parseConflicts(text, operation) {
47
60
  const lines = splitLines(text);
61
+ const frontmatter = frontmatterRange(lines);
48
62
  const hunks = [];
49
63
  for (let i = 0; i < lines.length; i++) {
50
64
  if (!START.test(lines[i]))
@@ -56,6 +70,7 @@ export function parseConflicts(text, operation) {
56
70
  let sawBase = false;
57
71
  let closed = false;
58
72
  let j = i + 1;
73
+ const label = lines[i].match(START)?.[1] ?? "";
59
74
  for (; j < lines.length; j++) {
60
75
  const line = lines[j];
61
76
  if (BASE.test(line)) {
@@ -84,6 +99,8 @@ export function parseConflicts(text, operation) {
84
99
  local,
85
100
  remote,
86
101
  ...(sawBase ? { base } : {}),
102
+ inFrontmatter: label.includes("frontmatter") || (frontmatter !== null && i >= frontmatter.start && i <= frontmatter.end),
103
+ hoisted: label.includes("frontmatter"),
87
104
  });
88
105
  i = j;
89
106
  }
@@ -44,13 +44,11 @@ export type PullResult = {
44
44
  } | {
45
45
  status: "updated";
46
46
  changed: string[];
47
- } | {
48
- status: "diverged";
49
- ahead: number;
50
- behind: number;
47
+ autoCommitted?: boolean;
51
48
  } | {
52
49
  status: "conflict";
53
50
  conflicted: string[];
51
+ autoCommitted?: boolean;
54
52
  };
55
53
  export declare class SyncEngine {
56
54
  private storage;
@@ -88,6 +86,15 @@ export declare class SyncEngine {
88
86
  */
89
87
  pull(): Promise<PullResult>;
90
88
  private diffNames;
89
+ /**
90
+ * Stages and commits whatever the working tree holds. Committing is what makes
91
+ * local edits safe to rebase over, so both push and pull go through here
92
+ * rather than asking the caller to tidy up first.
93
+ *
94
+ * .gitignore (written by ensureRepo) excludes all derived and machine-local
95
+ * state, so `add -A` only ever stages versioned files.
96
+ */
97
+ private commitLocalChanges;
91
98
  /** Stages versioned files, commits if there is anything to commit, and pushes. */
92
99
  push(message?: string): Promise<{
93
100
  pushed: boolean;
@@ -205,20 +205,23 @@ export class SyncEngine {
205
205
  }
206
206
  // Ensure upstream tracking so status ahead/behind is meaningful.
207
207
  await this.git.branch(["--set-upstream-to", `origin/${cfg.branch}`, cfg.branch]).catch(() => { });
208
- const status = await this.git.status();
209
- if (status.behind === 0)
208
+ let status = await this.git.status();
209
+ if (status.behind === 0 && status.files.length === 0)
210
210
  return { status: "up-to-date" };
211
- // Uncommitted local changes block a safe merge — never risk clobbering them.
212
- // Surface as diverged so the user commits/pushes (or stashes) first.
213
- if (status.files.length > 0) {
214
- return { status: "diverged", ahead: status.ahead, behind: status.behind };
215
- }
211
+ // Uncommitted local edits cannot be rebased over, and reporting the
212
+ // divergence instead used to strand the caller: the pull asked for a push,
213
+ // which the remote rejected and which asked for a pull. Committing them
214
+ // resolves it - the edits become a commit the rebase can replay, nothing is
215
+ // published, and nothing can be clobbered.
216
+ const autoCommitted = status.files.length > 0 ? await this.commitLocalChanges() : false;
217
+ if (autoCommitted)
218
+ status = await this.git.status();
216
219
  const before = await this.git.revparse(["HEAD"]).catch(() => "");
217
220
  // Pure behind → fast-forward.
218
221
  if (status.ahead === 0) {
219
222
  await this.git.merge(["--ff-only", `origin/${cfg.branch}`]);
220
223
  const after = await this.git.revparse(["HEAD"]).catch(() => "");
221
- return { status: "updated", changed: await this.diffNames(before, after) };
224
+ return { status: "updated", changed: await this.diffNames(before, after), autoCommitted };
222
225
  }
223
226
  // Diverged with a clean tree → replay local commits on top of the remote. A
224
227
  // conflict stops the rebase with both versions preserved in the index, so it
@@ -229,17 +232,35 @@ export class SyncEngine {
229
232
  catch {
230
233
  const after = await this.git.status();
231
234
  if (after.conflicted.length > 0)
232
- return { status: "conflict", conflicted: after.conflicted };
235
+ return { status: "conflict", conflicted: after.conflicted, autoCommitted };
233
236
  throw new Error("Rebase failed without recorded conflicts. Inspect the data directory, or run `git rebase --abort` there to get back to the previous state.");
234
237
  }
235
238
  const rebased = await this.git.revparse(["HEAD"]).catch(() => "");
236
- return { status: "updated", changed: await this.diffNames(before, rebased) };
239
+ return { status: "updated", changed: await this.diffNames(before, rebased), autoCommitted };
237
240
  }
238
241
  async diffNames(before, after) {
239
242
  if (!before || !after || before === after)
240
243
  return [];
241
244
  return (await this.git.raw(["diff", "--name-only", before, after])).split("\n").filter(Boolean);
242
245
  }
246
+ /**
247
+ * Stages and commits whatever the working tree holds. Committing is what makes
248
+ * local edits safe to rebase over, so both push and pull go through here
249
+ * rather than asking the caller to tidy up first.
250
+ *
251
+ * .gitignore (written by ensureRepo) excludes all derived and machine-local
252
+ * state, so `add -A` only ever stages versioned files.
253
+ */
254
+ async commitLocalChanges(message) {
255
+ await this.git.add(["-A"]);
256
+ const status = await this.git.status();
257
+ const hasWork = status.staged.length > 0 || status.created.length > 0 || status.deleted.length > 0 || status.modified.length > 0;
258
+ if (!hasWork)
259
+ return false;
260
+ const machine = process.env.COMPUTERNAME || process.env.HOSTNAME || "unknown-machine";
261
+ await this.git.commit(message?.trim() || `docs sync from ${machine} · ${new Date().toISOString()}`);
262
+ return true;
263
+ }
243
264
  /** Stages versioned files, commits if there is anything to commit, and pushes. */
244
265
  async push(message) {
245
266
  const cfg = this.requireConfig();
@@ -252,24 +273,14 @@ export class SyncEngine {
252
273
  throw new Error(`A ${operation === "none" ? "conflict" : operation} is still unresolved (${pending.conflicted.length} conflicted file(s)). ` +
253
274
  "Resolve it with resolve_sync_conflict before pushing, otherwise the conflict markers would be committed as documentation content.");
254
275
  }
255
- // .gitignore (written by ensureRepo) excludes all derived/local state,
256
- // so -A safely stages only versioned files (projects/, config.json, .gitignore).
257
- await this.git.add(["-A"]);
258
- const status = await this.git.status();
259
- let committed = false;
260
- if (status.staged.length > 0 || status.created.length > 0 || status.deleted.length > 0 || status.modified.length > 0) {
261
- const machine = process.env.COMPUTERNAME || process.env.HOSTNAME || "unknown-machine";
262
- const msg = message?.trim() || `docs sync from ${machine} · ${new Date().toISOString()}`;
263
- await this.git.commit(msg);
264
- committed = true;
265
- }
276
+ const committed = await this.commitLocalChanges(message);
266
277
  try {
267
278
  await this.git.push(["-u", "origin", `HEAD:${cfg.branch}`]);
268
279
  }
269
280
  catch (e) {
270
281
  const msg = e instanceof Error ? e.message : String(e);
271
282
  if (/fetch first|non-fast-forward|\[rejected\]/i.test(msg)) {
272
- throw new Error("Push rejected: the remote has commits you don't have locally. Run pull_docs to integrate them, then push again.");
283
+ throw new Error("Push rejected: the remote has commits you don't have locally. Run sync_docs, which integrates them and pushes in one step.");
273
284
  }
274
285
  throw e;
275
286
  }
@@ -1,7 +1,6 @@
1
1
  import type { DocEntry, DocSummary, ExportBundle } from "../../shared/schemas/doc.schema.js";
2
2
  import type { HistoryStore } from "./history-store.js";
3
3
  interface GlobalConfig {
4
- currentProject: string;
5
4
  projects: string[];
6
5
  foldersMigrated?: boolean;
7
6
  }
@@ -22,16 +21,16 @@ export declare class StorageEngine {
22
21
  constructor(rootDir: string);
23
22
  setHistoryStore(store: HistoryStore): void;
24
23
  getDataDir(): string;
25
- getProjectDir(project?: string): string;
24
+ /**
25
+ * There is no "current project" fallback: every caller names the project it
26
+ * targets. The UI's project selector is a client-side view preference only.
27
+ */
28
+ getProjectDir(project: string): string;
26
29
  private getConfigPath;
27
- private getLocalStatePath;
28
- private readLocalState;
29
- private writeLocalState;
30
30
  /**
31
- * Returns the merged config. The shared part (projects list, migration flags)
32
- * comes from config.json (versioned), while currentProject is a machine-local
33
- * preference sourced from local-state.json (gitignored). Legacy config.json
34
- * files that still embed currentProject are migrated transparently.
31
+ * Reads config.json (versioned): the project list and migration flags.
32
+ * A legacy currentProject key is stripped on read — project selection is a
33
+ * per-client UI concern and is no longer server state.
35
34
  */
36
35
  getConfig(): GlobalConfig;
37
36
  saveConfig(config: GlobalConfig): void;
@@ -41,13 +40,13 @@ export declare class StorageEngine {
41
40
  private walkMdFiles;
42
41
  private docIdToFilePath;
43
42
  private cleanEmptyDirs;
44
- saveDoc(doc: DocEntry, project?: string, changeType?: "create" | "update" | "patch"): Promise<void>;
45
- getDoc(id: string, project?: string): Promise<DocEntry | null>;
46
- listDocs(project?: string): Promise<DocSummary[]>;
47
- deleteDoc(id: string, project?: string): Promise<boolean>;
48
- moveDoc(oldId: string, newId: string, project?: string): Promise<boolean>;
43
+ saveDoc(doc: DocEntry, project: string, changeType?: "create" | "update" | "patch"): Promise<void>;
44
+ getDoc(id: string, project: string): Promise<DocEntry | null>;
45
+ listDocs(project: string): Promise<DocSummary[]>;
46
+ deleteDoc(id: string, project: string): Promise<boolean>;
47
+ moveDoc(oldId: string, newId: string, project: string): Promise<boolean>;
49
48
  getBundles(): ExportBundle[];
50
49
  saveBundles(bundles: ExportBundle[]): void;
51
- rewriteLinks(oldId: string, newId: string, project?: string): Promise<string[]>;
50
+ rewriteLinks(oldId: string, newId: string, project: string): Promise<string[]>;
52
51
  }
53
52
  export {};
@@ -19,39 +19,20 @@ export class StorageEngine {
19
19
  const envDir = process.env.MCP_DATA_DIR;
20
20
  return envDir && envDir.trim() !== "" ? envDir : path.join(this.rootDir, ".docs-data");
21
21
  }
22
+ /**
23
+ * There is no "current project" fallback: every caller names the project it
24
+ * targets. The UI's project selector is a client-side view preference only.
25
+ */
22
26
  getProjectDir(project) {
23
- const config = this.getConfig();
24
- const proj = project ?? config.currentProject;
25
- return path.join(this.getDataDir(), "projects", proj);
27
+ return path.join(this.getDataDir(), "projects", project);
26
28
  }
27
29
  getConfigPath() {
28
30
  return path.join(this.getDataDir(), "config.json");
29
31
  }
30
- getLocalStatePath() {
31
- return path.join(this.getDataDir(), "local-state.json");
32
- }
33
- readLocalState() {
34
- const p = this.getLocalStatePath();
35
- if (fs.existsSync(p)) {
36
- try {
37
- return JSON.parse(fs.readFileSync(p, "utf-8"));
38
- }
39
- catch {
40
- logger.warn("Failed to parse local-state.json, using defaults");
41
- }
42
- }
43
- return {};
44
- }
45
- writeLocalState(state) {
46
- const p = this.getLocalStatePath();
47
- fs.mkdirSync(path.dirname(p), { recursive: true });
48
- fs.writeFileSync(p, JSON.stringify(state, null, 2), "utf-8");
49
- }
50
32
  /**
51
- * Returns the merged config. The shared part (projects list, migration flags)
52
- * comes from config.json (versioned), while currentProject is a machine-local
53
- * preference sourced from local-state.json (gitignored). Legacy config.json
54
- * files that still embed currentProject are migrated transparently.
33
+ * Reads config.json (versioned): the project list and migration flags.
34
+ * A legacy currentProject key is stripped on read — project selection is a
35
+ * per-client UI concern and is no longer server state.
55
36
  */
56
37
  getConfig() {
57
38
  const configPath = this.getConfigPath();
@@ -64,35 +45,20 @@ export class StorageEngine {
64
45
  logger.warn("Failed to parse config.json, using defaults");
65
46
  }
66
47
  }
67
- const local = this.readLocalState();
68
- const projects = shared.projects ?? ["default"];
69
- // Migrate a legacy currentProject embedded in config.json into local-state.
70
- if (shared.currentProject !== undefined && local.currentProject === undefined) {
71
- local.currentProject = shared.currentProject;
72
- this.writeLocalState(local);
48
+ if (shared.currentProject !== undefined) {
73
49
  delete shared.currentProject;
74
50
  fs.writeFileSync(configPath, JSON.stringify(shared, null, 2), "utf-8");
75
- logger.info("Migrated currentProject from config.json to local-state.json");
51
+ logger.info("Dropped obsolete currentProject from config.json");
76
52
  }
77
53
  return {
78
- currentProject: local.currentProject ?? projects[0] ?? "default",
79
- projects,
54
+ projects: shared.projects ?? ["default"],
80
55
  foldersMigrated: shared.foldersMigrated,
81
56
  };
82
57
  }
83
58
  saveConfig(config) {
84
- // Shared, versioned part → config.json (currentProject is intentionally excluded).
85
59
  const configPath = this.getConfigPath();
86
- const shared = {
87
- projects: config.projects,
88
- foldersMigrated: config.foldersMigrated,
89
- };
90
60
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
91
- fs.writeFileSync(configPath, JSON.stringify(shared, null, 2), "utf-8");
92
- // Machine-local part → local-state.json.
93
- const local = this.readLocalState();
94
- local.currentProject = config.currentProject;
95
- this.writeLocalState(local);
61
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
96
62
  }
97
63
  /** Reads sync settings from environment variables (admin-controlled, not agent-settable). */
98
64
  getSyncConfig() {
@@ -146,8 +112,7 @@ export class StorageEngine {
146
112
  }
147
113
  }
148
114
  async saveDoc(doc, project, changeType = "update") {
149
- const config = this.getConfig();
150
- const proj = project ?? config.currentProject;
115
+ const proj = project;
151
116
  const projectDir = this.getProjectDir(project);
152
117
  const targetPath = this.docIdToFilePath(doc.id, projectDir);
153
118
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
@@ -220,8 +185,7 @@ export class StorageEngine {
220
185
  // existed in several. Doc-targeted callers must name their project and go through
221
186
  // getDoc(id, project) — see core/project-scope.ts.
222
187
  async deleteDoc(id, project) {
223
- const config = this.getConfig();
224
- const proj = project ?? config.currentProject;
188
+ const proj = project;
225
189
  const projectDir = this.getProjectDir(project);
226
190
  const filePath = this.docIdToFilePath(id, projectDir);
227
191
  if (!fs.existsSync(filePath))
@@ -277,7 +241,7 @@ export class StorageEngine {
277
241
  }
278
242
  async rewriteLinks(oldId, newId, project) {
279
243
  const config = this.getConfig();
280
- const proj = project ?? config.currentProject;
244
+ const proj = project;
281
245
  const updated = [];
282
246
  // Within the same project: rewrite same-project refs and explicit self-cross-project refs
283
247
  const sameProjDocs = await this.listDocs(proj);