@cohaku/mcp 0.1.0 → 0.2.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 (2) hide show
  1. package/dist/index.js +240 -66
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { join } from "path";
4
+ import { join, basename } from "path";
5
5
  import { homedir } from "os";
6
+ import { execSync } from "child_process";
6
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
8
 
8
9
  // ../core/dist/storage/sqlite-driver.js
@@ -25,11 +26,12 @@ var DEFAULT_DECAY_RATE = 0.01;
25
26
  var DEFAULT_EMBEDDING_DIMENSIONS = 384;
26
27
 
27
28
  // ../core/dist/storage/schema.js
28
- var SCHEMA_VERSION = 1;
29
+ var SCHEMA_VERSION = 2;
29
30
  var CREATE_TABLES = `
30
31
  -- Memory (3-layer)
31
32
  CREATE TABLE IF NOT EXISTS memories (
32
33
  id TEXT PRIMARY KEY,
34
+ project_id TEXT,
33
35
  content TEXT NOT NULL,
34
36
  layer TEXT NOT NULL DEFAULT 'long_term',
35
37
  type TEXT NOT NULL DEFAULT 'note',
@@ -73,6 +75,7 @@ var CREATE_TABLES = `
73
75
  -- Entity nodes
74
76
  CREATE TABLE IF NOT EXISTS nodes (
75
77
  id TEXT PRIMARY KEY,
78
+ project_id TEXT,
76
79
  name TEXT NOT NULL,
77
80
  name_embedding BLOB,
78
81
  summary TEXT,
@@ -84,6 +87,7 @@ var CREATE_TABLES = `
84
87
  -- Edges (bi-temporal)
85
88
  CREATE TABLE IF NOT EXISTS edges (
86
89
  id TEXT PRIMARY KEY,
90
+ project_id TEXT,
87
91
  source_id TEXT NOT NULL REFERENCES nodes(id),
88
92
  target_id TEXT NOT NULL REFERENCES nodes(id),
89
93
  relation TEXT NOT NULL,
@@ -98,6 +102,7 @@ var CREATE_TABLES = `
98
102
  -- Episodes
99
103
  CREATE TABLE IF NOT EXISTS episodes (
100
104
  id TEXT PRIMARY KEY,
105
+ project_id TEXT,
101
106
  content TEXT NOT NULL,
102
107
  content_type TEXT,
103
108
  source TEXT,
@@ -115,6 +120,7 @@ var CREATE_TABLES = `
115
120
  -- Sessions
116
121
  CREATE TABLE IF NOT EXISTS sessions (
117
122
  id TEXT PRIMARY KEY,
123
+ project_id TEXT,
118
124
  status TEXT NOT NULL DEFAULT 'active',
119
125
  started_at TEXT NOT NULL,
120
126
  ended_at TEXT,
@@ -132,10 +138,28 @@ var CREATE_TABLES = `
132
138
  -- Indexes
133
139
  CREATE INDEX IF NOT EXISTS idx_memories_layer ON memories(layer);
134
140
  CREATE INDEX IF NOT EXISTS idx_memories_deleted ON memories(deleted_at);
141
+ CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
142
+ CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project_id);
135
143
  CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
136
144
  CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
137
145
  CREATE INDEX IF NOT EXISTS idx_edges_expired ON edges(expired_at);
146
+ CREATE INDEX IF NOT EXISTS idx_edges_project ON edges(project_id);
147
+ CREATE INDEX IF NOT EXISTS idx_episodes_project ON episodes(project_id);
138
148
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
149
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
150
+ `;
151
+ var MIGRATE_V1_TO_V2 = `
152
+ ALTER TABLE memories ADD COLUMN project_id TEXT;
153
+ ALTER TABLE nodes ADD COLUMN project_id TEXT;
154
+ ALTER TABLE edges ADD COLUMN project_id TEXT;
155
+ ALTER TABLE episodes ADD COLUMN project_id TEXT;
156
+ ALTER TABLE sessions ADD COLUMN project_id TEXT;
157
+
158
+ CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
159
+ CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project_id);
160
+ CREATE INDEX IF NOT EXISTS idx_edges_project ON edges(project_id);
161
+ CREATE INDEX IF NOT EXISTS idx_episodes_project ON episodes(project_id);
162
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
139
163
  `;
140
164
  function createVecTables(dimensions) {
141
165
  return `
