claude-memory-layer 1.0.0

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 (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Local Embedding Generator using @xenova/transformers
3
+ * AXIOMMIND Principle 7: Standard JSON format for vectors
4
+ */
5
+
6
+ import { pipeline, Pipeline } from '@xenova/transformers';
7
+
8
+ export interface EmbeddingResult {
9
+ vector: number[];
10
+ model: string;
11
+ dimensions: number;
12
+ }
13
+
14
+ export class Embedder {
15
+ private pipeline: Pipeline | null = null;
16
+ private readonly modelName: string;
17
+ private initialized = false;
18
+
19
+ constructor(modelName: string = 'Xenova/all-MiniLM-L6-v2') {
20
+ this.modelName = modelName;
21
+ }
22
+
23
+ /**
24
+ * Initialize the embedding pipeline
25
+ */
26
+ async initialize(): Promise<void> {
27
+ if (this.initialized) return;
28
+
29
+ this.pipeline = await pipeline('feature-extraction', this.modelName);
30
+ this.initialized = true;
31
+ }
32
+
33
+ /**
34
+ * Generate embedding for a single text
35
+ */
36
+ async embed(text: string): Promise<EmbeddingResult> {
37
+ await this.initialize();
38
+
39
+ if (!this.pipeline) {
40
+ throw new Error('Embedding pipeline not initialized');
41
+ }
42
+
43
+ const output = await this.pipeline(text, {
44
+ pooling: 'mean',
45
+ normalize: true
46
+ });
47
+
48
+ const vector = Array.from(output.data as Float32Array);
49
+
50
+ return {
51
+ vector,
52
+ model: this.modelName,
53
+ dimensions: vector.length
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Generate embeddings for multiple texts in batch
59
+ */
60
+ async embedBatch(texts: string[]): Promise<EmbeddingResult[]> {
61
+ await this.initialize();
62
+
63
+ if (!this.pipeline) {
64
+ throw new Error('Embedding pipeline not initialized');
65
+ }
66
+
67
+ const results: EmbeddingResult[] = [];
68
+
69
+ // Process in batches of 32 for memory efficiency
70
+ const batchSize = 32;
71
+ for (let i = 0; i < texts.length; i += batchSize) {
72
+ const batch = texts.slice(i, i + batchSize);
73
+
74
+ for (const text of batch) {
75
+ const output = await this.pipeline(text, {
76
+ pooling: 'mean',
77
+ normalize: true
78
+ });
79
+
80
+ const vector = Array.from(output.data as Float32Array);
81
+
82
+ results.push({
83
+ vector,
84
+ model: this.modelName,
85
+ dimensions: vector.length
86
+ });
87
+ }
88
+ }
89
+
90
+ return results;
91
+ }
92
+
93
+ /**
94
+ * Get embedding dimensions for the current model
95
+ */
96
+ async getDimensions(): Promise<number> {
97
+ const result = await this.embed('test');
98
+ return result.dimensions;
99
+ }
100
+
101
+ /**
102
+ * Check if embedder is ready
103
+ */
104
+ isReady(): boolean {
105
+ return this.initialized && this.pipeline !== null;
106
+ }
107
+
108
+ /**
109
+ * Get model name
110
+ */
111
+ getModelName(): string {
112
+ return this.modelName;
113
+ }
114
+ }
115
+
116
+ // Singleton instance for reuse
117
+ let defaultEmbedder: Embedder | null = null;
118
+
119
+ export function getDefaultEmbedder(): Embedder {
120
+ if (!defaultEmbedder) {
121
+ defaultEmbedder = new Embedder();
122
+ }
123
+ return defaultEmbedder;
124
+ }
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Entity Repository - CRUD operations for Task/Condition/Artifact entities
3
+ * AXIOMMIND Principle 5: Task is Entity
4
+ */
5
+
6
+ import { Database } from 'duckdb';
7
+ import { randomUUID } from 'crypto';
8
+ import type {
9
+ Entity,
10
+ EntityType,
11
+ EntityStage,
12
+ EntityStatus,
13
+ EntityAlias,
14
+ TaskCurrentJson
15
+ } from './types.js';
16
+ import { makeEntityCanonicalKey } from './canonical-key.js';
17
+
18
+ export interface CreateEntityInput {
19
+ entityType: EntityType;
20
+ title: string;
21
+ currentJson: Record<string, unknown>;
22
+ project?: string;
23
+ stage?: EntityStage;
24
+ status?: EntityStatus;
25
+ }
26
+
27
+ export interface UpdateEntityInput {
28
+ currentJson?: Record<string, unknown>;
29
+ stage?: EntityStage;
30
+ status?: EntityStatus;
31
+ searchText?: string;
32
+ }
33
+
34
+ export class EntityRepo {
35
+ constructor(private db: Database) {}
36
+
37
+ /**
38
+ * Create a new entity
39
+ */
40
+ async create(input: CreateEntityInput): Promise<Entity> {
41
+ const entityId = randomUUID();
42
+ const canonicalKey = makeEntityCanonicalKey(input.entityType, input.title, {
43
+ project: input.project
44
+ });
45
+
46
+ const titleNorm = input.title.toLowerCase().trim();
47
+ const searchText = `${input.title} ${JSON.stringify(input.currentJson)}`;
48
+
49
+ const now = new Date();
50
+
51
+ await this.db.run(
52
+ `INSERT INTO entities (
53
+ entity_id, entity_type, canonical_key, title, stage, status,
54
+ current_json, title_norm, search_text, created_at, updated_at
55
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
56
+ [
57
+ entityId,
58
+ input.entityType,
59
+ canonicalKey,
60
+ input.title,
61
+ input.stage ?? 'raw',
62
+ input.status ?? 'active',
63
+ JSON.stringify(input.currentJson),
64
+ titleNorm,
65
+ searchText,
66
+ now.toISOString(),
67
+ now.toISOString()
68
+ ]
69
+ );
70
+
71
+ // Create primary alias
72
+ await this.db.run(
73
+ `INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
74
+ VALUES (?, ?, ?, TRUE)
75
+ ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
76
+ [input.entityType, canonicalKey, entityId]
77
+ );
78
+
79
+ return {
80
+ entityId,
81
+ entityType: input.entityType,
82
+ canonicalKey,
83
+ title: input.title,
84
+ stage: input.stage ?? 'raw',
85
+ status: input.status ?? 'active',
86
+ currentJson: input.currentJson,
87
+ titleNorm,
88
+ searchText,
89
+ createdAt: now,
90
+ updatedAt: now
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Find entity by ID
96
+ */
97
+ async findById(entityId: string): Promise<Entity | null> {
98
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
99
+ `SELECT * FROM entities WHERE entity_id = ?`,
100
+ [entityId]
101
+ );
102
+
103
+ if (rows.length === 0) return null;
104
+ return this.rowToEntity(rows[0]);
105
+ }
106
+
107
+ /**
108
+ * Find entity by canonical key
109
+ */
110
+ async findByCanonicalKey(
111
+ entityType: EntityType,
112
+ canonicalKey: string
113
+ ): Promise<Entity | null> {
114
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
115
+ `SELECT * FROM entities
116
+ WHERE entity_type = ? AND canonical_key = ?`,
117
+ [entityType, canonicalKey]
118
+ );
119
+
120
+ if (rows.length === 0) return null;
121
+ return this.rowToEntity(rows[0]);
122
+ }
123
+
124
+ /**
125
+ * Find or create entity by title (idempotent)
126
+ */
127
+ async findOrCreate(input: CreateEntityInput): Promise<{ entity: Entity; created: boolean }> {
128
+ const canonicalKey = makeEntityCanonicalKey(input.entityType, input.title, {
129
+ project: input.project
130
+ });
131
+
132
+ const existing = await this.findByCanonicalKey(input.entityType, canonicalKey);
133
+ if (existing) {
134
+ return { entity: existing, created: false };
135
+ }
136
+
137
+ const entity = await this.create(input);
138
+ return { entity, created: true };
139
+ }
140
+
141
+ /**
142
+ * Update entity
143
+ */
144
+ async update(entityId: string, input: UpdateEntityInput): Promise<Entity | null> {
145
+ const existing = await this.findById(entityId);
146
+ if (!existing) return null;
147
+
148
+ const updates: string[] = [];
149
+ const values: unknown[] = [];
150
+
151
+ if (input.currentJson !== undefined) {
152
+ updates.push('current_json = ?');
153
+ values.push(JSON.stringify(input.currentJson));
154
+ }
155
+ if (input.stage !== undefined) {
156
+ updates.push('stage = ?');
157
+ values.push(input.stage);
158
+ }
159
+ if (input.status !== undefined) {
160
+ updates.push('status = ?');
161
+ values.push(input.status);
162
+ }
163
+ if (input.searchText !== undefined) {
164
+ updates.push('search_text = ?');
165
+ values.push(input.searchText);
166
+ }
167
+
168
+ updates.push('updated_at = ?');
169
+ values.push(new Date().toISOString());
170
+
171
+ values.push(entityId);
172
+
173
+ await this.db.run(
174
+ `UPDATE entities SET ${updates.join(', ')} WHERE entity_id = ?`,
175
+ values
176
+ );
177
+
178
+ return this.findById(entityId);
179
+ }
180
+
181
+ /**
182
+ * List entities by type
183
+ */
184
+ async listByType(
185
+ entityType: EntityType,
186
+ options?: { status?: EntityStatus; limit?: number; offset?: number }
187
+ ): Promise<Entity[]> {
188
+ let query = `SELECT * FROM entities WHERE entity_type = ?`;
189
+ const params: unknown[] = [entityType];
190
+
191
+ if (options?.status) {
192
+ query += ` AND status = ?`;
193
+ params.push(options.status);
194
+ }
195
+
196
+ query += ` ORDER BY updated_at DESC`;
197
+
198
+ if (options?.limit) {
199
+ query += ` LIMIT ?`;
200
+ params.push(options.limit);
201
+ }
202
+ if (options?.offset) {
203
+ query += ` OFFSET ?`;
204
+ params.push(options.offset);
205
+ }
206
+
207
+ const rows = await this.db.all<Array<Record<string, unknown>>>(query, params);
208
+ return rows.map(row => this.rowToEntity(row));
209
+ }
210
+
211
+ /**
212
+ * Search entities by text
213
+ */
214
+ async search(
215
+ query: string,
216
+ options?: { entityType?: EntityType; limit?: number }
217
+ ): Promise<Entity[]> {
218
+ const searchPattern = `%${query.toLowerCase()}%`;
219
+
220
+ let sql = `SELECT * FROM entities WHERE (title_norm LIKE ? OR search_text LIKE ?)`;
221
+ const params: unknown[] = [searchPattern, searchPattern];
222
+
223
+ if (options?.entityType) {
224
+ sql += ` AND entity_type = ?`;
225
+ params.push(options.entityType);
226
+ }
227
+
228
+ sql += ` AND status = 'active' ORDER BY updated_at DESC`;
229
+
230
+ if (options?.limit) {
231
+ sql += ` LIMIT ?`;
232
+ params.push(options.limit);
233
+ }
234
+
235
+ const rows = await this.db.all<Array<Record<string, unknown>>>(sql, params);
236
+ return rows.map(row => this.rowToEntity(row));
237
+ }
238
+
239
+ /**
240
+ * Get tasks by status
241
+ */
242
+ async getTasksByStatus(status: string): Promise<Entity[]> {
243
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
244
+ `SELECT * FROM entities
245
+ WHERE entity_type = 'task'
246
+ AND json_extract(current_json, '$.status') = ?
247
+ AND status = 'active'
248
+ ORDER BY updated_at DESC`,
249
+ [status]
250
+ );
251
+
252
+ return rows.map(row => this.rowToEntity(row));
253
+ }
254
+
255
+ /**
256
+ * Get blocked tasks with their blockers
257
+ */
258
+ async getBlockedTasksWithBlockers(): Promise<Array<{
259
+ task: Entity;
260
+ blockers: Array<{ entityId: string; entityType: string; title: string }>;
261
+ }>> {
262
+ const tasks = await this.getTasksByStatus('blocked');
263
+
264
+ const results: Array<{
265
+ task: Entity;
266
+ blockers: Array<{ entityId: string; entityType: string; title: string }>;
267
+ }> = [];
268
+
269
+ for (const task of tasks) {
270
+ const blockerEdges = await this.db.all<Array<Record<string, unknown>>>(
271
+ `SELECT e.dst_id, ent.entity_type, ent.title
272
+ FROM edges e
273
+ JOIN entities ent ON ent.entity_id = e.dst_id
274
+ WHERE e.src_id = ? AND e.rel_type = 'blocked_by'`,
275
+ [task.entityId]
276
+ );
277
+
278
+ results.push({
279
+ task,
280
+ blockers: blockerEdges.map(row => ({
281
+ entityId: row.dst_id as string,
282
+ entityType: row.entity_type as string,
283
+ title: row.title as string
284
+ }))
285
+ });
286
+ }
287
+
288
+ return results;
289
+ }
290
+
291
+ /**
292
+ * Add alias for entity
293
+ */
294
+ async addAlias(
295
+ entityType: EntityType,
296
+ canonicalKey: string,
297
+ entityId: string
298
+ ): Promise<void> {
299
+ await this.db.run(
300
+ `INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
301
+ VALUES (?, ?, ?, FALSE)
302
+ ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
303
+ [entityType, canonicalKey, entityId]
304
+ );
305
+ }
306
+
307
+ /**
308
+ * Find entity by alias
309
+ */
310
+ async findByAlias(entityType: EntityType, canonicalKey: string): Promise<Entity | null> {
311
+ const rows = await this.db.all<Array<Record<string, unknown>>>(
312
+ `SELECT e.* FROM entities e
313
+ JOIN entity_aliases a ON e.entity_id = a.entity_id
314
+ WHERE a.entity_type = ? AND a.canonical_key = ?`,
315
+ [entityType, canonicalKey]
316
+ );
317
+
318
+ if (rows.length === 0) return null;
319
+ return this.rowToEntity(rows[0]);
320
+ }
321
+
322
+ /**
323
+ * Convert database row to Entity
324
+ */
325
+ private rowToEntity(row: Record<string, unknown>): Entity {
326
+ return {
327
+ entityId: row.entity_id as string,
328
+ entityType: row.entity_type as EntityType,
329
+ canonicalKey: row.canonical_key as string,
330
+ title: row.title as string,
331
+ stage: row.stage as EntityStage,
332
+ status: row.status as EntityStatus,
333
+ currentJson: typeof row.current_json === 'string'
334
+ ? JSON.parse(row.current_json)
335
+ : row.current_json as Record<string, unknown>,
336
+ titleNorm: row.title_norm as string | undefined,
337
+ searchText: row.search_text as string | undefined,
338
+ createdAt: new Date(row.created_at as string),
339
+ updatedAt: new Date(row.updated_at as string)
340
+ };
341
+ }
342
+ }