@imenam/mcp-documentor 1.0.38 → 1.0.41

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.
@@ -164,6 +164,25 @@ export declare const RestoreDocVersionArgsSchema: z.ZodObject<{
164
164
  version: z.ZodNumber;
165
165
  project: z.ZodOptional<z.ZodString>;
166
166
  }, z.core.$strip>;
167
+ export declare const ExportBundleSchema: z.ZodObject<{
168
+ id: z.ZodString;
169
+ name: z.ZodString;
170
+ zipFilename: z.ZodString;
171
+ docs: z.ZodArray<z.ZodString>;
172
+ description: z.ZodOptional<z.ZodString>;
173
+ createdAt: z.ZodString;
174
+ updatedAt: z.ZodString;
175
+ }, z.core.$strip>;
176
+ export type ExportBundle = z.infer<typeof ExportBundleSchema>;
177
+ export declare const PrepareExportBundleArgsSchema: z.ZodObject<{
178
+ name: z.ZodString;
179
+ zip_filename: z.ZodString;
180
+ docs: z.ZodArray<z.ZodString>;
181
+ description: z.ZodOptional<z.ZodString>;
182
+ }, z.core.$strip>;
183
+ export declare const DeleteExportBundleArgsSchema: z.ZodObject<{
184
+ id: z.ZodString;
185
+ }, z.core.$strip>;
167
186
  export declare const IPCMessageSchema: z.ZodObject<{
168
187
  type: z.ZodString;
169
188
  correlationId: z.ZodOptional<z.ZodString>;
@@ -109,6 +109,24 @@ export const RestoreDocVersionArgsSchema = z.object({
109
109
  version: z.number().int().positive(),
110
110
  project: z.string().optional(),
111
111
  });
112
+ export const ExportBundleSchema = z.object({
113
+ id: z.string(),
114
+ name: z.string(),
115
+ zipFilename: z.string(),
116
+ docs: z.array(z.string()),
117
+ description: z.string().optional(),
118
+ createdAt: z.string(),
119
+ updatedAt: z.string(),
120
+ });
121
+ export const PrepareExportBundleArgsSchema = z.object({
122
+ name: z.string().describe("Bundle name (becomes the identifier via slugify)"),
123
+ zip_filename: z.string().describe("ZIP filename to download, e.g. 'my-bundle.zip'"),
124
+ docs: z.array(z.string()).min(1).describe("Doc references: 'project:doc-id' or 'doc-id' for current project"),
125
+ description: z.string().optional().describe("Short description of the bundle"),
126
+ });
127
+ export const DeleteExportBundleArgsSchema = z.object({
128
+ id: z.string().describe("Bundle identifier to delete (slug)"),
129
+ });
112
130
  export const IPCMessageSchema = z.object({
113
131
  type: z.string(),
114
132
  correlationId: z.string().optional(),
@@ -1,4 +1,4 @@
1
- import type { DocEntry, DocSummary } from "../../shared/schemas/doc.schema.js";
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
4
  currentProject: string;
@@ -28,6 +28,8 @@ export declare class StorageEngine {
28
28
  } | null>;
29
29
  deleteDoc(id: string, project?: string): Promise<boolean>;
30
30
  moveDoc(oldId: string, newId: string, project?: string): Promise<boolean>;
31
+ getBundles(): ExportBundle[];
32
+ saveBundles(bundles: ExportBundle[]): void;
31
33
  rewriteLinks(oldId: string, newId: string, project?: string): Promise<string[]>;
32
34
  }
33
35
  export {};
@@ -206,17 +206,37 @@ export class StorageEngine {
206
206
  logger.info(`Doc moved: ${oldFilePath} → ${newFilePath}`);
207
207
  return true;
208
208
  }
209
+ getBundles() {
210
+ const p = path.join(this.getDataDir(), "export-bundles.json");
211
+ if (!fs.existsSync(p))
212
+ return [];
213
+ try {
214
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
215
+ }
216
+ catch {
217
+ return [];
218
+ }
219
+ }
220
+ saveBundles(bundles) {
221
+ const p = path.join(this.getDataDir(), "export-bundles.json");
222
+ fs.writeFileSync(p, JSON.stringify(bundles, null, 2), "utf-8");
223
+ }
209
224
  async rewriteLinks(oldId, newId, project) {
210
- const docs = await this.listDocs(project);
225
+ const config = this.getConfig();
226
+ const proj = project ?? config.currentProject;
211
227
  const updated = [];
212
- for (const summary of docs) {
228
+ // Within the same project: rewrite same-project refs and explicit self-cross-project refs
229
+ const sameProjDocs = await this.listDocs(proj);
230
+ for (const summary of sameProjDocs) {
213
231
  if (summary.id === newId)
214
232
  continue;
215
- const doc = await this.getDoc(summary.id, project);
233
+ const doc = await this.getDoc(summary.id, proj);
216
234
  if (!doc)
217
235
  continue;
218
236
  let changed = false;
219
- const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}`, "g"), `doc://${newId}`);
237
+ let newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}`, "g"), `doc://${newId}`);
238
+ // Also rewrite explicit self cross-project refs (doc://proj:oldId → doc://proj:newId)
239
+ newContent = newContent.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${proj}:${newId}`);
220
240
  if (newContent !== doc.content) {
221
241
  doc.content = newContent;
222
242
  changed = true;
@@ -226,15 +246,49 @@ export class StorageEngine {
226
246
  changed = true;
227
247
  return newId;
228
248
  }
249
+ if (ref === `${proj}:${oldId}`) {
250
+ changed = true;
251
+ return `${proj}:${newId}`;
252
+ }
229
253
  return ref;
230
254
  });
231
255
  doc.related_docs = newRelated;
232
256
  if (changed) {
233
257
  doc.updatedAt = new Date().toISOString();
234
- await this.saveDoc(doc, project);
258
+ await this.saveDoc(doc, proj);
235
259
  updated.push(summary.id);
236
260
  }
237
261
  }
262
+ // In other projects: rewrite cross-project refs pointing to this project
263
+ for (const otherProj of config.projects) {
264
+ if (otherProj === proj)
265
+ continue;
266
+ const otherDocs = await this.listDocs(otherProj);
267
+ for (const summary of otherDocs) {
268
+ const doc = await this.getDoc(summary.id, otherProj);
269
+ if (!doc)
270
+ continue;
271
+ let changed = false;
272
+ const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${proj}:${newId}`);
273
+ if (newContent !== doc.content) {
274
+ doc.content = newContent;
275
+ changed = true;
276
+ }
277
+ const newRelated = doc.related_docs.map((ref) => {
278
+ if (ref === `${proj}:${oldId}`) {
279
+ changed = true;
280
+ return `${proj}:${newId}`;
281
+ }
282
+ return ref;
283
+ });
284
+ doc.related_docs = newRelated;
285
+ if (changed) {
286
+ doc.updatedAt = new Date().toISOString();
287
+ await this.saveDoc(doc, otherProj);
288
+ updated.push(`${otherProj}:${summary.id}`);
289
+ }
290
+ }
291
+ }
238
292
  return updated;
