@synth-coder/memhub 0.2.3 → 0.2.5

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 (118) hide show
  1. package/.github/workflows/ci.yml +48 -12
  2. package/.github/workflows/release.yml +70 -0
  3. package/AGENTS.md +71 -73
  4. package/README.md +284 -195
  5. package/README.zh-CN.md +90 -30
  6. package/dist/src/cli/agents/claude-code.d.ts +5 -0
  7. package/dist/src/cli/agents/claude-code.d.ts.map +1 -0
  8. package/dist/src/cli/agents/claude-code.js +14 -0
  9. package/dist/src/cli/agents/claude-code.js.map +1 -0
  10. package/dist/src/cli/agents/cline.d.ts +5 -0
  11. package/dist/src/cli/agents/cline.d.ts.map +1 -0
  12. package/dist/src/cli/agents/cline.js +14 -0
  13. package/dist/src/cli/agents/cline.js.map +1 -0
  14. package/dist/src/cli/agents/cursor.d.ts +5 -0
  15. package/dist/src/cli/agents/cursor.d.ts.map +1 -0
  16. package/dist/src/cli/agents/cursor.js +14 -0
  17. package/dist/src/cli/agents/cursor.js.map +1 -0
  18. package/dist/src/cli/agents/factory-droid.d.ts +5 -0
  19. package/dist/src/cli/agents/factory-droid.d.ts.map +1 -0
  20. package/dist/src/cli/agents/factory-droid.js +14 -0
  21. package/dist/src/cli/agents/factory-droid.js.map +1 -0
  22. package/dist/src/cli/agents/gemini-cli.d.ts +5 -0
  23. package/dist/src/cli/agents/gemini-cli.d.ts.map +1 -0
  24. package/dist/src/cli/agents/gemini-cli.js +14 -0
  25. package/dist/src/cli/agents/gemini-cli.js.map +1 -0
  26. package/dist/src/cli/agents/index.d.ts +13 -0
  27. package/dist/src/cli/agents/index.d.ts.map +1 -0
  28. package/dist/src/cli/agents/index.js +27 -0
  29. package/dist/src/cli/agents/index.js.map +1 -0
  30. package/dist/src/cli/agents/windsurf.d.ts +5 -0
  31. package/dist/src/cli/agents/windsurf.d.ts.map +1 -0
  32. package/dist/src/cli/agents/windsurf.js +14 -0
  33. package/dist/src/cli/agents/windsurf.js.map +1 -0
  34. package/dist/src/cli/index.d.ts +8 -0
  35. package/dist/src/cli/index.d.ts.map +1 -0
  36. package/dist/src/cli/index.js +168 -0
  37. package/dist/src/cli/index.js.map +1 -0
  38. package/dist/src/cli/init.d.ts +34 -0
  39. package/dist/src/cli/init.d.ts.map +1 -0
  40. package/dist/src/cli/init.js +140 -0
  41. package/dist/src/cli/init.js.map +1 -0
  42. package/dist/src/cli/instructions.d.ts +29 -0
  43. package/dist/src/cli/instructions.d.ts.map +1 -0
  44. package/dist/src/cli/instructions.js +141 -0
  45. package/dist/src/cli/instructions.js.map +1 -0
  46. package/dist/src/cli/types.d.ts +22 -0
  47. package/dist/src/cli/types.d.ts.map +1 -0
  48. package/dist/src/cli/types.js +75 -0
  49. package/dist/src/cli/types.js.map +1 -0
  50. package/dist/src/contracts/mcp.js +34 -34
  51. package/dist/src/contracts/schemas.js.map +1 -1
  52. package/dist/src/server/mcp-server.d.ts.map +1 -1
  53. package/dist/src/server/mcp-server.js +7 -14
  54. package/dist/src/server/mcp-server.js.map +1 -1
  55. package/dist/src/services/embedding-service.d.ts.map +1 -1
  56. package/dist/src/services/embedding-service.js +1 -1
  57. package/dist/src/services/embedding-service.js.map +1 -1
  58. package/dist/src/services/memory-service.d.ts.map +1 -1
  59. package/dist/src/services/memory-service.js.map +1 -1
  60. package/dist/src/storage/markdown-storage.d.ts.map +1 -1
  61. package/dist/src/storage/markdown-storage.js +1 -1
  62. package/dist/src/storage/markdown-storage.js.map +1 -1
  63. package/dist/src/storage/vector-index.d.ts.map +1 -1
  64. package/dist/src/storage/vector-index.js +4 -5
  65. package/dist/src/storage/vector-index.js.map +1 -1
  66. package/docs/README.md +21 -0
  67. package/docs/mcp-tools.md +136 -0
  68. package/docs/user-guide.md +184 -0
  69. package/package.json +61 -59
  70. package/src/cli/agents/claude-code.ts +14 -0
  71. package/src/cli/agents/cline.ts +14 -0
  72. package/src/cli/agents/codex.ts +14 -0
  73. package/src/cli/agents/cursor.ts +14 -0
  74. package/src/cli/agents/factory-droid.ts +14 -0
  75. package/src/cli/agents/gemini-cli.ts +14 -0
  76. package/src/cli/agents/index.ts +36 -0
  77. package/src/cli/agents/windsurf.ts +14 -0
  78. package/src/cli/index.ts +192 -0
  79. package/src/cli/init.ts +218 -0
  80. package/src/cli/instructions.ts +156 -0
  81. package/src/cli/types.ts +112 -0
  82. package/src/contracts/index.ts +12 -12
  83. package/src/contracts/mcp.ts +223 -223
  84. package/src/contracts/schemas.ts +307 -307
  85. package/src/contracts/types.ts +410 -410
  86. package/src/index.ts +8 -8
  87. package/src/server/index.ts +5 -5
  88. package/src/server/mcp-server.ts +169 -186
  89. package/src/services/embedding-service.ts +114 -114
  90. package/src/services/index.ts +5 -5
  91. package/src/services/memory-service.ts +656 -663
  92. package/src/storage/frontmatter-parser.ts +243 -243
  93. package/src/storage/index.ts +6 -6
  94. package/src/storage/markdown-storage.ts +228 -236
  95. package/src/storage/vector-index.ts +159 -160
  96. package/src/utils/index.ts +5 -5
  97. package/src/utils/slugify.ts +63 -63
  98. package/test/cli/init.test.ts +402 -0
  99. package/test/contracts/schemas.test.ts +313 -313
  100. package/test/contracts/types.test.ts +21 -21
  101. package/test/frontmatter-parser-more.test.ts +94 -94
  102. package/test/server/mcp-server.test.ts +211 -210
  103. package/test/services/memory-service-edge.test.ts +248 -248
  104. package/test/services/memory-service.test.ts +291 -279
  105. package/test/storage/frontmatter-parser.test.ts +223 -223
  106. package/test/storage/markdown-storage.test.ts +226 -217
  107. package/test/storage/storage-edge.test.ts +238 -238
  108. package/test/storage/vector-index.test.ts +149 -153
  109. package/test/utils/slugify-edge.test.ts +94 -94
  110. package/test/utils/slugify.test.ts +72 -68
  111. package/docs/architecture-diagrams.md +0 -368
  112. package/docs/architecture.md +0 -381
  113. package/docs/contracts.md +0 -190
  114. package/docs/prompt-template.md +0 -33
  115. package/docs/proposals/mcp-typescript-sdk-refactor.md +0 -568
  116. package/docs/proposals/proposal-close-gates.md +0 -58
  117. package/docs/tool-calling-policy.md +0 -101
  118. package/docs/vector-search.md +0 -306
