@imenam/mcp-documentor 1.0.23

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 (59) hide show
  1. package/README.md +74 -0
  2. package/dist/shared/schemas/doc.schema.d.ts +112 -0
  3. package/dist/shared/schemas/doc.schema.js +83 -0
  4. package/dist/src/core/embedding-engine.d.ts +23 -0
  5. package/dist/src/core/embedding-engine.js +119 -0
  6. package/dist/src/core/embedding-store.d.ts +15 -0
  7. package/dist/src/core/embedding-store.js +97 -0
  8. package/dist/src/core/migrate-embeddings.d.ts +3 -0
  9. package/dist/src/core/migrate-embeddings.js +30 -0
  10. package/dist/src/core/migrate-folders.d.ts +2 -0
  11. package/dist/src/core/migrate-folders.js +45 -0
  12. package/dist/src/core/storage-engine.d.ts +29 -0
  13. package/dist/src/core/storage-engine.js +191 -0
  14. package/dist/src/gui-worker.d.ts +1 -0
  15. package/dist/src/gui-worker.js +349 -0
  16. package/dist/src/index.d.ts +2 -0
  17. package/dist/src/index.js +678 -0
  18. package/dist/src/tools/check-freshness.d.ts +18 -0
  19. package/dist/src/tools/check-freshness.js +167 -0
  20. package/dist/src/tools/check-links.d.ts +14 -0
  21. package/dist/src/tools/check-links.js +99 -0
  22. package/dist/src/tools/create-doc.d.ts +15 -0
  23. package/dist/src/tools/create-doc.js +78 -0
  24. package/dist/src/tools/delete-doc.d.ts +15 -0
  25. package/dist/src/tools/delete-doc.js +18 -0
  26. package/dist/src/tools/get-doc-metadata.d.ts +14 -0
  27. package/dist/src/tools/get-doc-metadata.js +40 -0
  28. package/dist/src/tools/get-doc-tree.d.ts +14 -0
  29. package/dist/src/tools/get-doc-tree.js +64 -0
  30. package/dist/src/tools/get-doc.d.ts +14 -0
  31. package/dist/src/tools/get-doc.js +55 -0
  32. package/dist/src/tools/git-diff-doc.d.ts +14 -0
  33. package/dist/src/tools/git-diff-doc.js +93 -0
  34. package/dist/src/tools/list-docs.d.ts +14 -0
  35. package/dist/src/tools/list-docs.js +78 -0
  36. package/dist/src/tools/list-projects.d.ts +14 -0
  37. package/dist/src/tools/list-projects.js +16 -0
  38. package/dist/src/tools/move-doc.d.ts +15 -0
  39. package/dist/src/tools/move-doc.js +34 -0
  40. package/dist/src/tools/patch-doc.d.ts +15 -0
  41. package/dist/src/tools/patch-doc.js +37 -0
  42. package/dist/src/tools/refresh-file-snapshot.d.ts +14 -0
  43. package/dist/src/tools/refresh-file-snapshot.js +45 -0
  44. package/dist/src/tools/search-docs.d.ts +15 -0
  45. package/dist/src/tools/search-docs.js +26 -0
  46. package/dist/src/tools/set-project.d.ts +14 -0
  47. package/dist/src/tools/set-project.js +18 -0
  48. package/dist/src/tools/update-doc.d.ts +15 -0
  49. package/dist/src/tools/update-doc.js +66 -0
  50. package/dist/src/version.d.ts +1 -0
  51. package/dist/src/version.js +2 -0
  52. package/gui/README.md +75 -0
  53. package/gui/dist/assets/index-BwyK1pan.css +1 -0
  54. package/gui/dist/assets/index-CzuFn4Pp.js +48 -0
  55. package/gui/dist/index.html +14 -0
  56. package/gui/dist/vite.svg +1 -0
  57. package/package.json +61 -0
  58. package/scripts/generate-version.js +20 -0
  59. package/scripts/migrate-links.js +258 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @imenam/mcp-documentor
