gnosys 5.0.0 → 5.1.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.
package/dist/index.js CHANGED
@@ -311,6 +311,53 @@ server.tool("gnosys_list", "List memories across all stores, optionally filtered
311
311
  projectRoot: projectRootParam,
312
312
  }, async ({ category, tag, store: storeFilter, status, projectRoot }) => {
313
313
  const ctx = await resolveToolContext(projectRoot);
314
+ // DB-first: read from central DB instead of scanning markdown files
315
+ const db = ctx.centralDb;
316
+ if (db?.isAvailable()) {
317
+ let dbMemories = status === "active" || !status
318
+ ? db.getActiveMemories()
319
+ : db.getAllMemories();
320
+ // Apply filters on DB results
321
+ if (status && status !== "active") {
322
+ dbMemories = dbMemories.filter((m) => m.status === status);
323
+ }
324
+ if (storeFilter) {
325
+ dbMemories = dbMemories.filter((m) => m.scope === storeFilter);
326
+ }
327
+ if (category) {
328
+ dbMemories = dbMemories.filter((m) => m.category === category);
329
+ }
330
+ if (tag) {
331
+ dbMemories = dbMemories.filter((m) => {
332
+ try {
333
+ const parsed = JSON.parse(m.tags || "[]");
334
+ const tagList = Array.isArray(parsed)
335
+ ? parsed
336
+ : Object.values(parsed).flat();
337
+ return tagList.includes(tag);
338
+ }
339
+ catch {
340
+ return false;
341
+ }
342
+ });
343
+ }
344
+ // Filter by project if we have a project ID (so scoped queries only see their project)
345
+ if (ctx.projectId && !storeFilter) {
346
+ dbMemories = dbMemories.filter((m) => m.project_id === ctx.projectId || m.scope !== "project");
347
+ }
348
+ const lines = dbMemories.map((m) => `- [${m.scope}] **${m.title}** (${m.category}/${m.id}) [${m.status}]`);
349
+ return {
350
+ content: [
351
+ {
352
+ type: "text",
353
+ text: lines.length > 0
354
+ ? `${lines.length} memories:\n\n${lines.join("\n")}`
355
+ : "No memories match the filter.",
356
+ },
357
+ ],
358
+ };
359
+ }
360
+ // Fallback: read from markdown files if central DB unavailable
314
361
  let memories = await ctx.resolver.getAllMemories();
315
362
  if (storeFilter) {
316
363
  memories = memories.filter((m) => m.sourceLayer === storeFilter || m.sourceLabel === storeFilter);
@@ -384,7 +431,13 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
384
431
  }
385
432
  try {
386
433
  const result = await ingestion.ingest(input);
387
- const id = await writeTarget.store.generateId(result.category);
434
+ if (!ctx.gnosysDb?.isAvailable()) {
435
+ return {
436
+ content: [{ type: "text", text: "Database not available. Cannot write memory." }],
437
+ isError: true,
438
+ };
439
+ }
440
+ const id = ctx.gnosysDb.getNextId(result.category, ctx.projectId ?? undefined);
388
441
  const today = new Date().toISOString().split("T")[0];
389
442
  const frontmatter = {
390
443
  id,
@@ -401,19 +454,15 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
401
454
  status: "active",
402
455
  supersedes: null,
403
456
  };
404
- const filename = `${result.filename}.md`;
405
457
  const content = `# ${result.title}\n\n${result.content}`;
406
- const relativePath = await writeTarget.store.writeMemory(result.category, filename, frontmatter, content);
407
- // v2.0: Dual-write to gnosys.db
408
- if (ctx.gnosysDb?.isAvailable()) {
409
- syncMemoryToDb(ctx.gnosysDb, frontmatter, content, relativePath);
410
- auditToDb(ctx.gnosysDb, "write", id, { tool: "gnosys_add", category: result.category });
411
- }
458
+ // Write to DB only (SQLite is sole source of truth)
459
+ syncMemoryToDb(ctx.gnosysDb, frontmatter, content, undefined, ctx.projectId, "project");
460
+ auditToDb(ctx.gnosysDb, "write", id, { tool: "gnosys_add", category: result.category });
412
461
  // Rebuild search index across all stores
413
462
  if (ctx.search) {
414
463
  await reindexAllStores();
415
464
  }
416
- let response = `Memory added to [${writeTarget.label}]: **${result.title}**\nPath: ${writeTarget.label}:${relativePath}\nCategory: ${result.category}\nConfidence: ${result.confidence}`;
465
+ let response = `Memory added to [${writeTarget.label}]: **${result.title}**\nID: ${id}\nCategory: ${result.category}\nConfidence: ${result.confidence}`;
417
466
  if (result.proposedNewTags && result.proposedNewTags.length > 0) {
418
467
  const proposed = result.proposedNewTags
419
468
  .map((t) => `${t.category}:${t.tag}`)
@@ -424,7 +473,7 @@ server.tool("gnosys_add", "Add a new memory. Accepts raw text — an LLM structu
424
473
  if (ctx.search && result.relevance) {
425
474
  const related = ctx.search.discover(result.relevance.split(" ").slice(0, 5).join(" "), 5);
426
475
  // Filter out the memory we just added
427
- const overlaps = related.filter((r) => !r.relative_path.endsWith(filename));
476
+ const overlaps = related.filter((r) => r.title !== result.title);
428
477
  if (overlaps.length > 0) {
429
478
  response += `\n\n⚠️ Potential overlaps detected — review these for contradictions:`;
430
479
  for (const o of overlaps.slice(0, 3)) {
@@ -475,12 +524,13 @@ server.tool("gnosys_add_structured", "Add a memory with structured input (no LLM
475
524
  isError: true,
476
525
  };
477
526
  }
478
- const id = await writeTarget.store.generateId(category);
479
- const slug = title
480
- .toLowerCase()
481
- .replace(/[^a-z0-9]+/g, "-")
482
- .replace(/^-|-$/g, "")
483
- .substring(0, 60);
527
+ if (!ctx.gnosysDb?.isAvailable()) {
528
+ return {
529
+ content: [{ type: "text", text: "Database not available. Cannot write memory." }],
530
+ isError: true,
531
+ };
532
+ }
533
+ const id = ctx.gnosysDb.getNextId(category, ctx.projectId ?? undefined);
484
534
  const today = new Date().toISOString().split("T")[0];
485
535
  const frontmatter = {
486
536
  id,
@@ -498,19 +548,16 @@ server.tool("gnosys_add_structured", "Add a memory with structured input (no LLM
498
548
  supersedes: null,
499
549
  };
500
550
  const fullContent = `# ${title}\n\n${content}`;
501
- const relativePath = await writeTarget.store.writeMemory(category, `${slug}.md`, frontmatter, fullContent);
502
- // v2.0: Dual-write to gnosys.db
503
- if (ctx.gnosysDb?.isAvailable()) {
504
- syncMemoryToDb(ctx.gnosysDb, frontmatter, fullContent, relativePath);
505
- auditToDb(ctx.gnosysDb, "write", id, { tool: "gnosys_add_structured", category });
506
- }
551
+ // Write to DB only (SQLite is sole source of truth)
552
+ syncMemoryToDb(ctx.gnosysDb, frontmatter, fullContent, undefined, ctx.projectId, "project");
553
+ auditToDb(ctx.gnosysDb, "write", id, { tool: "gnosys_add_structured", category });
507
554
  if (ctx.search)
508
555
  await reindexAllStores();
509
556
  return {
510
557
  content: [
511
558
  {
512
559
  type: "text",
513
- text: `Memory added to [${writeTarget.label}]: **${title}**\nPath: ${writeTarget.label}:${relativePath}`,
560
+ text: `Memory added to [${writeTarget.label}]: **${title}**\nID: ${id}`,
514
561
  },
515
562
  ],
516
563
  };
@@ -580,14 +627,9 @@ server.tool("gnosys_reinforce", "Signal whether a memory was useful. 'useful' re
580
627
  const sourceStore = ctx.resolver
581
628
  .getStores()
582
629
  .find((s) => s.label === memory.sourceLabel);
583
- if (sourceStore?.writable) {
630
+ if (sourceStore) {
584
631
  const count = (memory.frontmatter.reinforcement_count || 0) + 1;
585
- await sourceStore.store.updateMemory(memory.relativePath, {
586
- modified: new Date().toISOString().split("T")[0],
587
- reinforcement_count: count,
588
- last_reinforced: new Date().toISOString().split("T")[0],
589
- });
590
- // v2.0: Sync reinforcement to gnosys.db
632
+ // Write reinforcement to DB only (SQLite is sole source of truth)
591
633
  if (ctx.gnosysDb?.isAvailable()) {
592
634
  syncReinforcementToDb(ctx.gnosysDb, memory_id, count);
593
635
  auditToDb(ctx.gnosysDb, "reinforce", memory_id, { signal, context });
@@ -603,7 +645,7 @@ server.tool("gnosys_reinforce", "Signal whether a memory was useful. 'useful' re
603
645
  return { content: [{ type: "text", text: messages[signal] }] };
604
646
  });
605
647
  // ─── Tool: gnosys_init ───────────────────────────────────────────────────
606
- server.tool("gnosys_init", "Initialize Gnosys in a project directory. Creates .gnosys/ with project identity (gnosys.json), registers the project in the central DB (~/.gnosys/gnosys.db), and sets up tag registry + git. You MUST run this before any other Gnosys tool in a new project. Pass the full absolute path to the project root.", {
648
+ server.tool("gnosys_init", "Initialize Gnosys in a project directory. Creates .gnosys/ with project identity (gnosys.json), registers the project in the central DB (~/.gnosys/gnosys.db), and sets up tag registry. You MUST run this before any other Gnosys tool in a new project. Pass the full absolute path to the project root.", {
607
649
  directory: z
608
650
  .string()
609
651
  .describe("Absolute path to the project directory to initialize. Required."),
@@ -623,7 +665,7 @@ server.tool("gnosys_init", "Initialize Gnosys in a project directory. Creates .g
623
665
  // Good — doesn't exist yet
624
666
  }
625
667
  if (!isResync) {
626
- // Create directory structure
668
+ // Create directory structure (DB is sole source of truth — no category folders or changelog)
627
669
  await fs.mkdir(storePath, { recursive: true });
628
670
  await fs.mkdir(path.join(storePath, ".config"), { recursive: true });
629
671
  // Seed default tag registry
@@ -640,22 +682,6 @@ server.tool("gnosys_init", "Initialize Gnosys in a project directory. Creates .g
640
682
  status_tag: ["draft", "stable", "deprecated", "experimental"],
641
683
  };
642
684
  await fs.writeFile(path.join(storePath, ".config", "tags.json"), JSON.stringify(defaultRegistry, null, 2), "utf-8");
643
- // Seed changelog
644
- const changelog = `# Gnosys Changelog\n\n## ${new Date().toISOString().split("T")[0]}\n\n- Store initialized\n`;
645
- await fs.writeFile(path.join(storePath, "CHANGELOG.md"), changelog, "utf-8");
646
- // Init git
647
- try {
648
- const { execSync } = await import("child_process");
649
- execSync("git init", { cwd: storePath, stdio: "pipe" });
650
- execSync("git add -A", { cwd: storePath, stdio: "pipe" });
651
- execSync('git commit -m "Initialize Gnosys store"', {
652
- cwd: storePath,
653
- stdio: "pipe",
654
- });
655
- }
656
- catch {
657
- // Git not available — that's fine
658
- }
659
685
  }
660
686
  // v3.0: Create/update project identity and register in central DB
661
687
  const identity = await createProjectIdentity(targetDir, {
@@ -680,11 +706,82 @@ server.tool("gnosys_init", "Initialize Gnosys in a project directory. Creates .g
680
706
  content: [
681
707
  {
682
708
  type: "text",
683
- text: `Gnosys store ${action} at ${storePath}\n\nProject Identity:\n- ID: ${identity.projectId}\n- Name: ${identity.projectName}\n- Directory: ${identity.workingDirectory}\n- Agent rules target: ${identity.agentRulesTarget || "none detected"}\n- Central DB: ${centralDb?.isAvailable() ? "registered ✓" : "not available"}\n\n${isResync ? "Identity re-synced." : "Created:\n- gnosys.json (project identity)\n- .config/ (internal config)\n- tags.json (tag registry)\n- CHANGELOG.md\n- git repo"}\n\nThe store is ready. Use gnosys_discover to find existing memories or gnosys_add to create new ones.`,
709
+ text: `Gnosys store ${action} at ${storePath}\n\nProject Identity:\n- ID: ${identity.projectId}\n- Name: ${identity.projectName}\n- Directory: ${identity.workingDirectory}\n- Agent rules target: ${identity.agentRulesTarget || "none detected"}\n- Central DB: ${centralDb?.isAvailable() ? "registered ✓" : "not available"}\n\n${isResync ? "Identity re-synced." : "Created:\n- gnosys.json (project identity)\n- .config/ (internal config)\n- tags.json (tag registry)"}\n\nThe store is ready. Use gnosys_discover to find existing memories or gnosys_add to create new ones.`,
684
710
  },
685
711
  ],
686
712
  };
687
713
  });
714
+ // ─── Tool: gnosys_migrate ────────────────────────────────────────────────
715
+ server.tool("gnosys_migrate", "Migrate a Gnosys store (.gnosys/) from one directory to another. Updates the project name, working directory, and central DB registration. Use this when a project has moved or you want to consolidate stores.", {
716
+ sourcePath: z.string().describe("Directory that currently contains .gnosys/ (absolute path)"),
717
+ targetPath: z.string().describe("Directory to move .gnosys/ into (absolute path)"),
718
+ newName: z.string().optional().describe("New project name (default: basename of target directory)"),
719
+ syncMemories: z.boolean().optional().default(false).describe("Sync markdown memories into central DB after migration"),
720
+ deleteOld: z.boolean().optional().default(false).describe("Delete the old .gnosys/ directory after successful migration"),
721
+ }, async ({ sourcePath, targetPath, newName, syncMemories, deleteOld }) => {
722
+ try {
723
+ const { migrateProject } = await import("./lib/projectIdentity.js");
724
+ const result = await migrateProject({
725
+ sourcePath,
726
+ targetPath,
727
+ newName,
728
+ deleteSource: deleteOld,
729
+ centralDb: centralDb || undefined,
730
+ });
731
+ const resolvedTargetPath = path.resolve(targetPath);
732
+ const newStorePath = path.join(resolvedTargetPath, ".gnosys");
733
+ let summary = `Migration complete!\n\n`;
734
+ summary += `Project: ${result.oldIdentity.projectName} → ${result.newIdentity.projectName}\n`;
735
+ summary += `Path: ${result.oldIdentity.workingDirectory} → ${result.newIdentity.workingDirectory}\n`;
736
+ summary += `Memory files: ${result.memoryFileCount}\n`;
737
+ summary += `Central DB: ${centralDb?.isAvailable() ? "updated ✓" : "not available"}`;
738
+ // Sync memories to central DB if requested
739
+ if (syncMemories && centralDb?.isAvailable()) {
740
+ const matter = (await import("gray-matter")).default;
741
+ const { syncMemoryToDb } = await import("./lib/dbWrite.js");
742
+ const { glob } = await import("glob");
743
+ const mdFiles = await glob("**/*.md", {
744
+ cwd: newStorePath,
745
+ ignore: ["**/CHANGELOG.md", "**/MANIFEST.md", "**/.git/**", "**/.obsidian/**"],
746
+ });
747
+ let synced = 0;
748
+ for (const file of mdFiles) {
749
+ try {
750
+ const filePath = path.join(newStorePath, file);
751
+ const raw = await fs.readFile(filePath, "utf-8");
752
+ const parsed = matter(raw);
753
+ if (parsed.data?.id) {
754
+ syncMemoryToDb(centralDb, parsed.data, parsed.content, filePath, result.newIdentity.projectId, "project");
755
+ synced++;
756
+ }
757
+ }
758
+ catch {
759
+ // Skip files that fail to parse
760
+ }
761
+ }
762
+ summary += `\n\nSynced ${synced} memories to central DB.`;
763
+ }
764
+ if (deleteOld) {
765
+ summary += `\n\nOld .gnosys/ at ${sourcePath} has been removed.`;
766
+ }
767
+ // Add the new store location to the resolver so future tool calls find it
768
+ await resolver.registerProject(resolvedTargetPath);
769
+ await resolver.addProjectStore(newStorePath);
770
+ const writeTarget = resolver.getWriteTarget();
771
+ if (writeTarget) {
772
+ search = new GnosysSearch(writeTarget.store.getStorePath());
773
+ tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
774
+ await tagRegistry.load();
775
+ ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
776
+ await reindexAllStores();
777
+ }
778
+ return { content: [{ type: "text", text: summary }] };
779
+ }
780
+ catch (err) {
781
+ const msg = err instanceof Error ? err.message : String(err);
782
+ return { content: [{ type: "text", text: `Migration failed: ${msg}` }], isError: true };
783
+ }
784
+ });
688
785
  // ─── Tool: gnosys_update ─────────────────────────────────────────────────
689
786
  server.tool("gnosys_update", "Update an existing memory's frontmatter and/or content. Specify the memory path and the fields to change.", {
690
787
  path: z
@@ -758,37 +855,25 @@ server.tool("gnosys_update", "Update an existing memory's frontmatter and/or con
758
855
  if (superseded_by !== undefined)
759
856
  updates.superseded_by = superseded_by;
760
857
  const fullContent = newContent ? `# ${title || memory.frontmatter.title}\n\n${newContent}` : undefined;
761
- const updated = await sourceStore.store.updateMemory(memory.relativePath, updates, fullContent);
762
- if (!updated) {
858
+ const memoryId = memory.frontmatter.id;
859
+ if (!memoryId) {
763
860
  return {
764
- content: [{ type: "text", text: `Failed to update: ${memPath}` }],
861
+ content: [{ type: "text", text: `Memory has no ID: ${memPath}` }],
765
862
  isError: true,
766
863
  };
767
864
  }
768
- // Supersession cross-linking: if A supersedes B, mark B as superseded_by A
769
- if (supersedes && updated.frontmatter.id) {
770
- const allMemories = await ctx.resolver.getAllMemories();
771
- const supersededMemory = allMemories.find((m) => m.frontmatter.id === supersedes);
772
- if (supersededMemory) {
773
- const supersededStore = ctx.resolver
774
- .getStores()
775
- .find((s) => s.label === supersededMemory.sourceLabel);
776
- if (supersededStore?.writable) {
777
- await supersededStore.store.updateMemory(supersededMemory.relativePath, {
778
- superseded_by: updated.frontmatter.id,
779
- status: "superseded",
780
- });
781
- }
782
- }
865
+ if (!ctx.gnosysDb?.isAvailable()) {
866
+ return {
867
+ content: [{ type: "text", text: "Database not available. Cannot update memory." }],
868
+ isError: true,
869
+ };
783
870
  }
784
- // v2.0: Dual-write update to gnosys.db
785
- if (ctx.gnosysDb?.isAvailable() && updated.frontmatter.id) {
786
- syncUpdateToDb(ctx.gnosysDb, updated.frontmatter.id, updates, fullContent);
787
- auditToDb(ctx.gnosysDb, "write", updated.frontmatter.id, { tool: "gnosys_update", changed: Object.keys(updates) });
788
- // Cross-link supersession in db too
789
- if (supersedes) {
790
- syncUpdateToDb(ctx.gnosysDb, supersedes, { superseded_by: updated.frontmatter.id, status: "superseded" });
791
- }
871
+ // Write update to DB only (SQLite is sole source of truth)
872
+ syncUpdateToDb(ctx.gnosysDb, memoryId, updates, fullContent);
873
+ auditToDb(ctx.gnosysDb, "write", memoryId, { tool: "gnosys_update", changed: Object.keys(updates) });
874
+ // Supersession cross-linking: if A supersedes B, mark B as superseded_by A
875
+ if (supersedes) {
876
+ syncUpdateToDb(ctx.gnosysDb, supersedes, { superseded_by: memoryId, status: "superseded" });
792
877
  }
793
878
  // Rebuild search index
794
879
  if (ctx.search)
@@ -796,11 +881,12 @@ server.tool("gnosys_update", "Update an existing memory's frontmatter and/or con
796
881
  const changedFields = Object.keys(updates);
797
882
  if (newContent)
798
883
  changedFields.push("content");
884
+ const updatedTitle = title || memory.frontmatter.title;
799
885
  return {
800
886
  content: [
801
887
  {
802
888
  type: "text",
803
- text: `Memory updated: **${updated.frontmatter.title}**\nPath: ${memory.sourceLabel}:${memory.relativePath}\nChanged: ${changedFields.join(", ")}`,
889
+ text: `Memory updated: **${updatedTitle}**\nID: ${memoryId}\nChanged: ${changedFields.join(", ")}`,
804
890
  },
805
891
  ],
806
892
  };
@@ -958,8 +1044,12 @@ Output ONLY the JSON array, no markdown fences.`,
958
1044
  else {
959
1045
  // Actually add via ingestion
960
1046
  try {
1047
+ if (!ctx.gnosysDb?.isAvailable()) {
1048
+ results.push(`❌ FAILED: "${candidate.summary}": Database not available`);
1049
+ continue;
1050
+ }
961
1051
  const result = await ingestion.ingest(candidate.summary);
962
- const id = await writeTarget.store.generateId(result.category);
1052
+ const id = ctx.gnosysDb.getNextId(result.category, ctx.projectId ?? undefined);
963
1053
  const today = new Date().toISOString().split("T")[0];
964
1054
  const frontmatter = {
965
1055
  id,
@@ -976,10 +1066,11 @@ Output ONLY the JSON array, no markdown fences.`,
976
1066
  status: "active",
977
1067
  supersedes: null,
978
1068
  };
979
- const filename = `${result.filename}.md`;
980
1069
  const content = `# ${result.title}\n\n${result.content}`;
981
- const relPath = await writeTarget.store.writeMemory(result.category, filename, frontmatter, content);
982
- results.push(`➕ ADDED: "${result.title}"\n Path: ${writeTarget.label}:${relPath}`);
1070
+ // Write to DB only (SQLite is sole source of truth)
1071
+ syncMemoryToDb(ctx.gnosysDb, frontmatter, content, undefined, ctx.projectId, "project");
1072
+ auditToDb(ctx.gnosysDb, "write", id, { tool: "gnosys_commit_context", category: result.category });
1073
+ results.push(`➕ ADDED: "${result.title}"\n ID: ${id}`);
983
1074
  added++;
984
1075
  }
985
1076
  catch (err) {
@@ -1766,6 +1857,13 @@ async function reindexAllStores() {
1766
1857
  if (!search)
1767
1858
  return;
1768
1859
  search.clearIndex();
1860
+ // DB-first: read from central DB instead of scanning markdown files
1861
+ if (centralDb?.isAvailable()) {
1862
+ const memories = centralDb.getActiveMemories();
1863
+ search.addDbMemories(memories);
1864
+ return;
1865
+ }
1866
+ // Fallback: read from markdown files if central DB unavailable
1769
1867
  const allStores = resolver.getStores();
1770
1868
  for (const s of allStores) {
1771
1869
  await search.addStoreMemories(s.store, s.label);