@imenam/mcp-documentor 1.0.40 → 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,6 +206,21 @@ 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
225
  const config = this.getConfig();
211
226
  const proj = project ?? config.currentProject;
@@ -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" });
@@ -352,6 +355,77 @@ app.post("/api/search", async (c) => {
352
355
  return c.json({ error: result.error }, 500);
353
356
  return c.json(result.data);
354
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
+ });
355
429
  // Static files
356
430
  app.use("/assets/*", serveStatic({ root: path.relative(process.cwd(), absoluteStaticRoot) }));
357
431
  // SPA Fallback
package/dist/src/index.js CHANGED
@@ -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) {
@@ -150,10 +152,11 @@ async function handleGetInitialState(msg) {
150
152
  doc.isStale = report.status === "stale";
151
153
  }
152
154
  }
155
+ const bundles = storageEngine.getBundles();
153
156
  broadcastToGUI({
154
157
  type: "INITIAL_STATE",
155
158
  correlationId: msg.correlationId,
156
- data: { config, docs, projects: config.projects },
159
+ data: { config, docs, projects: config.projects, bundles },
157
160
  timestamp: new Date().toISOString(),
158
161
  });
159
162
  }
@@ -898,6 +901,35 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
898
901
  required: [],
899
902
  },
900
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
+ },
901
933
  ],
902
934
  };
903
935
  });
@@ -945,6 +977,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
945
977
  return await restoreDocVersion(historyStore, storageEngine, embeddingEngine, args);
946
978
  case "reindex_embeddings":
947
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
+ }
948
1002
  default:
949
1003
  throw new Error(`Tool not found: ${name}`);
950
1004
  }
@@ -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
+ }>;
@@ -0,0 +1,40 @@
1
+ import { PrepareExportBundleArgsSchema } from "../../shared/schemas/doc.schema.js";
2
+ import { slugify } from "./create-doc.js";
3
+ export async function prepareExportBundle(storageEngine, args) {
4
+ try {
5
+ const validated = PrepareExportBundleArgsSchema.parse(args);
6
+ const id = slugify(validated.name);
7
+ const now = new Date().toISOString();
8
+ const bundles = storageEngine.getBundles();
9
+ const existingIndex = bundles.findIndex((b) => b.id === id);
10
+ const bundle = {
11
+ id,
12
+ name: validated.name,
13
+ zipFilename: validated.zip_filename.endsWith(".zip")
14
+ ? validated.zip_filename
15
+ : `${validated.zip_filename}.zip`,
16
+ docs: validated.docs,
17
+ ...(validated.description && { description: validated.description }),
18
+ createdAt: existingIndex >= 0 ? bundles[existingIndex].createdAt : now,
19
+ updatedAt: now,
20
+ };
21
+ if (existingIndex >= 0) {
22
+ bundles[existingIndex] = bundle;
23
+ }
24
+ else {
25
+ bundles.push(bundle);
26
+ }
27
+ storageEngine.saveBundles(bundles);
28
+ const action = existingIndex >= 0 ? "updated" : "created";
29
+ return {
30
+ content: [{
31
+ type: "text",
32
+ text: `Export bundle "${validated.name}" (id: "${id}") ${action} with ${validated.docs.length} doc(s). ZIP filename: "${bundle.zipFilename}".`,
33
+ }],
34
+ };
35
+ }
36
+ catch (err) {
37
+ const message = err instanceof Error ? err.message : String(err);
38
+ return { isError: true, content: [{ type: "text", text: `Error preparing export bundle: ${message}` }] };
39
+ }
40
+ }
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.40";
1
+ export declare const APP_VERSION = "1.0.41";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.40';
2
+ export const APP_VERSION = '1.0.41';