2
+
3
+ An MCP server to create, manage, and search project documentation — with staleness tracking and optional semantic search.
4
+
5
+ Connect it to Cursor, Claude Desktop, or any MCP-compatible agent to let your AI document, browse and verify your codebase docs without leaving the chat.
6
+
7
+ ## Features
8
+
9
+ - **Create & update docs** — write markdown documentation entries linked to source files
10
+ - **Staleness tracking** — automatically detects when tracked files have changed since the last doc update
11
+ - **Semantic search** — find relevant docs by meaning (requires an OpenAI API key)
12
+ - **Multi-project support** — organize docs into separate projects and switch between them
13
+ - **GUI** — built-in web interface to browse and read documentation
14
+ - **Proxy-aware** — integrates with the [proxy-setup](https://github.com/Imena/proxy-setup) for GUI exposure
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npx -y @imenam/mcp-documentor
20
+ ```
21
+
22
+ Or install globally:
23
+
24
+ ```bash
25
+ npm install -g @imenam/mcp-documentor
26
+ ```
27
+
28
+ ## MCP Configuration
29
+
30
+ Add to your MCP client config (e.g. Cursor `mcp.json`):
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "mcp-documentor": {
36
+ "command": "npx",
37
+ "args": ["-y", "@imenam/mcp-documentor"],
38
+ "env": {
39
+ "OPENAI_API_KEY": "sk-..."
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Environment Variables
47
+
48
+ | Variable | Required | Description |
49
+ |---|---|---|
50
+ | `OPENAI_API_KEY` | No | Enables semantic search via OpenAI embeddings |
51
+ | `PROXY_URL` | No | URL of the proxy server to register the GUI |
52
+
53
+ ## Available Tools
54
+
55
+ | Tool | Description |
56
+ |---|---|
57
+ | `create_doc` | Create a new documentation entry linked to source files |
58
+ | `get_doc` | Retrieve a documentation entry with full content |
59
+ | `get_doc_metadata` | Get metadata and freshness status without loading content |
60
+ | `list_docs` | List all docs in the current project with staleness indicators |
61
+ | `update_doc` | Update an existing documentation entry |
62
+ | `search_docs` | Search docs by keyword or semantic similarity |
63
+ | `check_freshness` | Check if a doc is up-to-date relative to its tracked files |
64
+ | `refresh_file_snapshot` | Mark files as up-to-date without editing the doc content |
65
+ | `list_projects` | List all documentation projects |
66
+ | `set_project` | Switch to a different project |
67
+
68
+ ## Data Storage
69
+
70
+ Documentation is stored locally in a `data/` directory next to the server process. This directory is **not** included in the npm package — it is created at runtime in the working directory.
71
+
72
+ ## License
73
+
74
+ ISC
@@ -0,0 +1,112 @@
1
+ import { z } from "zod";
2
+ export declare const TrackedFileSchema: z.ZodObject<{
3
+ path: z.ZodString;
4
+ lastModifiedAt: z.ZodString;
5
+ contentHash: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ export type TrackedFile = z.infer<typeof TrackedFileSchema>;
8
+ export declare const DocSchema: z.ZodObject<{
9
+ id: z.ZodString;
10
+ title: z.ZodString;
11
+ content: z.ZodString;
12
+ tracked_files: z.ZodDefault<z.ZodArray<z.ZodObject<{
13
+ path: z.ZodString;
14
+ lastModifiedAt: z.ZodString;
15
+ contentHash: z.ZodOptional<z.ZodString>;
16
+ }, z.core.$strip>>>;
17
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
18
+ folder: z.ZodOptional<z.ZodString>;
19
+ related_docs: z.ZodDefault<z.ZodArray<z.ZodString>>;
20
+ createdAt: z.ZodString;
21
+ updatedAt: z.ZodString;
22
+ }, z.core.$strip>;
23
+ export type DocEntry = z.infer<typeof DocSchema>;
24
+ export interface DocSummary {
25
+ id: string;
26
+ title: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ tags: string[];
30
+ isStale: boolean;
31
+ folder?: string;
32
+ }
33
+ export interface DocMatch {
34
+ id: string;
35
+ title: string;
36
+ score: number;
37
+ excerpt: string;
38
+ /** Set when the hit comes from a known project scope (e.g. multi-project search). */
39
+ project?: string;
40
+ }
41
+ export declare const FreshnessReportSchema: z.ZodObject<{
42
+ status: z.ZodEnum<{
43
+ up_to_date: "up_to_date";
44
+ stale: "stale";
45
+ no_tracking: "no_tracking";
46
+ }>;
47
+ modified: z.ZodArray<z.ZodString>;
48
+ missing: z.ZodArray<z.ZodString>;
49
+ unchanged: z.ZodArray<z.ZodString>;
50
+ total: z.ZodNumber;
51
+ }, z.core.$strip>;
52
+ export type FreshnessReport = z.infer<typeof FreshnessReportSchema>;
53
+ export declare const CreateDocArgsSchema: z.ZodObject<{
54
+ title: z.ZodString;
55
+ content: z.ZodString;
56
+ tracked_files: z.ZodOptional<z.ZodArray<z.ZodString>>;
57
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
+ folder: z.ZodOptional<z.ZodString>;
59
+ related_docs: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ }, z.core.$strip>;
61
+ export declare const UpdateDocArgsSchema: z.ZodObject<{
62
+ path: z.ZodString;
63
+ content: z.ZodOptional<z.ZodString>;
64
+ tracked_files: z.ZodOptional<z.ZodArray<z.ZodString>>;
65
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
66
+ related_docs: z.ZodOptional<z.ZodArray<z.ZodString>>;
67
+ }, z.core.$strip>;
68
+ export declare const GetDocArgsSchema: z.ZodObject<{
69
+ path: z.ZodString;
70
+ }, z.core.$strip>;
71
+ export declare const MoveDocArgsSchema: z.ZodObject<{
72
+ path: z.ZodString;
73
+ new_path: z.ZodString;
74
+ }, z.core.$strip>;
75
+ export declare const CheckFreshnessArgsSchema: z.ZodObject<{
76
+ path: z.ZodOptional<z.ZodString>;
77
+ project: z.ZodOptional<z.ZodString>;
78
+ }, z.core.$strip>;
79
+ export declare const SearchDocsArgsSchema: z.ZodObject<{
80
+ query: z.ZodString;
81
+ top_k: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
82
+ }, z.core.$strip>;
83
+ export declare const RefreshFileSnapshotArgsSchema: z.ZodObject<{
84
+ paths: z.ZodArray<z.ZodString>;
85
+ file_paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
86
+ }, z.core.$strip>;
87
+ export declare const GetDocMetadataArgsSchema: z.ZodObject<{
88
+ path: z.ZodString;
89
+ }, z.core.$strip>;
90
+ export declare const PatchDocArgsSchema: z.ZodObject<{
91
+ path: z.ZodString;
92
+ search: z.ZodString;
93
+ replacement: z.ZodString;
94
+ }, z.core.$strip>;
95
+ export declare const CheckLinksArgsSchema: z.ZodObject<{
96
+ project: z.ZodOptional<z.ZodString>;
97
+ }, z.core.$strip>;
98
+ export declare const GitDiffDocArgsSchema: z.ZodObject<{
99
+ paths: z.ZodArray<z.ZodString>;
100
+ file_paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
101
+ }, z.core.$strip>;
102
+ export declare const SetProjectArgsSchema: z.ZodObject<{
103
+ project: z.ZodString;
104
+ }, z.core.$strip>;
105
+ export declare const IPCMessageSchema: z.ZodObject<{
106
+ type: z.ZodString;
107
+ correlationId: z.ZodOptional<z.ZodString>;
108
+ data: z.ZodOptional<z.ZodAny>;
109
+ error: z.ZodOptional<z.ZodString>;
110
+ timestamp: z.ZodString;
111
+ }, z.core.$strip>;
112
+ export type IPCMessage = z.infer<typeof IPCMessageSchema>;
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ export const TrackedFileSchema = z.object({
3
+ path: z.string(),
4
+ lastModifiedAt: z.string(),
5
+ contentHash: z.string().optional(),
6
+ });
7
+ export const DocSchema = z.object({
8
+ id: z.string(),
9
+ title: z.string(),
10
+ content: z.string(),
11
+ tracked_files: z.array(TrackedFileSchema).default([]),
12
+ tags: z.array(z.string()).default([]),
13
+ folder: z.string().optional(),
14
+ related_docs: z.array(z.string()).default([]),
15
+ createdAt: z.string(),
16
+ updatedAt: z.string(),
17
+ });
18
+ export const FreshnessReportSchema = z.object({
19
+ status: z.enum(["up_to_date", "stale", "no_tracking"]),
20
+ modified: z.array(z.string()),
21
+ missing: z.array(z.string()),
22
+ unchanged: z.array(z.string()),
23
+ total: z.number(),
24
+ });
25
+ export const CreateDocArgsSchema = z.object({
26
+ title: z.string().describe("Title of the documentation entry"),
27
+ content: z.string().describe("Documentation content in markdown"),
28
+ tracked_files: z.array(z.string()).optional().describe("File paths to track for staleness detection"),
29
+ tags: z.array(z.string()).optional().describe("Tags for categorization"),
30
+ folder: z.string().optional().describe("Folder path for organization, e.g. 'frontend/components'"),
31
+ related_docs: z.array(z.string()).optional().describe("IDs of related documentation entries"),
32
+ });
33
+ export const UpdateDocArgsSchema = z.object({
34
+ path: z.string().describe("Path of the documentation entry to update, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
35
+ content: z.string().optional().describe("New content (replaces existing)"),
36
+ tracked_files: z.array(z.string()).optional().describe("New list of file paths (replaces existing)"),
37
+ tags: z.array(z.string()).optional().describe("New tags (replaces existing)"),
38
+ related_docs: z.array(z.string()).optional().describe("Paths of related documentation entries (replaces existing)"),
39
+ });
40
+ export const GetDocArgsSchema = z.object({
41
+ path: z.string().describe("Path of the documentation entry, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
42
+ });
43
+ export const MoveDocArgsSchema = z.object({
44
+ path: z.string().describe("Current path of the documentation entry, e.g. 'backend/auth/authentication'"),
45
+ new_path: z.string().describe("New path for the documentation entry, e.g. 'backend/v2/authentication'. Changes folder and/or name."),
46
+ });
47
+ export const CheckFreshnessArgsSchema = z.object({
48
+ path: z.string().optional().describe("Path of a single documentation entry to check, e.g. 'backend/auth/authentication'. If omitted, checks all docs in the project."),
49
+ project: z.string().optional().describe("Project to scan (defaults to current project). Only used when path is omitted."),
50
+ });
51
+ export const SearchDocsArgsSchema = z.object({
52
+ query: z.string().describe("Search query in natural language"),
53
+ top_k: z.number().optional().default(5).describe("Number of results to return"),
54
+ });
55
+ export const RefreshFileSnapshotArgsSchema = z.object({
56
+ paths: z.array(z.string()).min(1).describe("Paths of documentation entries to refresh, e.g. ['backend/auth/authentication', 'setup']"),
57
+ file_paths: z.array(z.string()).optional().describe("Specific tracked file paths to refresh (refreshes all tracked files if omitted)"),
58
+ });
59
+ export const GetDocMetadataArgsSchema = z.object({
60
+ path: z.string().describe("Path of the documentation entry, e.g. 'backend/auth/authentication'"),
61
+ });
62
+ export const PatchDocArgsSchema = z.object({
63
+ path: z.string().describe("Path of the documentation entry to patch, e.g. 'backend/auth/authentication'"),
64
+ search: z.string().describe("Exact text to find in the current content"),
65
+ replacement: z.string().describe("Text to replace it with"),
66
+ });
67
+ export const CheckLinksArgsSchema = z.object({
68
+ project: z.string().optional().describe("Project to check (defaults to current project)"),
69
+ });
70
+ export const GitDiffDocArgsSchema = z.object({
71
+ paths: z.array(z.string()).min(1).describe("Paths of documentation entries to diff, e.g. ['backend/auth/authentication']"),
72
+ file_paths: z.array(z.string()).optional().describe("Specific tracked file paths to diff (all tracked files if omitted)"),
73
+ });
74
+ export const SetProjectArgsSchema = z.object({
75
+ project: z.string().describe("Project name to switch to (created if it does not exist)"),
76
+ });
77
+ export const IPCMessageSchema = z.object({
78
+ type: z.string(),
79
+ correlationId: z.string().optional(),
80
+ data: z.any().optional(),
81
+ error: z.string().optional(),
82
+ timestamp: z.string(),
83
+ });
@@ -0,0 +1,23 @@
1
+ import type { EmbeddingStore } from "./embedding-store.js";
2
+ import type { DocMatch } from "../../shared/schemas/doc.schema.js";
3
+ export declare class EmbeddingEngine {
4
+ private client;
5
+ private model;
6
+ private store;
7
+ constructor(store: EmbeddingStore);
8
+ isConfigured(): boolean;
9
+ private refreshClient;
10
+ generateEmbedding(text: string): Promise<number[] | null>;
11
+ cosineSimilarity(a: number[], b: number[]): number;
12
+ saveEmbedding(docId: string, embedding: number[], project: string): Promise<void>;
13
+ removeEmbedding(docId: string, project: string): Promise<void>;
14
+ moveEmbedding(oldDocId: string, newDocId: string, project: string): Promise<void>;
15
+ search(query: string, project: string, topK: number, getDocContent: (id: string) => Promise<{
16
+ title: string;
17
+ content: string;
18
+ } | null>): Promise<DocMatch[]>;
19
+ searchAcrossProjects(query: string, projects: string[], topK: number, getDocContent: (project: string, docId: string) => Promise<{
20
+ title: string;
21
+ content: string;
22
+ } | null>): Promise<DocMatch[]>;
23
+ }
@@ -0,0 +1,119 @@
1
+ import OpenAI from "openai";
2
+ import { createLogger } from "@imenam/mcp-gui-interface";
3
+ const logger = createLogger("EmbeddingEngine");
4
+ export class EmbeddingEngine {
5
+ client = null;
6
+ model;
7
+ store;
8
+ constructor(store) {
9
+ this.store = store;
10
+ this.model = process.env.MCP_DOCUMENTOR_OPENAI_MODEL || "text-embedding-3-small";
11
+ const apiKey = process.env.MCP_DOCUMENTOR_OPENAI_API_KEY;
12
+ if (apiKey && apiKey.trim() !== "") {
13
+ this.client = new OpenAI({ apiKey });
14
+ }
15
+ }
16
+ isConfigured() {
17
+ return this.client !== null;
18
+ }
19
+ refreshClient() {
20
+ const apiKey = process.env.MCP_DOCUMENTOR_OPENAI_API_KEY;
21
+ this.model = process.env.MCP_DOCUMENTOR_OPENAI_MODEL || "text-embedding-3-small";
22
+ if (apiKey && apiKey.trim() !== "") {
23
+ this.client = new OpenAI({ apiKey });
24
+ }
25
+ else {
26
+ this.client = null;
27
+ }
28
+ }
29
+ async generateEmbedding(text) {
30
+ this.refreshClient();
31
+ if (!this.client)
32
+ return null;
33
+ try {
34
+ const response = await this.client.embeddings.create({
35
+ model: this.model,
36
+ input: text,
37
+ });
38
+ return response.data[0].embedding;
39
+ }
40
+ catch (err) {
41
+ logger.error(`Failed to generate embedding: ${err.message}`);
42
+ return null;
43
+ }
44
+ }
45
+ cosineSimilarity(a, b) {
46
+ if (a.length !== b.length)
47
+ return 0;
48
+ let dotProduct = 0;
49
+ let normA = 0;
50
+ let normB = 0;
51
+ for (let i = 0; i < a.length; i++) {
52
+ dotProduct += a[i] * b[i];
53
+ normA += a[i] * a[i];
54
+ normB += b[i] * b[i];
55
+ }
56
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
57
+ if (denominator === 0)
58
+ return 0;
59
+ return dotProduct / denominator;
60
+ }
61
+ async saveEmbedding(docId, embedding, project) {
62
+ this.store.save(project, docId, embedding);
63
+ logger.debug(`Embedding saved for doc "${docId}" in project "${project}"`);
64
+ }
65
+ async removeEmbedding(docId, project) {
66
+ this.store.remove(project, docId);
67
+ logger.debug(`Embedding removed for doc "${docId}" in project "${project}"`);
68
+ }
69
+ async moveEmbedding(oldDocId, newDocId, project) {
70
+ this.store.move(project, oldDocId, newDocId);
71
+ logger.debug(`Embedding moved: "${oldDocId}" → "${newDocId}" in project "${project}"`);
72
+ }
73
+ async search(query, project, topK, getDocContent) {
74
+ return this.searchAcrossProjects(query, [project], topK, async (_proj, docId) => getDocContent(docId));
75
+ }
76
+ async searchAcrossProjects(query, projects, topK, getDocContent) {
77
+ this.refreshClient();
78
+ if (!this.client) {
79
+ throw new Error("OpenAI API key is not configured. Set MCP_DOCUMENTOR_OPENAI_API_KEY in your .env file to enable semantic search.");
80
+ }
81
+ const queryEmbedding = await this.generateEmbedding(query);
82
+ if (!queryEmbedding) {
83
+ throw new Error("Failed to generate embedding for search query.");
84
+ }
85
+ const allEntries = this.store.loadAllProjects(projects);
86
+ const seenKeys = new Set();
87
+ const scored = [];
88
+ for (const { project, docId, embedding } of allEntries) {
89
+ const dedupeKey = `${project}::${docId}`;
90
+ if (seenKeys.has(dedupeKey))
91
+ continue;
92
+ seenKeys.add(dedupeKey);
93
+ scored.push({
94
+ project,
95
+ id: docId,
96
+ score: this.cosineSimilarity(queryEmbedding, embedding),
97
+ });
98
+ }
99
+ if (scored.length === 0)
100
+ return [];
101
+ scored.sort((a, b) => b.score - a.score);
102
+ const topResults = scored.slice(0, topK);
103
+ const matches = [];
104
+ const multiProject = projects.length > 1;
105
+ for (const result of topResults) {
106
+ const doc = await getDocContent(result.project, result.id);
107
+ if (doc) {
108
+ matches.push({
109
+ id: result.id,
110
+ title: doc.title,
111
+ score: Math.round(result.score * 100) / 100,
112
+ excerpt: doc.content.substring(0, 200),
113
+ ...(multiProject ? { project: result.project } : {}),
114
+ });
115
+ }
116
+ }
117
+ return matches;
118
+ }
119
+ }
@@ -0,0 +1,15 @@
1
+ export declare class EmbeddingStore {
2
+ private db;
3
+ init(dataDir: string, model: string): void;
4
+ private handleModelDrift;
5
+ private getDb;
6
+ save(project: string, docId: string, embedding: number[]): void;
7
+ remove(project: string, docId: string): void;
8
+ move(project: string, oldDocId: string, newDocId: string): void;
9
+ loadAll(project: string): Map<string, number[]>;
10
+ loadAllProjects(projects: string[]): Array<{
11
+ project: string;
12
+ docId: string;
13
+ embedding: number[];
14
+ }>;
15
+ }
@@ -0,0 +1,97 @@
1
+ import { createRequire } from "module";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import { createLogger } from "@imenam/mcp-gui-interface";
5
+ const require = createRequire(import.meta.url);
6
+ const Database = require("better-sqlite3");
7
+ const logger = createLogger("EmbeddingStore");
8
+ const DB_FILENAME = "embeddings.db";
9
+ const META_MODEL_KEY = "embedding_model";
10
+ function embeddingToBuffer(embedding) {
11
+ return Buffer.from(new Float32Array(embedding).buffer);
12
+ }
13
+ function bufferToEmbedding(buffer) {
14
+ return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
15
+ }
16
+ export class EmbeddingStore {
17
+ db = null;
18
+ init(dataDir, model) {
19
+ if (this.db)
20
+ return;
21
+ fs.mkdirSync(dataDir, { recursive: true });
22
+ this.db = new Database(path.join(dataDir, DB_FILENAME));
23
+ this.db.pragma("journal_mode = WAL");
24
+ this.db.pragma("synchronous = NORMAL");
25
+ this.db.exec(`
26
+ CREATE TABLE IF NOT EXISTS embeddings (
27
+ project TEXT NOT NULL,
28
+ doc_id TEXT NOT NULL,
29
+ embedding BLOB NOT NULL,
30
+ PRIMARY KEY (project, doc_id)
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS meta (
34
+ key TEXT PRIMARY KEY,
35
+ value TEXT NOT NULL
36
+ );
37
+ `);
38
+ this.handleModelDrift(model);
39
+ const { c } = this.db.prepare("SELECT COUNT(*) as c FROM embeddings").get();
40
+ logger.info(`EmbeddingStore initialized — ${c} embeddings in store`);
41
+ }
42
+ handleModelDrift(currentModel) {
43
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(META_MODEL_KEY);
44
+ if (row && row.value !== currentModel) {
45
+ logger.warn(`Embedding model changed from "${row.value}" to "${currentModel}" — clearing embedding store`);
46
+ this.db.prepare("DELETE FROM embeddings").run();
47
+ }
48
+ this.db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(META_MODEL_KEY, currentModel);
49
+ }
50
+ getDb() {
51
+ if (!this.db)
52
+ throw new Error("EmbeddingStore not initialized. Call init() first.");
53
+ return this.db;
54
+ }
55
+ save(project, docId, embedding) {
56
+ const buf = embeddingToBuffer(embedding);
57
+ this.getDb()
58
+ .prepare("INSERT OR REPLACE INTO embeddings (project, doc_id, embedding) VALUES (?, ?, ?)")
59
+ .run(project, docId, buf);
60
+ logger.debug(`Embedding saved: [${project}] ${docId}`);
61
+ }
62
+ remove(project, docId) {
63
+ this.getDb()
64
+ .prepare("DELETE FROM embeddings WHERE project = ? AND doc_id = ?")
65
+ .run(project, docId);
66
+ logger.debug(`Embedding removed: [${project}] ${docId}`);
67
+ }
68
+ move(project, oldDocId, newDocId) {
69
+ this.getDb()
70
+ .prepare("UPDATE embeddings SET doc_id = ? WHERE project = ? AND doc_id = ?")
71
+ .run(newDocId, project, oldDocId);
72
+ logger.debug(`Embedding moved: [${project}] ${oldDocId} → ${newDocId}`);
73
+ }
74
+ loadAll(project) {
75
+ const rows = this.getDb()
76
+ .prepare("SELECT doc_id, embedding FROM embeddings WHERE project = ?")
77
+ .all(project);
78
+ const result = new Map();
79
+ for (const row of rows) {
80
+ result.set(row.doc_id, bufferToEmbedding(row.embedding));
81
+ }
82
+ return result;
83
+ }
84
+ loadAllProjects(projects) {
85
+ if (projects.length === 0)
86
+ return [];
87
+ const placeholders = projects.map(() => "?").join(", ");
88
+ const rows = this.getDb()
89
+ .prepare(`SELECT project, doc_id, embedding FROM embeddings WHERE project IN (${placeholders})`)
90
+ .all(...projects);
91
+ return rows.map((row) => ({
92
+ project: row.project,
93
+ docId: row.doc_id,
94
+ embedding: bufferToEmbedding(row.embedding),
95
+ }));
96
+ }
97
+ }
@@ -0,0 +1,3 @@
1
+ import type { EmbeddingStore } from "./embedding-store.js";
2
+ import type { StorageEngine } from "./storage-engine.js";
3
+ export declare function migrateEmbeddings(embeddingStore: EmbeddingStore, storageEngine: StorageEngine): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { createLogger } from "@imenam/mcp-gui-interface";
4
+ const logger = createLogger("MigrateEmbeddings");
5
+ export async function migrateEmbeddings(embeddingStore, storageEngine) {
6
+ const config = storageEngine.getConfig();
7
+ for (const project of config.projects) {
8
+ const projectDir = storageEngine.getProjectDir(project);
9
+ const jsonPath = path.join(projectDir, "embeddings.json");
10
+ if (!fs.existsSync(jsonPath))
11
+ continue;
12
+ try {
13
+ const raw = fs.readFileSync(jsonPath, "utf-8");
14
+ const store = JSON.parse(raw);
15
+ const entries = Object.entries(store);
16
+ if (entries.length === 0) {
17
+ fs.unlinkSync(jsonPath);
18
+ continue;
19
+ }
20
+ for (const [docId, embedding] of entries) {
21
+ embeddingStore.save(project, docId, embedding);
22
+ }
23
+ fs.unlinkSync(jsonPath);
24
+ logger.info(`Migrated ${entries.length} embeddings for project "${project}" from JSON to SQLite`);
25
+ }
26
+ catch (err) {
27
+ logger.warn(`Failed to migrate embeddings for project "${project}": ${err.message}`);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { StorageEngine } from "./storage-engine.js";
2
+ export declare function migrateFolders(storageEngine: StorageEngine): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import matter from "gray-matter";
4
+ import { createLogger } from "@imenam/mcp-gui-interface";
5
+ const logger = createLogger("MigrateFolders");
6
+ export async function migrateFolders(storageEngine) {
7
+ const config = storageEngine.getConfig();
8
+ if (config.foldersMigrated) {
9
+ logger.debug("Folder migration already done, skipping.");
10
+ return;
11
+ }
12
+ logger.info("Starting folder migration (virtual frontmatter → real disk folders)...");
13
+ let totalMoved = 0;
14
+ for (const project of config.projects) {
15
+ const projectDir = storageEngine.getProjectDir(project);
16
+ if (!fs.existsSync(projectDir))
17
+ continue;
18
+ const flatMdFiles = fs
19
+ .readdirSync(projectDir, { withFileTypes: true })
20
+ .filter((e) => e.isFile() && e.name.endsWith(".md"))
21
+ .map((e) => path.join(projectDir, e.name));
22
+ for (const filePath of flatMdFiles) {
23
+ try {
24
+ const raw = fs.readFileSync(filePath, "utf-8");
25
+ const { data } = matter(raw);
26
+ if (!data.folder)
27
+ continue;
28
+ const targetDir = path.join(projectDir, ...String(data.folder).split("/"));
29
+ const targetPath = path.join(targetDir, path.basename(filePath));
30
+ if (targetPath === filePath)
31
+ continue;
32
+ fs.mkdirSync(targetDir, { recursive: true });
33
+ fs.renameSync(filePath, targetPath);
34
+ logger.info(`Migrated: ${path.basename(filePath)} → ${data.folder}/`);
35
+ totalMoved++;
36
+ }
37
+ catch (err) {
38
+ logger.warn(`Failed to migrate ${filePath}: ${err.message}`);
39
+ }
40
+ }
41
+ }
42
+ config.foldersMigrated = true;
43
+ storageEngine.saveConfig(config);
44
+ logger.info(`Folder migration complete: ${totalMoved} file(s) moved.`);
45
+ }
@@ -0,0 +1,29 @@
1
+ import type { DocEntry, DocSummary } from "../../shared/schemas/doc.schema.js";
2
+ interface GlobalConfig {
3
+ currentProject: string;
4
+ projects: string[];
5
+ foldersMigrated?: boolean;
6
+ }
7
+ export declare class StorageEngine {
8
+ private rootDir;
9
+ constructor(rootDir: string);
10
+ getDataDir(): string;
11
+ getProjectDir(project?: string): string;
12
+ private getConfigPath;
13
+ getConfig(): GlobalConfig;
14
+ saveConfig(config: GlobalConfig): void;
15
+ ensureProject(project: string): void;
16
+ private walkMdFiles;
17
+ private docIdToFilePath;
18
+ private cleanEmptyDirs;
19
+ saveDoc(doc: DocEntry, project?: string): Promise<void>;
20
+ getDoc(id: string, project?: string): Promise<DocEntry | null>;
21
+ listDocs(project?: string): Promise<DocSummary[]>;
22
+ resolveDoc(id: string): Promise<{
23
+ doc: DocEntry;
24
+ project: string;
25
+ } | null>;
26
+ deleteDoc(id: string, project?: string): Promise<boolean>;
27
+ moveDoc(oldId: string, newId: string, project?: string): Promise<boolean>;
28
+ }
29
+ export {};