@@ -178,27 +202,51 @@ var SqliteDriver = class {
178
202
  this.db.pragma("journal_mode = WAL");
179
203
  this.db.pragma("foreign_keys = ON");
180
204
  sqliteVec.load(this.db);
205
+ this.db.exec("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
206
+ const existing = this.db.prepare("SELECT value FROM config WHERE key = ?").get("schema_version");
207
+ const currentVersion = existing ? Number(existing.value) : 0;
208
+ if (currentVersion >= 1 && currentVersion < 2) {
209
+ for (const stmt of MIGRATE_V1_TO_V2.split(";").filter((s) => s.trim())) {
210
+ this.db.exec(stmt);
211
+ }
212
+ }
181
213
  this.db.exec(CREATE_TABLES);
182
214
  const vecSQL = createVecTables(this.dimensions);
183
215
  for (const stmt of vecSQL.split(";").filter((s) => s.trim())) {
184
216
  this.db.exec(stmt);
185
217
  }
186
- const existing = this.db.prepare("SELECT value FROM config WHERE key = ?").get("schema_version");
187
- if (!existing) {
188
- this.db.prepare("INSERT INTO config (key, value) VALUES (?, ?)").run("schema_version", String(SCHEMA_VERSION));
189
- }
218
+ this.db.prepare("INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run("schema_version", String(SCHEMA_VERSION));
190
219
  }
191
220
  close() {
192
221
  this.db.close();
193
222
  }
223
+ // ── Project Filter Helper ──
224
+ buildProjectFilter(projectIds, tableAlias) {
225
+ if (!projectIds)
226
+ return { clause: "", params: [] };
227
+ const hasNull = projectIds.includes(null);
228
+ const nonNull = projectIds.filter((id) => id !== null);
229
+ const conditions = [];
230
+ const params = [];
231
+ if (hasNull) {
232
+ conditions.push(`${tableAlias}.project_id IS NULL`);
233
+ }
234
+ if (nonNull.length > 0) {
235
+ conditions.push(`${tableAlias}.project_id IN (${nonNull.map(() => "?").join(", ")})`);
236
+ params.push(...nonNull);
237
+ }
238
+ if (conditions.length === 0)
239
+ return { clause: "", params: [] };
240
+ return { clause: `AND (${conditions.join(" OR ")})`, params };
241
+ }
194
242
  // ── Memory ──
195
243
  async addMemory(memory) {
196
244
  const tagsStr = memory.tags ? JSON.stringify(memory.tags) : null;
197
245
  this.db.transaction(() => {
198
246
  this.db.prepare(`
199
- INSERT INTO memories (id, content, layer, type, priority, importance, tags, expires_at, last_accessed_at, created_at, updated_at, deleted_at)
200
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
201
- `).run(memory.id, memory.content, memory.layer, memory.type, memory.priority, memory.importance, tagsStr, memory.expiresAt ?? null, memory.lastAccessedAt, memory.createdAt, memory.updatedAt, memory.deletedAt ?? null);
247
+ INSERT INTO memories (id, project_id, content, layer, type, priority, importance, tags, expires_at, last_accessed_at, created_at, updated_at, deleted_at)
248
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
249
+ `).run(memory.id, memory.projectId ?? null, memory.content, memory.layer, memory.type, memory.priority, memory.importance, tagsStr, memory.expiresAt ?? null, memory.lastAccessedAt, memory.createdAt, memory.updatedAt, memory.deletedAt ?? null);
202
250
  if (memory.embedding) {
203
251
  this.db.prepare(`
204
252
  INSERT INTO vec_memories (memory_id, embedding) VALUES (?, ?)
@@ -249,7 +297,8 @@ var SqliteDriver = class {
249
297
  async deleteMemory(id) {
250
298
  this.db.prepare("UPDATE memories SET deleted_at = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), id);
251
299
  }
252
- async searchMemories(embedding, limit) {
300
+ async searchMemories(embedding, limit, projectIds) {
301
+ const { clause, params } = this.buildProjectFilter(projectIds, "m");
253
302
  const rows = this.db.prepare(`
254
303
  SELECT m.*, v.distance
255
304
  FROM vec_memories v
@@ -257,55 +306,60 @@ var SqliteDriver = class {
257
306
  WHERE v.embedding MATCH ?
258
307
  AND m.deleted_at IS NULL
259
308
  AND k = ?
260
- `).all(toVecBlob(embedding), limit);
309
+ ${clause}
310
+ `).all(toVecBlob(embedding), limit, ...params);
261
311
  return rows.map((row) => ({
262
312
  memory: this.rowToMemory(row),
263
313
  distance: row.distance
264
314
  }));
265
315
  }
266
- async fullTextSearch(query, limit) {
316
+ async fullTextSearch(query, limit, projectIds) {
317
+ const { clause, params } = this.buildProjectFilter(projectIds, "m");
267
318
  const rows = this.db.prepare(`
268
319
  SELECT m.*, fts.rank
269
320
  FROM memories_fts fts
270
321
  INNER JOIN memories m ON m.rowid = fts.rowid
271
322
  WHERE memories_fts MATCH ?
272
323
  AND m.deleted_at IS NULL
324
+ ${clause}
273
325
  ORDER BY fts.rank
274
326
  LIMIT ?
275
- `).all(query, limit);
327
+ `).all(query, ...params, limit);
276
328
  return rows.map((row) => ({
277
329
  memory: this.rowToMemory(row),
278
330
  rank: row.rank
279
331
  }));
280
332
  }
281
- async findSimilarMemories(embedding, threshold, limit) {
282
- const results = await this.searchMemories(embedding, limit * 3);
333
+ async findSimilarMemories(embedding, threshold, limit, projectIds) {
334
+ const results = await this.searchMemories(embedding, limit * 3, projectIds);
283
335
  return results.filter((r) => r.distance <= threshold).slice(0, limit);
284
336
  }
285
- async listMemoriesByLayer(layer, limit) {
337
+ async listMemoriesByLayer(layer, limit, projectIds) {
286
338
  const now = (/* @__PURE__ */ new Date()).toISOString();
339
+ const { clause, params } = this.buildProjectFilter(projectIds, "memories");
287
340
  const rows = this.db.prepare(`
288
341
  SELECT * FROM memories
289
342
  WHERE layer = ?
290
343
  AND deleted_at IS NULL
291
344
  AND (expires_at IS NULL OR expires_at > ?)
345
+ ${clause}
292
346
  ORDER BY updated_at DESC
293
347
  LIMIT ?
294
- `).all(layer, now, limit);
348
+ `).all(layer, now, ...params, limit);
295
349
  return rows.map((row) => this.rowToMemory(row));
296
350
  }
297
351
  // ── Nodes ──
298
352
  async upsertNode(node) {
299
353
  this.db.transaction(() => {
300
354
  this.db.prepare(`
301
- INSERT INTO nodes (id, name, summary, entity_type, created_at, updated_at)
302
- VALUES (?, ?, ?, ?, ?, ?)
355
+ INSERT INTO nodes (id, project_id, name, summary, entity_type, created_at, updated_at)
356
+ VALUES (?, ?, ?, ?, ?, ?, ?)
303
357
  ON CONFLICT(id) DO UPDATE SET
304
358
  name = excluded.name,
305
359
  summary = excluded.summary,
306
360
  entity_type = excluded.entity_type,
307
361
  updated_at = excluded.updated_at
308
- `).run(node.id, node.name, node.summary ?? null, node.entityType ?? null, node.createdAt, node.updatedAt);
362
+ `).run(node.id, node.projectId ?? null, node.name, node.summary ?? null, node.entityType ?? null, node.createdAt, node.updatedAt);
309
363
  if (node.nameEmbedding) {
310
364
  this.db.prepare(`
311
365
  DELETE FROM vec_nodes WHERE node_id = ?
@@ -320,29 +374,31 @@ var SqliteDriver = class {
320
374
  const row = this.db.prepare("SELECT * FROM nodes WHERE id = ?").get(id);
321
375
  return row ? this.rowToNode(row) : null;
322
376
  }
323
- async searchNodes(embedding, limit) {
377
+ async searchNodes(embedding, limit, projectIds) {
378
+ const { clause, params } = this.buildProjectFilter(projectIds, "n");
324
379
  const rows = this.db.prepare(`
325
380
  SELECT n.*
326
381
  FROM vec_nodes v
327
382
  INNER JOIN nodes n ON n.id = v.node_id
328
383
  WHERE v.embedding MATCH ?
329
384
  AND k = ?
330
- `).all(toVecBlob(embedding), limit);
385
+ ${clause}
386
+ `).all(toVecBlob(embedding), limit, ...params);
331
387
  return rows.map((row) => this.rowToNode(row));
332
388
  }
333
389
  // ── Edges ──
334
390
  async upsertEdge(edge) {
335
391
  this.db.transaction(() => {
336
392
  this.db.prepare(`
337
- INSERT INTO edges (id, source_id, target_id, relation, fact, created_at, expired_at, valid_at, invalid_at)
338
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
393
+ INSERT INTO edges (id, project_id, source_id, target_id, relation, fact, created_at, expired_at, valid_at, invalid_at)
394
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
339
395
  ON CONFLICT(id) DO UPDATE SET
340
396
  relation = excluded.relation,
341
397
  fact = excluded.fact,
342
398
  expired_at = excluded.expired_at,
343
399
  valid_at = excluded.valid_at,
344
400
  invalid_at = excluded.invalid_at
345
- `).run(edge.id, edge.sourceId, edge.targetId, edge.relation, edge.fact ?? null, edge.createdAt, edge.expiredAt ?? null, edge.validAt ?? null, edge.invalidAt ?? null);
401
+ `).run(edge.id, edge.projectId ?? null, edge.sourceId, edge.targetId, edge.relation, edge.fact ?? null, edge.createdAt, edge.expiredAt ?? null, edge.validAt ?? null, edge.invalidAt ?? null);
346
402
  if (edge.factEmbedding) {
347
403
  this.db.prepare(`
348
404
  DELETE FROM vec_edges WHERE edge_id = ?
@@ -360,7 +416,7 @@ var SqliteDriver = class {
360
416
  async invalidateEdge(id, expiredAt, invalidAt) {
361
417
  this.db.prepare("UPDATE edges SET expired_at = ?, invalid_at = ? WHERE id = ?").run(expiredAt, invalidAt, id);
362
418
  }
363
- async searchEdges(embedding, filters, limit) {
419
+ async searchEdges(embedding, filters, limit, projectIds) {
364
420
  let whereClause = "1=1";
365
421
  const params = [toVecBlob(embedding), limit];
366
422
  if (filters.excludeExpired !== false) {
@@ -372,6 +428,9 @@ var SqliteDriver = class {
372
428
  whereClause += " AND (e.invalid_at IS NULL OR e.invalid_at > ?)";
373
429
  params.push(filters.asOf);
374
430
  }
431
+ const { clause: projectClause, params: projectParams } = this.buildProjectFilter(projectIds, "e");
432
+ whereClause += ` ${projectClause}`;
433
+ params.push(...projectParams);
375
434
  const rows = this.db.prepare(`
376
435
  SELECT e.*
377
436
  FROM vec_edges v
@@ -427,9 +486,9 @@ var SqliteDriver = class {
427
486
  // ── Episodes ──
428
487
  async addEpisode(episode) {
429
488
  this.db.prepare(`
430
- INSERT INTO episodes (id, content, content_type, source, created_at)
431
- VALUES (?, ?, ?, ?, ?)
432
- `).run(episode.id, episode.content, episode.contentType ?? null, episode.source ?? null, episode.createdAt);
489
+ INSERT INTO episodes (id, project_id, content, content_type, source, created_at)
490
+ VALUES (?, ?, ?, ?, ?, ?)
491
+ `).run(episode.id, episode.projectId ?? null, episode.content, episode.contentType ?? null, episode.source ?? null, episode.createdAt);
433
492
  }
434
493
  async addEpisodeRef(ref) {
435
494
  this.db.prepare(`
@@ -441,8 +500,9 @@ var SqliteDriver = class {
441
500
  const row = this.db.prepare("SELECT * FROM episodes WHERE id = ?").get(id);
442
501
  return row ? this.rowToEpisode(row) : null;
443
502
  }
444
- async listEpisodes(limit) {
445
- const rows = this.db.prepare("SELECT * FROM episodes ORDER BY created_at DESC LIMIT ?").all(limit);
503
+ async listEpisodes(limit, projectIds) {
504
+ const { clause, params } = this.buildProjectFilter(projectIds, "episodes");
505
+ const rows = this.db.prepare(`SELECT * FROM episodes WHERE 1=1 ${clause} ORDER BY created_at DESC LIMIT ?`).all(...params, limit);
446
506
  return rows.map((row) => this.rowToEpisode(row));
447
507
  }
448
508
  async getEpisodeRefs(episodeId) {
@@ -456,9 +516,9 @@ var SqliteDriver = class {
456
516
  // ── Sessions ──
457
517
  async addSession(session) {
458
518
  this.db.prepare(`
459
- INSERT INTO sessions (id, status, started_at, ended_at, checkpoint_count, working_memory_snapshot, metadata)
460
- VALUES (?, ?, ?, ?, ?, ?, ?)
461
- `).run(session.id, session.status, session.startedAt, session.endedAt ?? null, session.checkpointCount, session.workingMemorySnapshot ?? null, session.metadata ?? null);
519
+ INSERT INTO sessions (id, project_id, status, started_at, ended_at, checkpoint_count, working_memory_snapshot, metadata)
520
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
521
+ `).run(session.id, session.projectId ?? null, session.status, session.startedAt, session.endedAt ?? null, session.checkpointCount, session.workingMemorySnapshot ?? null, session.metadata ?? null);
462
522
  }
463
523
  async updateSession(id, updates) {
464
524
  const sets = [];
@@ -492,8 +552,9 @@ var SqliteDriver = class {
492
552
  const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
493
553
  return row ? this.rowToSession(row) : null;
494
554
  }
495
- async listSessions(limit) {
496
- const rows = this.db.prepare("SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?").all(limit);
555
+ async listSessions(limit, projectIds) {
556
+ const { clause, params } = this.buildProjectFilter(projectIds, "sessions");
557
+ const rows = this.db.prepare(`SELECT * FROM sessions WHERE 1=1 ${clause} ORDER BY started_at DESC LIMIT ?`).all(...params, limit);
497
558
  return rows.map((row) => this.rowToSession(row));
498
559
  }
499
560
  // ── Config ──
@@ -508,6 +569,7 @@ var SqliteDriver = class {
508
569
  rowToMemory(row) {
509
570
  return {
510
571
  id: row.id,
572
+ projectId: row.project_id ?? void 0,
511
573
  content: row.content,
512
574
  layer: row.layer,
513
575
  type: row.type,
@@ -524,6 +586,7 @@ var SqliteDriver = class {
524
586
  rowToNode(row) {
525
587
  return {
526
588
  id: row.id,
589
+ projectId: row.project_id ?? void 0,
527
590
  name: row.name,
528
591
  summary: row.summary ?? void 0,
529
592
  entityType: row.entity_type ?? void 0,
@@ -534,6 +597,7 @@ var SqliteDriver = class {
534
597
  rowToEdge(row) {
535
598
  return {
536
599
  id: row.id,
600
+ projectId: row.project_id ?? void 0,
537
601
  sourceId: row.source_id,
538
602
  targetId: row.target_id,
539
603
  relation: row.relation,
@@ -547,6 +611,7 @@ var SqliteDriver = class {
547
611
  rowToEpisode(row) {
548
612
  return {
549
613
  id: row.id,
614
+ projectId: row.project_id ?? void 0,
550
615
  content: row.content,
551
616
  contentType: row.content_type ?? void 0,
552
617
  source: row.source ?? void 0,
@@ -556,6 +621,7 @@ var SqliteDriver = class {
556
621
  rowToSession(row) {
557
622
  return {
558
623
  id: row.id,
624
+ projectId: row.project_id ?? void 0,
559
625
  status: row.status,
560
626
  startedAt: row.started_at,
561
627
  endedAt: row.ended_at ?? void 0,
@@ -632,12 +698,12 @@ function distanceToSimilarity(distance) {
632
698
 
633
699
  // ../core/dist/engine/context.js
634
700
  async function getContext(storage, options = {}) {
635
- const { maxMemories = 50 } = options;
636
- const rules = await storage.listMemoriesByLayer("rule", maxMemories);
701
+ const { maxMemories = 50, projectIds } = options;
702
+ const rules = await storage.listMemoriesByLayer("rule", maxMemories, projectIds);
637
703
  const remaining1 = maxMemories - rules.length;
638
- const working = remaining1 > 0 ? await storage.listMemoriesByLayer("working", Math.ceil(remaining1 * 0.6)) : [];
704
+ const working = remaining1 > 0 ? await storage.listMemoriesByLayer("working", Math.ceil(remaining1 * 0.6), projectIds) : [];
639
705
  const remaining2 = maxMemories - rules.length - working.length;
640
- const longTerm = remaining2 > 0 ? await storage.listMemoriesByLayer("long_term", remaining2) : [];
706
+ const longTerm = remaining2 > 0 ? await storage.listMemoriesByLayer("long_term", remaining2, projectIds) : [];
641
707
  const memories = [...rules, ...working, ...longTerm];
642
708
  const totalCount = memories.length;
643
709
  return {
@@ -653,12 +719,14 @@ var MemoryEngine = class {
653
719
  embedding;
654
720
  weights;
655
721
  decayRate;
722
+ defaultProjectId;
656
723
  constructor(config) {
657
724
  mkdirSync(dirname(config.dbPath), { recursive: true });
658
725
  this.storage = new SqliteDriver(config.dbPath);
659
726
  this.embedding = new LocalEmbedding(config.embeddingCacheDir);
660
727
  this.weights = { ...DEFAULT_WEIGHTS, ...config.weights };
661
728
  this.decayRate = config.decayRate ?? DEFAULT_DECAY_RATE;
729
+ this.defaultProjectId = config.defaultProjectId;
662
730
  }
663
731
  async initialize() {
664
732
  await this.storage.initialize();
@@ -666,12 +734,28 @@ var MemoryEngine = class {
666
734
  close() {
667
735
  this.storage.close();
668
736
  }
737
+ // ── Scope Helpers ──
738
+ resolveProjectId(scope) {
739
+ if (scope === "global")
740
+ return void 0;
741
+ return this.defaultProjectId;
742
+ }
743
+ resolveProjectFilter(scope) {
744
+ if (scope === "all")
745
+ return void 0;
746
+ if (scope === "global")
747
+ return [null];
748
+ if (this.defaultProjectId)
749
+ return [this.defaultProjectId, null];
750
+ return void 0;
751
+ }
669
752
  // ── Memory ──
670
753
  async addMemory(input) {
671
754
  const now = nowISO();
672
755
  const layer = input.layer ?? "long_term";
673
756
  const memory = {
674
757
  id: generateId(),
758
+ projectId: this.resolveProjectId(input.scope),
675
759
  content: input.content,
676
760
  layer,
677
761
  type: input.type ?? "note",
@@ -690,11 +774,12 @@ var MemoryEngine = class {
690
774
  async searchMemories(query) {
691
775
  const limit = query.limit ?? 10;
692
776
  const weights = { ...this.weights, ...query.weights };
777
+ const projectIds = this.resolveProjectFilter(query.scope);
693
778
  const queryEmbedding = query.embedding ?? await this.embedding.embed(query.text);
694
- const semanticResults = await this.storage.searchMemories(queryEmbedding, limit * 2);
779
+ const semanticResults = await this.storage.searchMemories(queryEmbedding, limit * 2, projectIds);
695
780
  let bm25Results = [];
696
781
  try {
697
- bm25Results = await this.storage.fullTextSearch(query.text, limit * 2);
782
+ bm25Results = await this.storage.fullTextSearch(query.text, limit * 2, projectIds);
698
783
  } catch {
699
784
  }
700
785
  const candidateMap = /* @__PURE__ */ new Map();
@@ -752,7 +837,8 @@ var MemoryEngine = class {
752
837
  if (!target)
753
838
  throw new Error(`Memory not found: ${memoryId}`);
754
839
  const embedding = target.embedding ?? await this.embedding.embed(target.content);
755
- const similar = await this.storage.findSimilarMemories(embedding, threshold, 20);
840
+ const projectIds = this.resolveProjectFilter("project");
841
+ const similar = await this.storage.findSimilarMemories(embedding, threshold, 20, projectIds);
756
842
  return similar.filter((r) => r.memory.id !== memoryId);
757
843
  }
758
844
  async consolidateMemories(sourceIds, mergedContent, layer) {
@@ -771,6 +857,7 @@ var MemoryEngine = class {
771
857
  const now = nowISO();
772
858
  const node = {
773
859
  id: generateId(),
860
+ projectId: this.resolveProjectId(input.scope),
774
861
  name: input.name,
775
862
  nameEmbedding: await this.embedding.embed(input.name),
776
863
  summary: input.summary,
@@ -785,6 +872,7 @@ var MemoryEngine = class {
785
872
  const now = nowISO();
786
873
  const edge = {
787
874
  id: generateId(),
875
+ projectId: this.resolveProjectId(input.scope),
788
876
  sourceId: input.sourceId,
789
877
  targetId: input.targetId,
790
878
  relation: input.relation,
@@ -819,9 +907,10 @@ var MemoryEngine = class {
819
907
  }
820
908
  async searchGraph(query, limit = 10, options) {
821
909
  const depth = options?.traverseDepth ?? 1;
910
+ const projectIds = this.resolveProjectFilter(options?.scope);
822
911
  const queryEmbedding = await this.embedding.embed(query);
823
- const nodes = await this.storage.searchNodes(queryEmbedding, Math.ceil(limit / 2));
824
- const edges = await this.storage.searchEdges(queryEmbedding, { excludeExpired: true }, Math.ceil(limit / 2));
912
+ const nodes = await this.storage.searchNodes(queryEmbedding, Math.ceil(limit / 2), projectIds);
913
+ const edges = await this.storage.searchEdges(queryEmbedding, { excludeExpired: true }, Math.ceil(limit / 2), projectIds);
825
914
  const results = [];
826
915
  const seenNodeIds = /* @__PURE__ */ new Set();
827
916
  for (const node of nodes) {
@@ -864,6 +953,7 @@ var MemoryEngine = class {
864
953
  async addEpisode(input, refs) {
865
954
  const episode = {
866
955
  id: generateId(),
956
+ projectId: this.resolveProjectId(input.scope),
867
957
  content: input.content,
868
958
  contentType: input.contentType,
869
959
  source: input.source,
@@ -880,13 +970,15 @@ var MemoryEngine = class {
880
970
  async getEpisode(id) {
881
971
  return this.storage.getEpisode(id);
882
972
  }
883
- async listEpisodes(limit = 20) {
884
- return this.storage.listEpisodes(limit);
973
+ async listEpisodes(limit = 20, scope) {
974
+ const projectIds = this.resolveProjectFilter(scope);
975
+ return this.storage.listEpisodes(limit, projectIds);
885
976
  }
886
977
  // ── Sessions ──
887
978
  async sessionStart(metadata) {
888
979
  const session = {
889
980
  id: generateId(),
981
+ projectId: this.resolveProjectId("project"),
890
982
  status: "active",
891
983
  startedAt: nowISO(),
892
984
  checkpointCount: 0,
@@ -909,12 +1001,14 @@ var MemoryEngine = class {
909
1001
  checkpointCount: session.checkpointCount + 1
910
1002
  });
911
1003
  }
912
- async sessionList(limit = 20) {
913
- return this.storage.listSessions(limit);
1004
+ async sessionList(limit = 20, scope) {
1005
+ const projectIds = this.resolveProjectFilter(scope);
1006
+ return this.storage.listSessions(limit, projectIds);
914
1007
  }
915
1008
  // ── Context ──
916
1009
  async getContext(options) {
917
- return getContext(this.storage, options);
1010
+ const projectIds = this.resolveProjectFilter(options?.scope);
1011
+ return getContext(this.storage, { ...options, projectIds });
918
1012
  }
919
1013
  };
920
1014
 
@@ -967,6 +1061,8 @@ function textContent(text) {
967
1061
  }
968
1062
 
969
1063
  // src/tools/memory.ts
1064
+ var scopeSchema = z.enum(["project", "global"]).default("project").describe('Scope: "project" (default) stores in current project, "global" stores across all projects');
1065
+ var searchScopeSchema = z.enum(["project", "global", "all"]).default("project").describe('Scope: "project" (default) searches project + global, "global" searches global only, "all" searches everything');
970
1066
  function registerMemoryTools(server2, engine2) {
971
1067
  server2.tool(
972
1068
  "add_memory",
@@ -976,7 +1072,8 @@ function registerMemoryTools(server2, engine2) {
976
1072
  layer: z.enum(["rule", "working", "long_term"]).default("long_term").describe("Memory layer"),
977
1073
  type: z.enum(["rule", "decision", "fact", "note", "skill"]).default("note").describe("Memory type"),
978
1074
  tags: z.array(z.string()).optional().describe("Tags"),
979
- expiresAt: z.string().optional().describe("Expiration date (ISO 8601)")
1075
+ expiresAt: z.string().optional().describe("Expiration date (ISO 8601)"),
1076
+ scope: scopeSchema
980
1077
  },
981
1078
  async (params) => {
982
1079
  const memory = await engine2.addMemory(params);
@@ -991,13 +1088,15 @@ ID: ${memory.id}`);
991
1088
  {
992
1089
  query: z.string().describe("Search query"),
993
1090
  limit: z.number().int().min(1).max(100).default(10).describe("Max results"),
994
- layers: z.array(z.enum(["rule", "working", "long_term"])).optional().describe("Target layers")
1091
+ layers: z.array(z.enum(["rule", "working", "long_term"])).optional().describe("Target layers"),
1092
+ scope: searchScopeSchema
995
1093
  },
996
1094
  async (params) => {
997
1095
  const results = await engine2.searchMemories({
998
1096
  text: params.query,
999
1097
  limit: params.limit,
1000
- layers: params.layers
1098
+ layers: params.layers,
1099
+ scope: params.scope
1001
1100
  });
1002
1101
  return textContent(formatSearchResults(results));
1003
1102
  }
@@ -1067,6 +1166,7 @@ ID: ${merged.id}`);
1067
1166
 
1068
1167
  // src/tools/session.ts
1069
1168
  import { z as z2 } from "zod";
1169
+ var searchScopeSchema2 = z2.enum(["project", "global", "all"]).default("project").describe('Scope: "project" (default) searches project + global, "global" searches global only, "all" searches everything');
1070
1170
  function registerSessionTools(server2, engine2) {
1071
1171
  server2.tool(
1072
1172
  "session_start",
@@ -1106,10 +1206,11 @@ ${formatSession(session)}`);
1106
1206
  "session_list",
1107
1207
  "List past sessions",
1108
1208
  {
1109
- limit: z2.number().int().min(1).max(100).default(20).describe("Max results")
1209
+ limit: z2.number().int().min(1).max(100).default(20).describe("Max results"),
1210
+ scope: searchScopeSchema2
1110
1211
  },
1111
1212
  async (params) => {
1112
- const sessions = await engine2.sessionList(params.limit);
1213
+ const sessions = await engine2.sessionList(params.limit, params.scope);
1113
1214
  if (sessions.length === 0) return textContent("No sessions found");
1114
1215
  return textContent(sessions.map((s) => formatSession(s)).join("\n"));
1115
1216
  }
@@ -1118,15 +1219,17 @@ ${formatSession(session)}`);
1118
1219
 
1119
1220
  // src/tools/context.ts
1120
1221
  import { z as z3 } from "zod";
1222
+ var searchScopeSchema3 = z3.enum(["project", "global", "all"]).default("project").describe('Scope: "project" (default) searches project + global, "global" searches global only, "all" searches everything');
1121
1223
  function registerContextTools(server2, engine2) {
1122
1224
  server2.tool(
1123
1225
  "get_context",
1124
1226
  "Get current context (priority: rule > working > long_term)",
1125
1227
  {
1126
- maxMemories: z3.number().int().min(1).max(200).default(50).describe("Max number of memories")
1228
+ maxMemories: z3.number().int().min(1).max(200).default(50).describe("Max number of memories"),
1229
+ scope: searchScopeSchema3
1127
1230
  },
1128
1231
  async (params) => {
1129
- const ctx = await engine2.getContext({ maxMemories: params.maxMemories });
1232
+ const ctx = await engine2.getContext({ maxMemories: params.maxMemories, scope: params.scope });
1130
1233
  return textContent(formatContext(ctx));
1131
1234
  }
1132
1235
  );
@@ -1134,6 +1237,8 @@ function registerContextTools(server2, engine2) {
1134
1237
 
1135
1238
  // src/tools/graph.ts
1136
1239
  import { z as z4 } from "zod";
1240
+ var scopeSchema2 = z4.enum(["project", "global"]).default("project").describe('Scope: "project" (default) stores in current project, "global" stores across all projects');
1241
+ var searchScopeSchema4 = z4.enum(["project", "global", "all"]).default("project").describe('Scope: "project" (default) searches project + global, "global" searches global only, "all" searches everything');
1137
1242
  function registerGraphTools(server2, engine2) {
1138
1243
  server2.tool(
1139
1244
  "add_entity",
@@ -1141,7 +1246,8 @@ function registerGraphTools(server2, engine2) {
1141
1246
  {
1142
1247
  name: z4.string().describe("Entity name"),
1143
1248
  summary: z4.string().optional().describe("Summary"),
1144
- entityType: z4.string().optional().describe("Type (person, project, technology, etc.)")
1249
+ entityType: z4.string().optional().describe("Type (person, project, technology, etc.)"),
1250
+ scope: scopeSchema2
1145
1251
  },
1146
1252
  async (params) => {
1147
1253
  const node = await engine2.addEntity(params);
@@ -1158,7 +1264,8 @@ ID: ${node.id}`);
1158
1264
  targetId: z4.string().describe("Target node ID"),
1159
1265
  relation: z4.string().describe("Relation (works_at, uses, depends_on, etc.)"),
1160
1266
  fact: z4.string().optional().describe("Fact description"),
1161
- validAt: z4.string().optional().describe("Valid-from timestamp (ISO 8601)")
1267
+ validAt: z4.string().optional().describe("Valid-from timestamp (ISO 8601)"),
1268
+ scope: scopeSchema2
1162
1269
  },
1163
1270
  async (params) => {
1164
1271
  const edge = await engine2.addEdge(params);
@@ -1201,11 +1308,13 @@ New ID: ${edge.id}`);
1201
1308
  {
1202
1309
  query: z4.string().describe("Search query"),
1203
1310
  limit: z4.number().int().min(1).max(100).default(10).describe("Max results"),
1204
- traverseDepth: z4.number().int().min(0).max(2).default(1).describe("Graph traversal depth (0=none, 1=1-hop, 2=2-hop)")
1311
+ traverseDepth: z4.number().int().min(0).max(2).default(1).describe("Graph traversal depth (0=none, 1=1-hop, 2=2-hop)"),
1312
+ scope: searchScopeSchema4
1205
1313
  },
1206
1314
  async (params) => {
1207
1315
  const results = await engine2.searchGraph(params.query, params.limit, {
1208
- traverseDepth: params.traverseDepth
1316
+ traverseDepth: params.traverseDepth,
1317
+ scope: params.scope
1209
1318
  });
1210
1319
  return textContent(formatSearchResults(results));
1211
1320
  }
@@ -1214,6 +1323,8 @@ New ID: ${edge.id}`);
1214
1323
 
1215
1324
  // src/tools/episode.ts
1216
1325
  import { z as z5 } from "zod";
1326
+ var scopeSchema3 = z5.enum(["project", "global"]).default("project").describe('Scope: "project" (default) stores in current project, "global" stores across all projects');
1327
+ var searchScopeSchema5 = z5.enum(["project", "global", "all"]).default("project").describe('Scope: "project" (default) searches project + global, "global" searches global only, "all" searches everything');
1217
1328
  function registerEpisodeTools(server2, engine2) {
1218
1329
  server2.tool(
1219
1330
  "add_episode",
@@ -1225,7 +1336,8 @@ function registerEpisodeTools(server2, engine2) {
1225
1336
  refs: z5.array(z5.object({
1226
1337
  refType: z5.enum(["node", "edge"]).describe("Reference type"),
1227
1338
  refId: z5.string().describe("Referenced entity ID")
1228
- })).optional().describe("References to related nodes/edges")
1339
+ })).optional().describe("References to related nodes/edges"),
1340
+ scope: scopeSchema3
1229
1341
  },
1230
1342
  async (params) => {
1231
1343
  const refs = params.refs?.map((r) => ({
@@ -1235,7 +1347,7 @@ function registerEpisodeTools(server2, engine2) {
1235
1347
  refId: r.refId
1236
1348
  }));
1237
1349
  const episode = await engine2.addEpisode(
1238
- { content: params.content, contentType: params.contentType, source: params.source },
1350
+ { content: params.content, contentType: params.contentType, source: params.source, scope: params.scope },
1239
1351
  refs
1240
1352
  );
1241
1353
  return textContent(`Episode added:
@@ -1259,10 +1371,11 @@ ID: ${episode.id}`);
1259
1371
  "list_episodes",
1260
1372
  "List episodes",
1261
1373
  {
1262
- limit: z5.number().int().min(1).max(100).default(20).describe("Max results")
1374
+ limit: z5.number().int().min(1).max(100).default(20).describe("Max results"),
1375
+ scope: searchScopeSchema5
1263
1376
  },
1264
1377
  async (params) => {
1265
- const episodes = await engine2.listEpisodes(params.limit);
1378
+ const episodes = await engine2.listEpisodes(params.limit, params.scope);
1266
1379
  if (episodes.length === 0) return textContent("No episodes found");
1267
1380
  const lines = episodes.map((ep, i) => `${i + 1}. ${formatEpisode(ep)}
1268
1381
  ID: ${ep.id}`);
@@ -1364,11 +1477,72 @@ function createServer(engine2) {
1364
1477
  }
1365
1478
 
1366
1479
  // src/index.ts
1480
+ var VERSION = "0.2.0";
1481
+ var c = {
1482
+ reset: "\x1B[0m",
1483
+ bold: "\x1B[1m",
1484
+ dim: "\x1B[2m",
1485
+ amber: "\x1B[38;5;214m",
1486
+ yellow: "\x1B[38;5;220m",
1487
+ gray: "\x1B[38;5;245m",
1488
+ green: "\x1B[38;5;114m",
1489
+ white: "\x1B[97m"
1490
+ };
1367
1491
  var dbPath = process.env["COHAKU_DB_PATH"] ?? join(homedir(), ".config", "cohaku", "memory.db");
1368
- var engine = new MemoryEngine({ dbPath });
1492
+ function detectProjectId() {
1493
+ const envProjectId = process.env["COHAKU_PROJECT_ID"];
1494
+ if (envProjectId) return envProjectId;
1495
+ try {
1496
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
1497
+ encoding: "utf-8",
1498
+ stdio: ["pipe", "pipe", "pipe"]
1499
+ }).trim();
1500
+ return gitRoot || void 0;
1501
+ } catch {
1502
+ return void 0;
1503
+ }
1504
+ }
1505
+ async function checkForUpdate() {
1506
+ try {
1507
+ const res = await fetch("https://registry.npmjs.org/@cohaku/mcp/latest", {
1508
+ signal: AbortSignal.timeout(3e3)
1509
+ });
1510
+ if (!res.ok) return null;
1511
+ const data = await res.json();
1512
+ if (data.version && data.version !== VERSION) return data.version;
1513
+ return null;
1514
+ } catch {
1515
+ return null;
1516
+ }
1517
+ }
1518
+ function printBanner(projectId, latestVersion2) {
1519
+ const projectName = projectId ? basename(projectId) : void 0;
1520
+ const scopeLabel = projectName ? `${c.green}${projectName}${c.reset}` : `${c.gray}global${c.reset}`;
1521
+ const lines = [
1522
+ "",
1523
+ ` ${c.amber}${c.bold}\u25C6 Cohaku AI${c.reset} ${c.dim}v${VERSION}${c.reset}`
1524
+ ];
1525
+ if (latestVersion2) {
1526
+ lines.push(` ${c.yellow}\u26A0 v${latestVersion2} available${c.reset} ${c.dim}\u2014 npx @cohaku/mcp@latest${c.reset}`);
1527
+ }
1528
+ lines.push(
1529
+ "",
1530
+ ` ${c.gray}Scope${c.reset} ${scopeLabel}`,
1531
+ ` ${c.gray}Database${c.reset} ${c.dim}${dbPath.replace(homedir(), "~")}${c.reset}`,
1532
+ ` ${c.gray}Transport${c.reset} ${c.dim}stdio${c.reset}`,
1533
+ "",
1534
+ ` ${c.green}\u25CF${c.reset} ${c.white}Ready${c.reset} ${c.dim}\u2014 waiting for MCP client${c.reset}`,
1535
+ ""
1536
+ );
1537
+ process.stderr.write(lines.join("\n") + "\n");
1538
+ }
1539
+ var defaultProjectId = detectProjectId();
1540
+ var engine = new MemoryEngine({ dbPath, defaultProjectId });
1369
1541
  await engine.initialize();
1370
1542
  var server = createServer(engine);
1371
1543
  var transport = new StdioServerTransport();
1544
+ var latestVersion = await checkForUpdate();
1545
+ printBanner(defaultProjectId, latestVersion);
1372
1546
  await server.connect(transport);
1373
1547
  process.on("SIGINT", () => {
1374
1548
  engine.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cohaku/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Cohaku persistent memory layer",
5
5
  "type": "module",
6
6
  "license": "MIT",