@@ -1,160 +1,159 @@
1
- /**
2
- * VectorIndex - LanceDB-backed vector search index for memories.
3
- *
4
- * This is a search cache only. Markdown files remain the source of truth.
5
- * The index can be rebuilt from Markdown files at any time.
6
- */
7
-
8
- import * as lancedb from '@lancedb/lancedb';
9
- import { mkdir, access } from 'fs/promises';
10
- import { join } from 'path';
11
- import { constants } from 'fs';
12
- import type { Memory } from '../contracts/types.js';
13
-
14
- const TABLE_NAME = 'memories';
15
-
16
- /** Escape single quotes in id strings to prevent SQL injection */
17
- function escapeId(id: string): string {
18
- return id.replace(/'/g, "''");
19
- }
20
-
21
- /**
22
- * Row stored in the LanceDB table.
23
- * The `vector` field is the only one required by LanceDB; all others are metadata filters.
24
- */
25
- export interface VectorRow {
26
- id: string;
27
- vector: number[];
28
- title: string;
29
- category: string;
30
- tags: string; // JSON-serialised string[]
31
- importance: number;
32
- createdAt: string;
33
- updatedAt: string;
34
- }
35
-
36
- export interface VectorSearchResult {
37
- id: string;
38
- /** Cosine distance (lower = more similar). Converted to 0-1 score by caller. */
39
- _distance: number;
40
- }
41
-
42
- /**
43
- * LanceDB vector index wrapper.
44
- * Data lives at `{storagePath}/.lancedb/`.
45
- */
46
- export class VectorIndex {
47
- private readonly dbPath: string;
48
- private db: lancedb.Connection | null = null;
49
- private table: lancedb.Table | null = null;
50
- private initPromise: Promise<void> | null = null;
51
-
52
- constructor(storagePath: string) {
53
- this.dbPath = join(storagePath, '.lancedb');
54
- }
55
-
56
- /** Idempotent initialisation — safe to call multiple times. */
57
- async initialize(): Promise<void> {
58
- if (this.table) return;
59
-
60
- if (!this.initPromise) {
61
- this.initPromise = this._init();
62
- }
63
- await this.initPromise;
64
- }
65
-
66
- private async _init(): Promise<void> {
67
- // Ensure the directory exists
68
- try {
69
- await access(this.dbPath, constants.F_OK);
70
- } catch {
71
- await mkdir(this.dbPath, { recursive: true });
72
- }
73
-
74
- this.db = await lancedb.connect(this.dbPath);
75
-
76
- const existingTables = await this.db.tableNames();
77
- if (existingTables.includes(TABLE_NAME)) {
78
- this.table = await this.db.openTable(TABLE_NAME);
79
- } else {
80
- // Create table with a dummy row so schema is established, then delete it
81
- const dummy: VectorRow = {
82
- id: '__init__',
83
- vector: new Array(384).fill(0) as number[],
84
- title: '',
85
- category: '',
86
- tags: '[]',
87
- importance: 0,
88
- createdAt: '',
89
- updatedAt: '',
90
- };
91
- // LanceDB expects Record<string, unknown>[] but our VectorRow is typed more strictly
92
- // Cast is safe here as VectorRow is a subset of Record<string, unknown>
93
- this.table = await this.db.createTable(TABLE_NAME, [dummy as unknown as Record<string, unknown>]);
94
- await this.table.delete(`id = '__init__'`);
95
- }
96
- }
97
-
98
- /**
99
- * Upserts a memory row into the index.
100
- * LanceDB doesn't have a native upsert so we delete-then-add.
101
- */
102
- async upsert(memory: Memory, vector: number[]): Promise<void> {
103
- await this.initialize();
104
- const table = this.table!;
105
-
106
- // Remove existing row (if any)
107
- await table.delete(`id = '${escapeId(memory.id)}'`);
108
-
109
- const row: VectorRow = {
110
- id: memory.id,
111
- vector,
112
- title: memory.title,
113
- category: memory.category,
114
- tags: JSON.stringify(memory.tags),
115
- importance: memory.importance,
116
- createdAt: memory.createdAt,
117
- updatedAt: memory.updatedAt,
118
- };
119
-
120
- // LanceDB expects Record<string, unknown>[] but our VectorRow is typed more strictly
121
- await table.add([row as unknown as Record<string, unknown>]);
122
- }
123
-
124
- /**
125
- * Removes a memory from the index by ID.
126
- */
127
- async delete(id: string): Promise<void> {
128
- await this.initialize();
129
- await this.table!.delete(`id = '${escapeId(id)}'`);
130
- }
131
-
132
- /**
133
- * Searches for the nearest neighbours to `vector`.
134
- *
135
- * @param vector - Query embedding (must be 384-dim)
136
- * @param limit - Max results to return
137
- * @returns Array ordered by ascending distance (most similar first)
138
- */
139
- async search(vector: number[], limit = 10): Promise<VectorSearchResult[]> {
140
- await this.initialize();
141
-
142
- const results = await this.table!
143
- .vectorSearch(vector)
144
- .limit(limit)
145
- .toArray();
146
-
147
- return results.map((row: Record<string, unknown>) => ({
148
- id: row['id'] as string,
149
- _distance: row['_distance'] as number,
150
- }));
151
- }
152
-
153
- /**
154
- * Returns the number of rows in the index.
155
- */
156
- async count(): Promise<number> {
157
- await this.initialize();
158
- return this.table!.countRows();
159
- }
160
- }
1
+ /**
2
+ * VectorIndex - LanceDB-backed vector search index for memories.
3
+ *
4
+ * This is a search cache only. Markdown files remain the source of truth.
5
+ * The index can be rebuilt from Markdown files at any time.
6
+ */
7
+
8
+ import * as lancedb from '@lancedb/lancedb';
9
+ import { mkdir, access } from 'fs/promises';
10
+ import { join } from 'path';
11
+ import { constants } from 'fs';
12
+ import type { Memory } from '../contracts/types.js';
13
+
14
+ const TABLE_NAME = 'memories';
15
+
16
+ /** Escape single quotes in id strings to prevent SQL injection */
17
+ function escapeId(id: string): string {
18
+ return id.replace(/'/g, "''");
19
+ }
20
+
21
+ /**
22
+ * Row stored in the LanceDB table.
23
+ * The `vector` field is the only one required by LanceDB; all others are metadata filters.
24
+ */
25
+ export interface VectorRow {
26
+ id: string;
27
+ vector: number[];
28
+ title: string;
29
+ category: string;
30
+ tags: string; // JSON-serialised string[]
31
+ importance: number;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }
35
+
36
+ export interface VectorSearchResult {
37
+ id: string;
38
+ /** Cosine distance (lower = more similar). Converted to 0-1 score by caller. */
39
+ _distance: number;
40
+ }
41
+
42
+ /**
43
+ * LanceDB vector index wrapper.
44
+ * Data lives at `{storagePath}/.lancedb/`.
45
+ */
46
+ export class VectorIndex {
47
+ private readonly dbPath: string;
48
+ private db: lancedb.Connection | null = null;
49
+ private table: lancedb.Table | null = null;
50
+ private initPromise: Promise<void> | null = null;
51
+
52
+ constructor(storagePath: string) {
53
+ this.dbPath = join(storagePath, '.lancedb');
54
+ }
55
+
56
+ /** Idempotent initialisation — safe to call multiple times. */
57
+ async initialize(): Promise<void> {
58
+ if (this.table) return;
59
+
60
+ if (!this.initPromise) {
61
+ this.initPromise = this._init();
62
+ }
63
+ await this.initPromise;
64
+ }
65
+
66
+ private async _init(): Promise<void> {
67
+ // Ensure the directory exists
68
+ try {
69
+ await access(this.dbPath, constants.F_OK);
70
+ } catch {
71
+ await mkdir(this.dbPath, { recursive: true });
72
+ }
73
+
74
+ this.db = await lancedb.connect(this.dbPath);
75
+
76
+ const existingTables = await this.db.tableNames();
77
+ if (existingTables.includes(TABLE_NAME)) {
78
+ this.table = await this.db.openTable(TABLE_NAME);
79
+ } else {
80
+ // Create table with a dummy row so schema is established, then delete it
81
+ const dummy: VectorRow = {
82
+ id: '__init__',
83
+ vector: new Array(384).fill(0) as number[],
84
+ title: '',
85
+ category: '',
86
+ tags: '[]',
87
+ importance: 0,
88
+ createdAt: '',
89
+ updatedAt: '',
90
+ };
91
+ // LanceDB expects Record<string, unknown>[] but our VectorRow is typed more strictly
92
+ // Cast is safe here as VectorRow is a subset of Record<string, unknown>
93
+ this.table = await this.db.createTable(TABLE_NAME, [
94
+ dummy as unknown as Record<string, unknown>,
95
+ ]);
96
+ await this.table.delete(`id = '__init__'`);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Upserts a memory row into the index.
102
+ * LanceDB doesn't have a native upsert so we delete-then-add.
103
+ */
104
+ async upsert(memory: Memory, vector: number[]): Promise<void> {
105
+ await this.initialize();
106
+ const table = this.table!;
107
+
108
+ // Remove existing row (if any)
109
+ await table.delete(`id = '${escapeId(memory.id)}'`);
110
+
111
+ const row: VectorRow = {
112
+ id: memory.id,
113
+ vector,
114
+ title: memory.title,
115
+ category: memory.category,
116
+ tags: JSON.stringify(memory.tags),
117
+ importance: memory.importance,
118
+ createdAt: memory.createdAt,
119
+ updatedAt: memory.updatedAt,
120
+ };
121
+
122
+ // LanceDB expects Record<string, unknown>[] but our VectorRow is typed more strictly
123
+ await table.add([row as unknown as Record<string, unknown>]);
124
+ }
125
+
126
+ /**
127
+ * Removes a memory from the index by ID.
128
+ */
129
+ async delete(id: string): Promise<void> {
130
+ await this.initialize();
131
+ await this.table!.delete(`id = '${escapeId(id)}'`);
132
+ }
133
+
134
+ /**
135
+ * Searches for the nearest neighbours to `vector`.
136
+ *
137
+ * @param vector - Query embedding (must be 384-dim)
138
+ * @param limit - Max results to return
139
+ * @returns Array ordered by ascending distance (most similar first)
140
+ */
141
+ async search(vector: number[], limit = 10): Promise<VectorSearchResult[]> {
142
+ await this.initialize();
143
+
144
+ const results = await this.table!.vectorSearch(vector).limit(limit).toArray();
145
+
146
+ return results.map((row: Record<string, unknown>) => ({
147
+ id: row['id'] as string,
148
+ _distance: row['_distance'] as number,
149
+ }));
150
+ }
151
+
152
+ /**
153
+ * Returns the number of rows in the index.
154
+ */
155
+ async count(): Promise<number> {
156
+ await this.initialize();
157
+ return this.table!.countRows();
158
+ }
159
+ }
@@ -1,5 +1,5 @@
1
- /**
2
- * Utility exports
3
- */
4
-
5
- export * from './slugify.js';
1
+ /**
2
+ * Utility exports
3
+ */
4
+
5
+ export * from './slugify.js';
@@ -1,63 +1,63 @@
1
- /**
2
- * Slugify utility - Converts strings to URL-friendly slugs
3
- */
4
-
5
- /**
6
- * Converts a string to a URL-friendly slug
7
- * - Converts to lowercase
8
- * - Replaces spaces with hyphens
9
- * - Removes special characters
10
- * - Collapses multiple hyphens
11
- * - Trims leading/trailing hyphens
12
- *
13
- * @param input - The string to convert
14
- * @returns The slugified string
15
- */
16
- export function slugify(input: string): string {
17
- if (!input || input.trim().length === 0) {
18
- return 'untitled';
19
- }
20
-
21
- // Convert to lowercase and replace non-alphanumeric characters with hyphens
22
- const slug = input
23
- .toLowerCase()
24
- .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
25
- .replace(/\s+/g, '-') // Replace spaces with hyphens
26
- .replace(/-+/g, '-') // Collapse multiple hyphens
27
- .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
28
-
29
- // If empty after cleaning (e.g., only special characters), return 'untitled'
30
- if (slug.length === 0) {
31
- return 'untitled';
32
- }
33
-
34
- // Truncate to max 100 characters
35
- if (slug.length > 100) {
36
- return slug.substring(0, 100).replace(/-+$/, ''); // Don't end with hyphen
37
- }
38
-
39
- return slug;
40
- }
41
-
42
- /**
43
- * Generates a unique slug by appending a timestamp or counter if needed
44
- *
45
- * @param title - The title to slugify
46
- * @param existingSlugs - Array of existing slugs to check against
47
- * @returns A unique slug
48
- */
49
- export function generateUniqueSlug(title: string, existingSlugs: readonly string[] = []): string {
50
- const slug = slugify(title);
51
- let counter = 1;
52
- let uniqueSlug = slug;
53
-
54
- while (existingSlugs.includes(uniqueSlug)) {
55
- const suffix = `-${counter}`;
56
- const maxBaseLength = 100 - suffix.length;
57
- const baseSlug = slug.substring(0, maxBaseLength).replace(/-+$/, '');
58
- uniqueSlug = `${baseSlug}${suffix}`;
59
- counter++;
60
- }
61
-
62
- return uniqueSlug;
63
- }
1
+ /**
2
+ * Slugify utility - Converts strings to URL-friendly slugs
3
+ */
4
+
5
+ /**
6
+ * Converts a string to a URL-friendly slug
7
+ * - Converts to lowercase
8
+ * - Replaces spaces with hyphens
9
+ * - Removes special characters
10
+ * - Collapses multiple hyphens
11
+ * - Trims leading/trailing hyphens
12
+ *
13
+ * @param input - The string to convert
14
+ * @returns The slugified string
15
+ */
16
+ export function slugify(input: string): string {
17
+ if (!input || input.trim().length === 0) {
18
+ return 'untitled';
19
+ }
20
+
21
+ // Convert to lowercase and replace non-alphanumeric characters with hyphens
22
+ const slug = input
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
25
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
26
+ .replace(/-+/g, '-') // Collapse multiple hyphens
27
+ .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
28
+
29
+ // If empty after cleaning (e.g., only special characters), return 'untitled'
30
+ if (slug.length === 0) {
31
+ return 'untitled';
32
+ }
33
+
34
+ // Truncate to max 100 characters
35
+ if (slug.length > 100) {
36
+ return slug.substring(0, 100).replace(/-+$/, ''); // Don't end with hyphen
37
+ }
38
+
39
+ return slug;
40
+ }
41
+
42
+ /**
43
+ * Generates a unique slug by appending a timestamp or counter if needed
44
+ *
45
+ * @param title - The title to slugify
46
+ * @param existingSlugs - Array of existing slugs to check against
47
+ * @returns A unique slug
48
+ */
49
+ export function generateUniqueSlug(title: string, existingSlugs: readonly string[] = []): string {
50
+ const slug = slugify(title);
51
+ let counter = 1;
52
+ let uniqueSlug = slug;
53
+
54
+ while (existingSlugs.includes(uniqueSlug)) {
55
+ const suffix = `-${counter}`;
56
+ const maxBaseLength = 100 - suffix.length;
57
+ const baseSlug = slug.substring(0, maxBaseLength).replace(/-+$/, '');
58
+ uniqueSlug = `${baseSlug}${suffix}`;
59
+ counter++;
60
+ }
61
+
62
+ return uniqueSlug;
63
+ }