239
293
  }
240
294
  }
@@ -8,6 +8,8 @@ import path from "path";
8
8
  import fs from "fs";
9
9
  import { fileURLToPath } from "url";
10
10
  import { createLogger, setupLogging } from "@imenam/mcp-gui-interface";
11
+ import matter from "gray-matter";
12
+ import JSZip from "jszip";
11
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
14
  function findProjectRoot(startDir) {
13
15
  let current = startDir;
@@ -20,6 +22,7 @@ function findProjectRoot(startDir) {
20
22
  return startDir;
21
23
  }
22
24
  const rootDir = findProjectRoot(__dirname);
25
+ const dataDir = process.env.MCP_DATA_DIR?.trim() || path.join(rootDir, ".docs-data");
23
26
  const staticRoot = path.join(rootDir, "gui/dist");
24
27
  const absoluteStaticRoot = path.resolve(staticRoot);
25
28
  setupLogging({ processLabel: "mcp-documentor:GUI", logDir: path.join(rootDir, ".docs-data", "logs") });
@@ -116,7 +119,7 @@ function setupIpcErrorHandlers() {
116
119
  });
117
120
  return;
118
121
  }
119
- if (msg && (msg.type === "INITIAL_STATE" || msg.type === "CONFIG_UPDATED")) {
122
+ if (msg && (msg.type === "INITIAL_STATE" || msg.type === "CONFIG_UPDATED" || msg.type === "BUNDLES_UPDATED")) {
120
123
  for (const controller of sseControllers) {
121
124
  try {
122
125
  controller.writeSSE({ data: JSON.stringify(msg), event: "message" });
@@ -246,6 +249,13 @@ app.get("/api/projects", async (c) => {
246
249
  return c.json({ error: result.error }, 500);
247
250
  return c.json(result.data);
248
251
  });
252
+ app.get("/api/folders", async (c) => {
253
+ const project = c.req.query("project");
254
+ const result = await ipcRequest("LIST_FOLDERS", project ? { project } : {});
255
+ if (result.error)
256
+ return c.json({ error: result.error }, 500);
257
+ return c.json(result.data);
258
+ });
249
259
  app.put("/api/project", async (c) => {
250
260
  const { project } = await c.req.json();
251
261
  if (!project)
@@ -261,6 +271,27 @@ app.get("/api/docs", async (c) => {
261
271
  return c.json({ error: result.error }, 500);
262
272
  return c.json(result.data);
263
273
  });
274
+ app.post("/api/doc", async (c) => {
275
+ try {
276
+ const body = await c.req.json();
277
+ if (!body.title || !body.content) {
278
+ return c.json({ error: "title and content are required" }, 400);
279
+ }
280
+ const result = await ipcRequest("CREATE_DOC", {
281
+ title: body.title,
282
+ content: body.content,
283
+ folder: body.folder,
284
+ tags: body.tags,
285
+ project: body.project,
286
+ }, 15000);
287
+ if (result.error)
288
+ return c.json({ error: result.error }, 500);
289
+ return c.json(result.data ?? { success: true });
290
+ }
291
+ catch (err) {
292
+ return c.json({ error: err.message }, 500);
293
+ }
294
+ });
264
295
  app.get("/api/doc/*", async (c) => {
265
296
  const id = decodeURIComponent(c.req.path.slice("/api/doc/".length));
266
297
  const result = await ipcRequest("GET_DOC", { id });
@@ -324,6 +355,77 @@ app.post("/api/search", async (c) => {
324
355
  return c.json({ error: result.error }, 500);
325
356
  return c.json(result.data);
326
357
  });
358
+ // Export bundles
359
+ app.get("/api/export-bundles", (c) => {
360
+ const bundlesPath = path.join(dataDir, "export-bundles.json");
361
+ if (!fs.existsSync(bundlesPath))
362
+ return c.json([]);
363
+ try {
364
+ return c.json(JSON.parse(fs.readFileSync(bundlesPath, "utf-8")));
365
+ }
366
+ catch {
367
+ return c.json([]);
368
+ }
369
+ });
370
+ app.get("/api/export-zip/:id", async (c) => {
371
+ const id = c.req.param("id");
372
+ const bundlesPath = path.join(dataDir, "export-bundles.json");
373
+ let bundles = [];
374
+ try {
375
+ if (fs.existsSync(bundlesPath))
376
+ bundles = JSON.parse(fs.readFileSync(bundlesPath, "utf-8"));
377
+ }
378
+ catch {
379
+ return c.json({ error: "Failed to read bundles" }, 500);
380
+ }
381
+ const bundle = bundles.find((b) => b.id === id);
382
+ if (!bundle)
383
+ return c.json({ error: "Bundle not found" }, 404);
384
+ let currentProject = "default";
385
+ try {
386
+ const configPath = path.join(dataDir, "config.json");
387
+ if (fs.existsSync(configPath)) {
388
+ currentProject = JSON.parse(fs.readFileSync(configPath, "utf-8")).currentProject ?? "default";
389
+ }
390
+ }
391
+ catch { /* use default */ }
392
+ const zip = new JSZip();
393
+ for (const ref of bundle.docs) {
394
+ let project;
395
+ let docId;
396
+ if (ref.includes(":")) {
397
+ const colonIdx = ref.indexOf(":");
398
+ project = ref.slice(0, colonIdx);
399
+ docId = ref.slice(colonIdx + 1);
400
+ }
401
+ else {
402
+ project = currentProject;
403
+ docId = ref;
404
+ }
405
+ const docFilePath = path.join(dataDir, "projects", project, ...docId.split("/")) + ".md";
406
+ let content;
407
+ if (!fs.existsSync(docFilePath)) {
408
+ content = `# [Doc not found: ${ref}]\n`;
409
+ }
410
+ else {
411
+ try {
412
+ content = matter(fs.readFileSync(docFilePath, "utf-8")).content.trim();
413
+ }
414
+ catch {
415
+ content = `# [Read error: ${ref}]\n`;
416
+ }
417
+ }
418
+ zip.file(`${project}/${docId}.md`, content);
419
+ }
420
+ const zipBuffer = await zip.generateAsync({ type: "nodebuffer" });
421
+ return new Response(new Uint8Array(zipBuffer), {
422
+ status: 200,
423
+ headers: {
424
+ "Content-Type": "application/zip",
425
+ "Content-Disposition": `attachment; filename="${bundle.zipFilename}"`,
426
+ },
427
+ });
428
+ });
327
429
  // Static files
328
430
  app.use("/assets/*", serveStatic({ root: path.relative(process.cwd(), absoluteStaticRoot) }));
329
431
  // SPA Fallback
package/dist/src/index.js CHANGED
@@ -11,7 +11,7 @@ import { GuiLauncher, createLogger, setupLogging } from "@imenam/mcp-gui-interfa
11
11
  import { StorageEngine } from "./core/storage-engine.js";
12
12
  import { EmbeddingStore } from "./core/embedding-store.js";
13
13
  import { EmbeddingEngine } from "./core/embedding-engine.js";
14
- import { createDoc } from "./tools/create-doc.js";
14
+ import { createDoc, slugify } from "./tools/create-doc.js";
15
15
  import { getDoc } from "./tools/get-doc.js";
16
16
  import { listDocs } from "./tools/list-docs.js";
17
17
  import { updateDoc } from "./tools/update-doc.js";
@@ -34,6 +34,8 @@ import { listDocVersions } from "./tools/list-doc-versions.js";
34
34
  import { getDocVersion } from "./tools/get-doc-version.js";
35
35
  import { restoreDocVersion } from "./tools/restore-doc-version.js";
36
36
  import { reindexEmbeddings } from "./tools/reindex-embeddings.js";
37
+ import { prepareExportBundle } from "./tools/prepare-export-bundle.js";
38
+ import { deleteExportBundle } from "./tools/delete-export-bundle.js";
37
39
  const __filename = fileURLToPath(import.meta.url);
38
40
  const __dirname = path.dirname(__filename);
39
41
  function findProjectRoot(startDir) {
@@ -126,6 +128,12 @@ function handleIPCMessage(msg) {
126
128
  else if (msg.type === "GET_DOC_VERSION") {
127
129
  handleGetDocVersionIPC(msg);
128
130
  }
131
+ else if (msg.type === "CREATE_DOC") {
132
+ handleCreateDocIPC(msg);
133
+ }
134
+ else if (msg.type === "LIST_FOLDERS") {
135
+ handleListFoldersIPC(msg);
136
+ }
129
137
  else if (msg.type === "UPDATE_DOC") {
130
138
  handleUpdateDocIPC(msg);
131
139
  }
@@ -144,10 +152,11 @@ async function handleGetInitialState(msg) {
144
152
  doc.isStale = report.status === "stale";
145
153
  }
146
154
  }
155
+ const bundles = storageEngine.getBundles();
147
156
  broadcastToGUI({
148
157
  type: "INITIAL_STATE",
149
158
  correlationId: msg.correlationId,
150
- data: { config, docs, projects: config.projects },
159
+ data: { config, docs, projects: config.projects, bundles },
151
160
  timestamp: new Date().toISOString(),
152
161
  });
153
162
  }
@@ -257,6 +266,89 @@ async function handleGetDoc(msg) {
257
266
  });
258
267
  }
259
268
  }
269
+ async function handleListFoldersIPC(msg) {
270
+ try {
271
+ const project = msg.data?.project;
272
+ const docs = await storageEngine.listDocs(project);
273
+ const folders = new Set();
274
+ for (const doc of docs) {
275
+ if (doc.folder) {
276
+ const parts = doc.folder.split("/").filter(Boolean);
277
+ for (let i = 1; i <= parts.length; i++) {
278
+ folders.add(parts.slice(0, i).join("/"));
279
+ }
280
+ }
281
+ }
282
+ broadcastToGUI({
283
+ type: "LIST_FOLDERS_RESPONSE",
284
+ correlationId: msg.correlationId,
285
+ data: { folders: Array.from(folders).sort() },
286
+ timestamp: new Date().toISOString(),
287
+ });
288
+ }
289
+ catch (err) {
290
+ broadcastToGUI({
291
+ type: "LIST_FOLDERS_RESPONSE",
292
+ correlationId: msg.correlationId,
293
+ error: err.message,
294
+ timestamp: new Date().toISOString(),
295
+ });
296
+ }
297
+ }
298
+ async function handleCreateDocIPC(msg) {
299
+ try {
300
+ const { title, content, folder, tags, project } = msg.data ?? {};
301
+ if (!title || !content)
302
+ throw new Error("title and content are required");
303
+ const result = await createDoc(storageEngine, embeddingEngine, {
304
+ title,
305
+ content,
306
+ ...(folder && { folder }),
307
+ ...(tags && { tags }),
308
+ ...(project && { project }),
309
+ });
310
+ if (result.isError) {
311
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "Create failed";
312
+ broadcastToGUI({
313
+ type: "CREATE_DOC_RESPONSE",
314
+ correlationId: msg.correlationId,
315
+ error: text,
316
+ timestamp: new Date().toISOString(),
317
+ });
318
+ return;
319
+ }
320
+ const slug = slugify(title);
321
+ const id = folder ? `${folder}/${slug}` : slug;
322
+ broadcastToGUI({
323
+ type: "CREATE_DOC_RESPONSE",
324
+ correlationId: msg.correlationId,
325
+ data: { success: true, id },
326
+ timestamp: new Date().toISOString(),
327
+ });
328
+ const docs = await storageEngine.listDocs();
329
+ for (const doc of docs) {
330
+ const full = await storageEngine.getDoc(doc.id);
331
+ if (full && full.tracked_files.length > 0) {
332
+ const report = computeFreshness(full.tracked_files);
333
+ doc.isStale = report.status === "stale";
334
+ }
335
+ }
336
+ const config = storageEngine.getConfig();
337
+ broadcastToGUI({
338
+ type: "INITIAL_STATE",
339
+ data: { config, docs, projects: config.projects },
340
+ timestamp: new Date().toISOString(),
341
+ });
342
+ }
343
+ catch (err) {
344
+ broadcastToGUI({
345
+ type: "CREATE_DOC_RESPONSE",
346
+ correlationId: msg.correlationId,
347
+ error: err.message,
348
+ timestamp: new Date().toISOString(),
349
+ });
350
+ }
351
+ }
260
352
  async function handleUpdateDocIPC(msg) {
261
353
  try {
262
354
  const result = await updateDoc(storageEngine, embeddingEngine, {
@@ -809,6 +901,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
809
901
  required: [],
810
902
  },
811
903
  },
904
+ {
905
+ name: "prepare_export_bundle",
906
+ description: "Create or update a named export bundle: a set of documentation references that the user can download as a ZIP from the GUI. The ZIP contains clean markdown content (no frontmatter). Existing bundle with the same name is replaced.",
907
+ inputSchema: {
908
+ type: "object",
909
+ properties: {
910
+ name: { type: "string", description: "Bundle display name (becomes the identifier via slugify)" },
911
+ zip_filename: { type: "string", description: "ZIP filename to download, e.g. 'my-bundle.zip'" },
912
+ docs: {
913
+ type: "array",
914
+ items: { type: "string" },
915
+ description: "Doc references to include: 'project:doc-id' for cross-project, or 'doc-id' for current project",
916
+ },
917
+ description: { type: "string", description: "Short description of the bundle (optional)" },
918
+ },
919
+ required: ["name", "zip_filename", "docs"],
920
+ },
921
+ },
922
+ {
923
+ name: "delete_export_bundle",
924
+ description: "Delete an export bundle by its identifier (slug).",
925
+ inputSchema: {
926
+ type: "object",
927
+ properties: {
928
+ id: { type: "string", description: "Bundle identifier to delete (slug, e.g. 'my-bundle')" },
929
+ },
930
+ required: ["id"],
931
+ },
932
+ },
812
933
  ],
813
934
  };
814
935
  });
@@ -856,6 +977,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
856
977
  return await restoreDocVersion(historyStore, storageEngine, embeddingEngine, args);
857
978
  case "reindex_embeddings":
858
979
  return await reindexEmbeddings(storageEngine, embeddingEngine, embeddingStore, args);
980
+ case "prepare_export_bundle": {
981
+ const result = await prepareExportBundle(storageEngine, args);
982
+ if (!result.isError) {
983
+ broadcastToGUI({
984
+ type: "BUNDLES_UPDATED",
985
+ data: { bundles: storageEngine.getBundles() },
986
+ timestamp: new Date().toISOString(),
987
+ });
988
+ }
989
+ return result;
990
+ }
991
+ case "delete_export_bundle": {
992
+ const result = await deleteExportBundle(storageEngine, args);
993
+ if (!result.isError) {
994
+ broadcastToGUI({
995
+ type: "BUNDLES_UPDATED",
996
+ data: { bundles: storageEngine.getBundles() },
997
+ timestamp: new Date().toISOString(),
998
+ });
999
+ }
1000
+ return result;
1001
+ }
859
1002
  default:
860
1003
  throw new Error(`Tool not found: ${name}`);
861
1004
  }
@@ -28,13 +28,31 @@ function extractDocLinks(content) {
28
28
  }
29
29
  return ids;
30
30
  }
31
+ function parseCrossProjectRef(ref) {
32
+ const colonIdx = ref.indexOf(":");
33
+ if (colonIdx === -1)
34
+ return null;
35
+ return { project: ref.slice(0, colonIdx), docId: ref.slice(colonIdx + 1) };
36
+ }
31
37
  export async function checkLinks(storageEngine, args) {
32
38
  try {
33
39
  const validated = CheckLinksArgsSchema.parse(args);
34
40
  const config = storageEngine.getConfig();
35
41
  const project = validated.project ?? config.currentProject;
42
+ const knownProjects = new Set(config.projects);
36
43
  const docs = await storageEngine.listDocs(project);
37
44
  const validIds = new Set(docs.map((d) => d.id));
45
+ // Cache of validIds per project to avoid re-fetching
46
+ const projectValidIds = new Map();
47
+ projectValidIds.set(project, validIds);
48
+ async function getValidIdsForProject(proj) {
49
+ if (projectValidIds.has(proj))
50
+ return projectValidIds.get(proj);
51
+ const projDocs = await storageEngine.listDocs(proj);
52
+ const ids = new Set(projDocs.map((d) => d.id));
53
+ projectValidIds.set(proj, ids);
54
+ return ids;
55
+ }
38
56
  const brokenRefs = [];
39
57
  const brokenDocLinks = [];
40
58
  const brokenAnchors = [];
@@ -43,13 +61,37 @@ export async function checkLinks(storageEngine, args) {
43
61
  if (!doc)
44
62
  continue;
45
63
  for (const ref of doc.related_docs ?? []) {
46
- if (!validIds.has(ref)) {
47
- brokenRefs.push({ sourceId: summary.id, brokenId: ref });
64
+ const cross = parseCrossProjectRef(ref);
65
+ if (cross) {
66
+ if (!knownProjects.has(cross.project)) {
67
+ brokenRefs.push({ sourceId: summary.id, brokenId: ref, reason: `project "${cross.project}" not found` });
68
+ }
69
+ else {
70
+ const targetIds = await getValidIdsForProject(cross.project);
71
+ if (!targetIds.has(cross.docId)) {
72
+ brokenRefs.push({ sourceId: summary.id, brokenId: ref, reason: `doc "${cross.docId}" not found in project "${cross.project}"` });
73
+ }
74
+ }
75
+ }
76
+ else if (!validIds.has(ref)) {
77
+ brokenRefs.push({ sourceId: summary.id, brokenId: ref, reason: "doc not found" });
48
78
  }
49
79
  }
50
80
  for (const targetId of extractDocLinks(doc.content)) {
51
- if (!validIds.has(targetId)) {
52
- brokenDocLinks.push({ sourceId: summary.id, targetId });
81
+ const cross = parseCrossProjectRef(targetId);
82
+ if (cross) {
83
+ if (!knownProjects.has(cross.project)) {
84
+ brokenDocLinks.push({ sourceId: summary.id, targetId, reason: `project "${cross.project}" not found` });
85
+ }
86
+ else {
87
+ const targetIds = await getValidIdsForProject(cross.project);
88
+ if (!targetIds.has(cross.docId)) {
89
+ brokenDocLinks.push({ sourceId: summary.id, targetId, reason: `doc "${cross.docId}" not found in project "${cross.project}"` });
90
+ }
91
+ }
92
+ }
93
+ else if (!validIds.has(targetId)) {
94
+ brokenDocLinks.push({ sourceId: summary.id, targetId, reason: "doc not found" });
53
95
  }
54
96
  }
55
97
  const anchorLinks = extractAnchorLinks(doc.content);
@@ -75,10 +117,10 @@ export async function checkLinks(storageEngine, args) {
75
117
  }
76
118
  const lines = [];
77
119
  for (const b of brokenRefs) {
78
- lines.push(` [related_doc] ${b.sourceId} → ${b.brokenId} (doc not found)`);
120
+ lines.push(` [related_doc] ${b.sourceId} → ${b.brokenId} (${b.reason})`);
79
121
  }
80
122
  for (const b of brokenDocLinks) {
81
- lines.push(` [doc://] ${b.sourceId} → doc://${b.targetId} (doc not found)`);
123
+ lines.push(` [doc://] ${b.sourceId} → doc://${b.targetId} (${b.reason})`);
82
124
  }
83
125
  for (const b of brokenAnchors) {
84
126
  lines.push(` [anchor] ${b.sourceId} → #${b.anchor} (heading not found)`);
@@ -1,5 +1,6 @@
1
1
  import type { StorageEngine } from "../core/storage-engine.js";
2
2
  import type { EmbeddingEngine } from "../core/embedding-engine.js";
3
+ export declare function slugify(title: string): string;
3
4
  export declare function createDoc(storageEngine: StorageEngine, embeddingEngine: EmbeddingEngine, args: unknown): Promise<{
4
5
  isError: boolean;
5
6
  content: {
@@ -3,7 +3,7 @@ import path from "path";
3
3
  import { CreateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
4
4
  import { computeFileHash } from "./check-freshness.js";
5
5
  import { snapshotSection } from "../core/section-tracker.js";
6
- function slugify(title) {
6
+ export function slugify(title) {
7
7
  return title
8
8
  .normalize("NFD")
9
9
  .replace(/[\u0300-\u036f]/g, "")
@@ -0,0 +1,14 @@
1
+ import type { StorageEngine } from "../core/storage-engine.js";
2
+ export declare function deleteExportBundle(storageEngine: StorageEngine, args: unknown): Promise<{
3
+ isError: boolean;
4
+ content: {
5
+ type: string;
6
+ text: string;
7
+ }[];
8
+ } | {
9
+ content: {
10
+ type: string;
11
+ text: string;
12
+ }[];
13
+ isError?: undefined;
14
+ }>;
@@ -0,0 +1,24 @@
1
+ import { DeleteExportBundleArgsSchema } from "../../shared/schemas/doc.schema.js";
2
+ export async function deleteExportBundle(storageEngine, args) {
3
+ try {
4
+ const validated = DeleteExportBundleArgsSchema.parse(args);
5
+ const bundles = storageEngine.getBundles();
6
+ const index = bundles.findIndex((b) => b.id === validated.id);
7
+ if (index < 0) {
8
+ return {
9
+ isError: true,
10
+ content: [{ type: "text", text: `Bundle with id "${validated.id}" not found.` }],
11
+ };
12
+ }
13
+ const removed = bundles[index];
14
+ bundles.splice(index, 1);
15
+ storageEngine.saveBundles(bundles);
16
+ return {
17
+ content: [{ type: "text", text: `Export bundle "${removed.name}" (id: "${removed.id}") deleted.` }],
18
+ };
19
+ }
20
+ catch (err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ return { isError: true, content: [{ type: "text", text: `Error deleting export bundle: ${message}` }] };
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ import type { StorageEngine } from "../core/storage-engine.js";
2
+ export declare function prepareExportBundle(storageEngine: StorageEngine, args: unknown): Promise<{
3
+ content: {
4
+ type: string;
5
+ text: string;
6
+ }[];
7
+ isError?: undefined;
8
+ } | {
9
+ isError: boolean;
10
+ content: {
11
+ type: string;
12
+ text: string;
13
+ }[];
14
+ }>;