gnosys-mcp 0.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +927 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +1092 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/bootstrap.d.ts +56 -0
  12. package/dist/lib/bootstrap.d.ts.map +1 -0
  13. package/dist/lib/bootstrap.js +163 -0
  14. package/dist/lib/bootstrap.js.map +1 -0
  15. package/dist/lib/history.d.ts +39 -0
  16. package/dist/lib/history.d.ts.map +1 -0
  17. package/dist/lib/history.js +86 -0
  18. package/dist/lib/history.js.map +1 -0
  19. package/dist/lib/ingest.d.ts +47 -0
  20. package/dist/lib/ingest.d.ts.map +1 -0
  21. package/dist/lib/ingest.js +134 -0
  22. package/dist/lib/ingest.js.map +1 -0
  23. package/dist/lib/lensing.d.ts +35 -0
  24. package/dist/lib/lensing.d.ts.map +1 -0
  25. package/dist/lib/lensing.js +85 -0
  26. package/dist/lib/lensing.js.map +1 -0
  27. package/dist/lib/resolver.d.ts +84 -0
  28. package/dist/lib/resolver.d.ts.map +1 -0
  29. package/dist/lib/resolver.js +269 -0
  30. package/dist/lib/resolver.js.map +1 -0
  31. package/dist/lib/search.d.ts +50 -0
  32. package/dist/lib/search.d.ts.map +1 -0
  33. package/dist/lib/search.js +166 -0
  34. package/dist/lib/search.js.map +1 -0
  35. package/dist/lib/store.d.ts +67 -0
  36. package/dist/lib/store.d.ts.map +1 -0
  37. package/dist/lib/store.js +168 -0
  38. package/dist/lib/store.js.map +1 -0
  39. package/dist/lib/tags.d.ts +26 -0
  40. package/dist/lib/tags.d.ts.map +1 -0
  41. package/dist/lib/tags.js +109 -0
  42. package/dist/lib/tags.js.map +1 -0
  43. package/dist/lib/timeline.d.ts +34 -0
  44. package/dist/lib/timeline.d.ts.map +1 -0
  45. package/dist/lib/timeline.js +116 -0
  46. package/dist/lib/timeline.js.map +1 -0
  47. package/dist/lib/wikilinks.d.ts +63 -0
  48. package/dist/lib/wikilinks.d.ts.map +1 -0
  49. package/dist/lib/wikilinks.js +146 -0
  50. package/dist/lib/wikilinks.js.map +1 -0
  51. package/dist/test/bootstrap.test.d.ts +2 -0
  52. package/dist/test/bootstrap.test.d.ts.map +1 -0
  53. package/dist/test/bootstrap.test.js +208 -0
  54. package/dist/test/bootstrap.test.js.map +1 -0
  55. package/dist/test/history.test.d.ts +2 -0
  56. package/dist/test/history.test.d.ts.map +1 -0
  57. package/dist/test/history.test.js +141 -0
  58. package/dist/test/history.test.js.map +1 -0
  59. package/dist/test/ingest.test.d.ts +2 -0
  60. package/dist/test/ingest.test.d.ts.map +1 -0
  61. package/dist/test/ingest.test.js +132 -0
  62. package/dist/test/ingest.test.js.map +1 -0
  63. package/dist/test/lensing.test.d.ts +2 -0
  64. package/dist/test/lensing.test.d.ts.map +1 -0
  65. package/dist/test/lensing.test.js +154 -0
  66. package/dist/test/lensing.test.js.map +1 -0
  67. package/dist/test/search.test.d.ts +2 -0
  68. package/dist/test/search.test.d.ts.map +1 -0
  69. package/dist/test/search.test.js +129 -0
  70. package/dist/test/search.test.js.map +1 -0
  71. package/dist/test/store.test.d.ts +2 -0
  72. package/dist/test/store.test.d.ts.map +1 -0
  73. package/dist/test/store.test.js +152 -0
  74. package/dist/test/store.test.js.map +1 -0
  75. package/dist/test/tags.test.d.ts +2 -0
  76. package/dist/test/tags.test.d.ts.map +1 -0
  77. package/dist/test/tags.test.js +81 -0
  78. package/dist/test/tags.test.js.map +1 -0
  79. package/dist/test/timeline.test.d.ts +2 -0
  80. package/dist/test/timeline.test.d.ts.map +1 -0
  81. package/dist/test/timeline.test.js +127 -0
  82. package/dist/test/timeline.test.js.map +1 -0
  83. package/dist/test/wikilinks.test.d.ts +2 -0
  84. package/dist/test/wikilinks.test.d.ts.map +1 -0
  85. package/dist/test/wikilinks.test.js +178 -0
  86. package/dist/test/wikilinks.test.js.map +1 -0
  87. package/package.json +56 -0
package/dist/cli.js ADDED
@@ -0,0 +1,927 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Gnosys CLI — Thin wrapper around the core modules.
4
+ * Uses the resolver for layered multi-store support.
5
+ */
6
+ import { Command } from "commander";
7
+ import path from "path";
8
+ import fs from "fs/promises";
9
+ import { GnosysResolver } from "./lib/resolver.js";
10
+ import { GnosysSearch } from "./lib/search.js";
11
+ import { GnosysTagRegistry } from "./lib/tags.js";
12
+ import { GnosysIngestion } from "./lib/ingest.js";
13
+ import { applyLens } from "./lib/lensing.js";
14
+ import { getFileHistory, rollbackToCommit, hasGitHistory, getFileDiff } from "./lib/history.js";
15
+ import { groupByPeriod, computeStats } from "./lib/timeline.js";
16
+ import { buildLinkGraph, getBacklinks, getOutgoingLinks, formatGraphSummary } from "./lib/wikilinks.js";
17
+ import { bootstrap, discoverFiles } from "./lib/bootstrap.js";
18
+ const program = new Command();
19
+ async function getResolver() {
20
+ const resolver = new GnosysResolver();
21
+ await resolver.resolve();
22
+ return resolver;
23
+ }
24
+ program
25
+ .name("gnosys")
26
+ .description("Gnosys — LLM-native persistent memory system")
27
+ .version("0.1.0");
28
+ // ─── gnosys read <path> ──────────────────────────────────────────────────
29
+ program
30
+ .command("read <memoryPath>")
31
+ .description("Read a specific memory. Supports layer prefix (e.g., project:decisions/auth.md)")
32
+ .action(async (memoryPath) => {
33
+ const resolver = await getResolver();
34
+ const memory = await resolver.readMemory(memoryPath);
35
+ if (!memory) {
36
+ console.error(`Memory not found: ${memoryPath}`);
37
+ process.exit(1);
38
+ }
39
+ const raw = await fs.readFile(memory.filePath, "utf-8");
40
+ console.log(`[Source: ${memory.sourceLabel}]\n`);
41
+ console.log(raw);
42
+ });
43
+ // ─── gnosys discover <query> ─────────────────────────────────────────────
44
+ program
45
+ .command("discover <query>")
46
+ .description("Discover relevant memories by keyword. Searches relevance clouds, titles, and tags — returns metadata only, no content.")
47
+ .option("-n, --limit <number>", "Max results", "20")
48
+ .action(async (query, opts) => {
49
+ const resolver = await getResolver();
50
+ const stores = resolver.getStores();
51
+ if (stores.length === 0) {
52
+ console.error("No Gnosys stores found.");
53
+ process.exit(1);
54
+ }
55
+ const search = new GnosysSearch(stores[0].path);
56
+ search.clearIndex();
57
+ for (const s of stores) {
58
+ await search.addStoreMemories(s.store, s.label);
59
+ }
60
+ const results = search.discover(query, parseInt(opts.limit));
61
+ if (results.length === 0) {
62
+ console.log(`No memories found for "${query}". Try gnosys search for full-text.`);
63
+ search.close();
64
+ return;
65
+ }
66
+ console.log(`Found ${results.length} relevant memories for "${query}":\n`);
67
+ for (const r of results) {
68
+ console.log(` ${r.title}`);
69
+ console.log(` ${r.relative_path}`);
70
+ if (r.relevance)
71
+ console.log(` Relevance: ${r.relevance}`);
72
+ console.log();
73
+ }
74
+ search.close();
75
+ });
76
+ // ─── gnosys search <query> ───────────────────────────────────────────────
77
+ program
78
+ .command("search <query>")
79
+ .description("Search memories by keyword across all stores")
80
+ .option("-n, --limit <number>", "Max results", "20")
81
+ .action(async (query, opts) => {
82
+ const resolver = await getResolver();
83
+ const stores = resolver.getStores();
84
+ if (stores.length === 0) {
85
+ console.error("No Gnosys stores found.");
86
+ process.exit(1);
87
+ }
88
+ const search = new GnosysSearch(stores[0].path);
89
+ search.clearIndex();
90
+ for (const s of stores) {
91
+ await search.addStoreMemories(s.store, s.label);
92
+ }
93
+ const results = search.search(query, parseInt(opts.limit));
94
+ if (results.length === 0) {
95
+ console.log(`No results for "${query}".`);
96
+ search.close();
97
+ return;
98
+ }
99
+ console.log(`Found ${results.length} results for "${query}":\n`);
100
+ for (const r of results) {
101
+ console.log(` ${r.title}`);
102
+ console.log(` ${r.relative_path}`);
103
+ console.log(` ${r.snippet.replace(/>>>/g, "").replace(/<<</g, "")}`);
104
+ console.log();
105
+ }
106
+ search.close();
107
+ });
108
+ // ─── gnosys list ─────────────────────────────────────────────────────────
109
+ program
110
+ .command("list")
111
+ .description("List all memories across all stores")
112
+ .option("-c, --category <category>", "Filter by category")
113
+ .option("-t, --tag <tag>", "Filter by tag")
114
+ .option("-s, --store <store>", "Filter by store layer")
115
+ .action(async (opts) => {
116
+ const resolver = await getResolver();
117
+ let memories = await resolver.getAllMemories();
118
+ if (opts.store) {
119
+ memories = memories.filter((m) => m.sourceLayer === opts.store || m.sourceLabel === opts.store);
120
+ }
121
+ if (opts.category) {
122
+ memories = memories.filter((m) => m.frontmatter.category === opts.category);
123
+ }
124
+ if (opts.tag) {
125
+ memories = memories.filter((m) => {
126
+ const tags = Array.isArray(m.frontmatter.tags)
127
+ ? m.frontmatter.tags
128
+ : Object.values(m.frontmatter.tags).flat();
129
+ return tags.includes(opts.tag);
130
+ });
131
+ }
132
+ console.log(`${memories.length} memories:\n`);
133
+ for (const m of memories) {
134
+ console.log(` [${m.sourceLabel}] [${m.frontmatter.status}] ${m.frontmatter.title}`);
135
+ console.log(` ${m.sourceLabel}:${m.relativePath}`);
136
+ console.log();
137
+ }
138
+ });
139
+ // ─── gnosys add <input> ──────────────────────────────────────────────────
140
+ program
141
+ .command("add <input>")
142
+ .description("Add a new memory (uses LLM to structure raw input)")
143
+ .option("-a, --author <author>", "Author (human|ai|human+ai)", "human")
144
+ .option("--authority <authority>", "Authority level (declared|observed|imported|inferred)", "declared")
145
+ .option("-s, --store <store>", "Target store (project|personal|global)", undefined)
146
+ .action(async (input, opts) => {
147
+ const resolver = await getResolver();
148
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
149
+ if (!writeTarget) {
150
+ console.error("No writable store found. Create a .gnosys/ directory or set GNOSYS_PERSONAL.");
151
+ process.exit(1);
152
+ }
153
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
154
+ await tagRegistry.load();
155
+ const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
156
+ if (!ingestion.isLLMAvailable) {
157
+ console.error("Error: ANTHROPIC_API_KEY not set. Smart ingestion requires an LLM.");
158
+ process.exit(1);
159
+ }
160
+ console.log("Structuring memory via LLM...");
161
+ const result = await ingestion.ingest(input);
162
+ const id = await writeTarget.store.generateId(result.category);
163
+ const today = new Date().toISOString().split("T")[0];
164
+ const frontmatter = {
165
+ id,
166
+ title: result.title,
167
+ category: result.category,
168
+ tags: result.tags,
169
+ relevance: result.relevance,
170
+ author: opts.author,
171
+ authority: opts.authority,
172
+ confidence: result.confidence,
173
+ created: today,
174
+ modified: today,
175
+ last_reviewed: today,
176
+ status: "active",
177
+ supersedes: null,
178
+ };
179
+ const content = `# ${result.title}\n\n${result.content}`;
180
+ const relPath = await writeTarget.store.writeMemory(result.category, `${result.filename}.md`, frontmatter, content);
181
+ console.log(`\nMemory added to [${writeTarget.label}]: ${result.title}`);
182
+ console.log(`Path: ${writeTarget.label}:${relPath}`);
183
+ console.log(`Category: ${result.category}`);
184
+ console.log(`Confidence: ${result.confidence}`);
185
+ if (result.proposedNewTags && result.proposedNewTags.length > 0) {
186
+ console.log("\nProposed new tags (not yet in registry):");
187
+ for (const t of result.proposedNewTags) {
188
+ console.log(` ${t.category}:${t.tag}`);
189
+ }
190
+ }
191
+ });
192
+ // ─── gnosys init ─────────────────────────────────────────────────────────
193
+ program
194
+ .command("init")
195
+ .description("Initialize a new .gnosys store in the current directory")
196
+ .option("-d, --directory <dir>", "Target directory (default: cwd)")
197
+ .action(async (opts) => {
198
+ const targetDir = opts.directory
199
+ ? path.resolve(opts.directory)
200
+ : process.cwd();
201
+ const storePath = path.join(targetDir, ".gnosys");
202
+ try {
203
+ await fs.stat(storePath);
204
+ console.error(`A .gnosys store already exists at ${storePath}`);
205
+ process.exit(1);
206
+ }
207
+ catch {
208
+ // Good
209
+ }
210
+ await fs.mkdir(storePath, { recursive: true });
211
+ await fs.mkdir(path.join(storePath, ".gnosys"), { recursive: true });
212
+ const defaultRegistry = {
213
+ domain: [
214
+ "architecture", "api", "auth", "database", "devops",
215
+ "frontend", "backend", "testing", "security", "performance",
216
+ ],
217
+ type: [
218
+ "decision", "concept", "convention", "requirement",
219
+ "observation", "fact", "question",
220
+ ],
221
+ concern: ["dx", "scalability", "maintainability", "reliability"],
222
+ status_tag: ["draft", "stable", "deprecated", "experimental"],
223
+ };
224
+ await fs.writeFile(path.join(storePath, "tags.json"), JSON.stringify(defaultRegistry, null, 2), "utf-8");
225
+ const changelog = `# Gnosys Changelog\n\n## ${new Date().toISOString().split("T")[0]}\n\n- Store initialized\n`;
226
+ await fs.writeFile(path.join(storePath, "CHANGELOG.md"), changelog, "utf-8");
227
+ try {
228
+ const { execSync } = await import("child_process");
229
+ execSync("git init", { cwd: storePath, stdio: "pipe" });
230
+ execSync("git add -A", { cwd: storePath, stdio: "pipe" });
231
+ execSync('git commit -m "Initialize Gnosys store"', {
232
+ cwd: storePath,
233
+ stdio: "pipe",
234
+ });
235
+ }
236
+ catch {
237
+ // Git not available
238
+ }
239
+ console.log(`Gnosys store initialized at ${storePath}`);
240
+ console.log(`\nCreated:`);
241
+ console.log(` .gnosys/ (internal config)`);
242
+ console.log(` tags.json (tag registry)`);
243
+ console.log(` CHANGELOG.md`);
244
+ console.log(` git repo`);
245
+ console.log(`\nStart adding memories with: gnosys add "your knowledge here"`);
246
+ });
247
+ // ─── gnosys stale ───────────────────────────────────────────────────────
248
+ program
249
+ .command("stale")
250
+ .description("Find memories not modified within a given number of days")
251
+ .option("-d, --days <number>", "Days threshold", "90")
252
+ .option("-n, --limit <number>", "Max results", "20")
253
+ .action(async (opts) => {
254
+ const resolver = await getResolver();
255
+ const threshold = parseInt(opts.days);
256
+ const cutoff = new Date();
257
+ cutoff.setDate(cutoff.getDate() - threshold);
258
+ const cutoffStr = cutoff.toISOString().split("T")[0];
259
+ const allMemories = await resolver.getAllMemories();
260
+ const stale = allMemories
261
+ .filter((m) => {
262
+ const lastTouched = m.frontmatter.last_reviewed || m.frontmatter.modified;
263
+ return lastTouched && lastTouched < cutoffStr;
264
+ })
265
+ .sort((a, b) => {
266
+ const aDate = a.frontmatter.last_reviewed || a.frontmatter.modified;
267
+ const bDate = b.frontmatter.last_reviewed || b.frontmatter.modified;
268
+ return (aDate || "").localeCompare(bDate || "");
269
+ })
270
+ .slice(0, parseInt(opts.limit));
271
+ if (stale.length === 0) {
272
+ console.log(`No memories older than ${threshold} days.`);
273
+ return;
274
+ }
275
+ console.log(`${stale.length} memories not touched in ${threshold}+ days:\n`);
276
+ for (const m of stale) {
277
+ const lr = m.frontmatter.last_reviewed;
278
+ console.log(` ${m.frontmatter.title}`);
279
+ console.log(` ${m.sourceLabel}:${m.relativePath}`);
280
+ console.log(` Modified: ${m.frontmatter.modified}${lr ? `, Reviewed: ${lr}` : ""}`);
281
+ console.log();
282
+ }
283
+ });
284
+ // ─── gnosys tags ─────────────────────────────────────────────────────────
285
+ program
286
+ .command("tags")
287
+ .description("List all tags in the registry")
288
+ .action(async () => {
289
+ const resolver = await getResolver();
290
+ const writeTarget = resolver.getWriteTarget();
291
+ if (!writeTarget) {
292
+ console.error("No store found.");
293
+ process.exit(1);
294
+ }
295
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
296
+ await tagRegistry.load();
297
+ const registry = tagRegistry.getRegistry();
298
+ for (const [category, tags] of Object.entries(registry)) {
299
+ console.log(`\n${category}:`);
300
+ console.log(` ${tags.sort().join(", ")}`);
301
+ }
302
+ });
303
+ // ─── gnosys update <path> ────────────────────────────────────────────────
304
+ program
305
+ .command("update <memoryPath>")
306
+ .description("Update an existing memory's frontmatter and/or content")
307
+ .option("--title <title>", "New title")
308
+ .option("--status <status>", "New status (active|archived|superseded)")
309
+ .option("--confidence <n>", "New confidence (0-1)")
310
+ .option("--relevance <keywords>", "Updated relevance keyword cloud")
311
+ .option("--supersedes <id>", "ID of memory this supersedes")
312
+ .option("--superseded-by <id>", "ID of memory that supersedes this one")
313
+ .option("--content <content>", "New markdown content (replaces body)")
314
+ .action(async (memPath, opts) => {
315
+ const resolver = await getResolver();
316
+ const memory = await resolver.readMemory(memPath);
317
+ if (!memory) {
318
+ console.error(`Memory not found: ${memPath}`);
319
+ process.exit(1);
320
+ }
321
+ const sourceStore = resolver
322
+ .getStores()
323
+ .find((s) => s.label === memory.sourceLabel);
324
+ if (!sourceStore?.writable) {
325
+ console.error(`Cannot update: store [${memory.sourceLabel}] is read-only.`);
326
+ process.exit(1);
327
+ }
328
+ const updates = {};
329
+ if (opts.title !== undefined)
330
+ updates.title = opts.title;
331
+ if (opts.status !== undefined)
332
+ updates.status = opts.status;
333
+ if (opts.confidence !== undefined)
334
+ updates.confidence = parseFloat(opts.confidence);
335
+ if (opts.relevance !== undefined)
336
+ updates.relevance = opts.relevance;
337
+ if (opts.supersedes !== undefined)
338
+ updates.supersedes = opts.supersedes;
339
+ if (opts.supersededBy !== undefined)
340
+ updates.superseded_by = opts.supersededBy;
341
+ const fullContent = opts.content
342
+ ? `# ${opts.title || memory.frontmatter.title}\n\n${opts.content}`
343
+ : undefined;
344
+ const updated = await sourceStore.store.updateMemory(memory.relativePath, updates, fullContent);
345
+ if (!updated) {
346
+ console.error(`Failed to update: ${memPath}`);
347
+ process.exit(1);
348
+ }
349
+ // Supersession cross-linking
350
+ if (opts.supersedes && updated.frontmatter.id) {
351
+ const allMemories = await resolver.getAllMemories();
352
+ const supersededMemory = allMemories.find((m) => m.frontmatter.id === opts.supersedes);
353
+ if (supersededMemory) {
354
+ const supersededStore = resolver
355
+ .getStores()
356
+ .find((s) => s.label === supersededMemory.sourceLabel);
357
+ if (supersededStore?.writable) {
358
+ await supersededStore.store.updateMemory(supersededMemory.relativePath, { superseded_by: updated.frontmatter.id, status: "superseded" });
359
+ console.log(`Cross-linked: ${supersededMemory.frontmatter.title} marked as superseded.`);
360
+ }
361
+ }
362
+ }
363
+ const changedFields = Object.keys(updates);
364
+ if (opts.content)
365
+ changedFields.push("content");
366
+ console.log(`Memory updated: ${updated.frontmatter.title}`);
367
+ console.log(`Path: ${memory.sourceLabel}:${memory.relativePath}`);
368
+ console.log(`Changed: ${changedFields.join(", ")}`);
369
+ });
370
+ // ─── gnosys reinforce <memoryId> ────────────────────────────────────────
371
+ program
372
+ .command("reinforce <memoryId>")
373
+ .description("Signal whether a memory was useful, not relevant, or outdated")
374
+ .requiredOption("--signal <signal>", "Reinforcement signal (useful|not_relevant|outdated)")
375
+ .option("--context <context>", "Why this signal was given")
376
+ .action(async (memoryId, opts) => {
377
+ const resolver = await getResolver();
378
+ const writeTarget = resolver.getWriteTarget();
379
+ if (!writeTarget) {
380
+ console.error("No writable store found.");
381
+ process.exit(1);
382
+ }
383
+ // Log reinforcement
384
+ const logDir = path.join(writeTarget.store.getStorePath(), ".gnosys");
385
+ await fs.mkdir(logDir, { recursive: true });
386
+ const logPath = path.join(logDir, "reinforcement.log");
387
+ const entry = JSON.stringify({
388
+ memory_id: memoryId,
389
+ signal: opts.signal,
390
+ context: opts.context,
391
+ timestamp: new Date().toISOString(),
392
+ });
393
+ await fs.appendFile(logPath, entry + "\n", "utf-8");
394
+ // If 'useful', update the memory's modified date (reset decay)
395
+ if (opts.signal === "useful") {
396
+ const allMemories = await resolver.getAllMemories();
397
+ const memory = allMemories.find((m) => m.frontmatter.id === memoryId);
398
+ if (memory) {
399
+ const sourceStore = resolver
400
+ .getStores()
401
+ .find((s) => s.label === memory.sourceLabel);
402
+ if (sourceStore?.writable) {
403
+ await sourceStore.store.updateMemory(memory.relativePath, {
404
+ modified: new Date().toISOString().split("T")[0],
405
+ });
406
+ }
407
+ }
408
+ }
409
+ const messages = {
410
+ useful: `Memory ${memoryId} reinforced. Decay clock reset.`,
411
+ not_relevant: `Routing feedback logged for ${memoryId}. Memory unchanged.`,
412
+ outdated: `Memory ${memoryId} flagged for review as outdated.`,
413
+ };
414
+ console.log(messages[opts.signal] || `Signal '${opts.signal}' logged for ${memoryId}.`);
415
+ });
416
+ // ─── gnosys add-structured ──────────────────────────────────────────────
417
+ program
418
+ .command("add-structured")
419
+ .description("Add a memory with structured input (no LLM needed)")
420
+ .requiredOption("--title <title>", "Memory title")
421
+ .requiredOption("--category <category>", "Category directory name")
422
+ .requiredOption("--content <content>", "Memory content as markdown")
423
+ .option("--tags <json>", "Tags as JSON object", "{}")
424
+ .option("--relevance <keywords>", "Keyword cloud for discovery search", "")
425
+ .option("-a, --author <author>", "Author", "human")
426
+ .option("--authority <authority>", "Authority level", "declared")
427
+ .option("--confidence <n>", "Confidence 0-1", "0.8")
428
+ .option("-s, --store <store>", "Target store", undefined)
429
+ .action(async (opts) => {
430
+ const resolver = await getResolver();
431
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
432
+ if (!writeTarget) {
433
+ console.error("No writable store found.");
434
+ process.exit(1);
435
+ }
436
+ let tags;
437
+ try {
438
+ tags = JSON.parse(opts.tags);
439
+ }
440
+ catch {
441
+ console.error("Invalid --tags JSON. Example: '{\"domain\":[\"auth\"],\"type\":[\"decision\"]}'");
442
+ process.exit(1);
443
+ }
444
+ const id = await writeTarget.store.generateId(opts.category);
445
+ const slug = opts.title
446
+ .toLowerCase()
447
+ .replace(/[^a-z0-9]+/g, "-")
448
+ .replace(/^-|-$/g, "")
449
+ .substring(0, 60);
450
+ const today = new Date().toISOString().split("T")[0];
451
+ const frontmatter = {
452
+ id,
453
+ title: opts.title,
454
+ category: opts.category,
455
+ tags,
456
+ relevance: opts.relevance,
457
+ author: opts.author,
458
+ authority: opts.authority,
459
+ confidence: parseFloat(opts.confidence),
460
+ created: today,
461
+ modified: today,
462
+ last_reviewed: today,
463
+ status: "active",
464
+ supersedes: null,
465
+ };
466
+ const content = `# ${opts.title}\n\n${opts.content}`;
467
+ const relPath = await writeTarget.store.writeMemory(opts.category, `${slug}.md`, frontmatter, content);
468
+ console.log(`Memory added to [${writeTarget.label}]: ${opts.title}`);
469
+ console.log(`Path: ${writeTarget.label}:${relPath}`);
470
+ });
471
+ // ─── gnosys tags-add ────────────────────────────────────────────────────
472
+ program
473
+ .command("tags-add")
474
+ .description("Add a new tag to the registry")
475
+ .requiredOption("--category <category>", "Tag category (domain, type, concern, status_tag)")
476
+ .requiredOption("--tag <tag>", "The new tag to add")
477
+ .action(async (opts) => {
478
+ const resolver = await getResolver();
479
+ const writeTarget = resolver.getWriteTarget();
480
+ if (!writeTarget) {
481
+ console.error("No store found.");
482
+ process.exit(1);
483
+ }
484
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
485
+ await tagRegistry.load();
486
+ const added = await tagRegistry.addTag(opts.category, opts.tag);
487
+ if (added) {
488
+ console.log(`Tag '${opts.tag}' added to category '${opts.category}'.`);
489
+ }
490
+ else {
491
+ console.log(`Tag '${opts.tag}' already exists in '${opts.category}'.`);
492
+ }
493
+ });
494
+ // ─── gnosys commit-context <context> ─────────────────────────────────────
495
+ program
496
+ .command("commit-context <context>")
497
+ .description("Pre-compaction sweep: extract atomic memories from a context string, check novelty, commit novel ones")
498
+ .option("--dry-run", "Show what would be committed without writing")
499
+ .option("-s, --store <store>", "Target store (project|personal|global)", undefined)
500
+ .action(async (context, opts) => {
501
+ const resolver = await getResolver();
502
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
503
+ if (!writeTarget) {
504
+ console.error("No writable store found.");
505
+ process.exit(1);
506
+ }
507
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
508
+ await tagRegistry.load();
509
+ const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
510
+ if (!ingestion.isLLMAvailable) {
511
+ console.error("Error: ANTHROPIC_API_KEY not set. commit-context requires an LLM.");
512
+ process.exit(1);
513
+ }
514
+ // Build search index
515
+ const stores = resolver.getStores();
516
+ const search = new GnosysSearch(stores[0].path);
517
+ search.clearIndex();
518
+ for (const s of stores) {
519
+ await search.addStoreMemories(s.store, s.label);
520
+ }
521
+ // Step 1: Extract candidates via LLM
522
+ console.log("Extracting knowledge candidates from context...");
523
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
524
+ const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
525
+ const extractResponse = await client.messages.create({
526
+ model: "claude-haiku-4-5-20251001",
527
+ max_tokens: 4000,
528
+ system: `You extract atomic knowledge items from conversations. Each item should be ONE decision, fact, insight, or observation — not compound.
529
+
530
+ Output a JSON array of objects, each with:
531
+ - summary: One-sentence description of the knowledge
532
+ - type: "decision" | "insight" | "fact" | "observation" | "requirement"
533
+ - search_terms: 3-5 keywords someone would search for to find if this already exists
534
+
535
+ Be selective. Only extract things worth remembering long-term. Skip small talk, debugging steps, and transient details. Focus on decisions made, architecture choices, requirements established, and insights gained.
536
+
537
+ Output ONLY the JSON array, no markdown fences.`,
538
+ messages: [
539
+ {
540
+ role: "user",
541
+ content: `Extract atomic knowledge items from this context:\n\n${context}`,
542
+ },
543
+ ],
544
+ });
545
+ const extractText = extractResponse.content[0].type === "text"
546
+ ? extractResponse.content[0].text
547
+ : "[]";
548
+ let candidates;
549
+ try {
550
+ const jsonMatch = extractText.match(/```json\s*([\s\S]*?)```/) ||
551
+ extractText.match(/```\s*([\s\S]*?)```/) || [null, extractText];
552
+ candidates = JSON.parse(jsonMatch[1] || extractText);
553
+ }
554
+ catch {
555
+ console.error("Failed to extract candidates — LLM output was not valid JSON.");
556
+ search.close();
557
+ process.exit(1);
558
+ }
559
+ if (!Array.isArray(candidates) || candidates.length === 0) {
560
+ console.log("No extractable knowledge found in the provided context.");
561
+ search.close();
562
+ return;
563
+ }
564
+ console.log(`Found ${candidates.length} candidates. Checking novelty...\n`);
565
+ // Step 2: Check novelty and commit
566
+ let added = 0;
567
+ let skipped = 0;
568
+ for (const candidate of candidates) {
569
+ const searchTerms = candidate.search_terms.join(" ");
570
+ const existing = search.discover(searchTerms, 3);
571
+ if (existing.length > 0) {
572
+ console.log(` ⏭ SKIP: "${candidate.summary}"`);
573
+ console.log(` Overlaps with: ${existing[0].title}`);
574
+ skipped++;
575
+ }
576
+ else if (opts.dryRun) {
577
+ console.log(` ➕ WOULD ADD: "${candidate.summary}" [${candidate.type}]`);
578
+ added++;
579
+ }
580
+ else {
581
+ try {
582
+ const result = await ingestion.ingest(candidate.summary);
583
+ const id = await writeTarget.store.generateId(result.category);
584
+ const today = new Date().toISOString().split("T")[0];
585
+ const frontmatter = {
586
+ id,
587
+ title: result.title,
588
+ category: result.category,
589
+ tags: result.tags,
590
+ relevance: result.relevance,
591
+ author: "ai",
592
+ authority: "observed",
593
+ confidence: result.confidence,
594
+ created: today,
595
+ modified: today,
596
+ last_reviewed: today,
597
+ status: "active",
598
+ supersedes: null,
599
+ };
600
+ const content = `# ${result.title}\n\n${result.content}`;
601
+ const relPath = await writeTarget.store.writeMemory(result.category, `${result.filename}.md`, frontmatter, content);
602
+ console.log(` ➕ ADDED: "${result.title}"`);
603
+ console.log(` Path: ${writeTarget.label}:${relPath}`);
604
+ added++;
605
+ }
606
+ catch (err) {
607
+ console.error(` ❌ FAILED: "${candidate.summary}": ${err instanceof Error ? err.message : String(err)}`);
608
+ }
609
+ }
610
+ console.log();
611
+ }
612
+ search.close();
613
+ const mode = opts.dryRun ? "DRY RUN" : "COMMITTED";
614
+ console.log(`\n${mode}: ${candidates.length} candidates, ${added} ${opts.dryRun ? "would be added" : "added"}, ${skipped} duplicates skipped.`);
615
+ });
616
+ // ─── gnosys lens ────────────────────────────────────────────────────────
617
+ program
618
+ .command("lens")
619
+ .description("Filtered view of memories. Combine criteria to focus on what matters.")
620
+ .option("-c, --category <category>", "Filter by category")
621
+ .option("-t, --tag <tags...>", "Filter by tag(s)")
622
+ .option("--match <mode>", "Tag match mode: any (default) or all", "any")
623
+ .option("--status <statuses...>", "Filter by status (active, archived, superseded)")
624
+ .option("--author <authors...>", "Filter by author (human, ai, human+ai)")
625
+ .option("--authority <authorities...>", "Filter by authority (declared, observed, imported, inferred)")
626
+ .option("--min-confidence <n>", "Minimum confidence (0-1)")
627
+ .option("--max-confidence <n>", "Maximum confidence (0-1)")
628
+ .option("--created-after <date>", "Created after ISO date")
629
+ .option("--created-before <date>", "Created before ISO date")
630
+ .option("--modified-after <date>", "Modified after ISO date")
631
+ .option("--modified-before <date>", "Modified before ISO date")
632
+ .option("--or", "Combine filters with OR instead of AND (default: AND)")
633
+ .action(async (opts) => {
634
+ const resolver = await getResolver();
635
+ const allMemories = await resolver.getAllMemories();
636
+ const lens = {};
637
+ if (opts.category)
638
+ lens.category = opts.category;
639
+ if (opts.tag) {
640
+ lens.tags = opts.tag;
641
+ lens.tagMatchMode = opts.match;
642
+ }
643
+ if (opts.status)
644
+ lens.status = opts.status;
645
+ if (opts.author)
646
+ lens.author = opts.author;
647
+ if (opts.authority)
648
+ lens.authority = opts.authority;
649
+ if (opts.minConfidence)
650
+ lens.minConfidence = parseFloat(opts.minConfidence);
651
+ if (opts.maxConfidence)
652
+ lens.maxConfidence = parseFloat(opts.maxConfidence);
653
+ if (opts.createdAfter)
654
+ lens.createdAfter = opts.createdAfter;
655
+ if (opts.createdBefore)
656
+ lens.createdBefore = opts.createdBefore;
657
+ if (opts.modifiedAfter)
658
+ lens.modifiedAfter = opts.modifiedAfter;
659
+ if (opts.modifiedBefore)
660
+ lens.modifiedBefore = opts.modifiedBefore;
661
+ const result = applyLens(allMemories, lens);
662
+ if (result.length === 0) {
663
+ console.log("No memories match the lens filter.");
664
+ return;
665
+ }
666
+ console.log(`${result.length} memories match:\n`);
667
+ for (const m of result) {
668
+ const src = m.sourceLabel || "";
669
+ console.log(` [${m.frontmatter.status}] ${m.frontmatter.title} (${m.frontmatter.confidence})`);
670
+ console.log(` ${src ? src + ":" : ""}${m.relativePath}`);
671
+ console.log();
672
+ }
673
+ });
674
+ // ─── gnosys history <path> ───────────────────────────────────────────────
675
+ program
676
+ .command("history <memoryPath>")
677
+ .description("Show version history for a memory (git-backed)")
678
+ .option("-n, --limit <number>", "Max entries", "20")
679
+ .option("--diff <hash>", "Show diff from this commit to current")
680
+ .action(async (memPath, opts) => {
681
+ const resolver = await getResolver();
682
+ const memory = await resolver.readMemory(memPath);
683
+ if (!memory) {
684
+ console.error(`Memory not found: ${memPath}`);
685
+ process.exit(1);
686
+ }
687
+ const sourceStore = resolver.getStores().find((s) => s.label === memory.sourceLabel);
688
+ if (!sourceStore) {
689
+ console.error("Could not locate source store.");
690
+ process.exit(1);
691
+ }
692
+ if (!hasGitHistory(sourceStore.path)) {
693
+ console.error("No git history available for this store.");
694
+ process.exit(1);
695
+ }
696
+ if (opts.diff) {
697
+ const diff = getFileDiff(sourceStore.path, memory.relativePath, opts.diff, "HEAD");
698
+ if (!diff) {
699
+ console.error("Could not generate diff.");
700
+ process.exit(1);
701
+ }
702
+ console.log(diff);
703
+ return;
704
+ }
705
+ const history = getFileHistory(sourceStore.path, memory.relativePath, parseInt(opts.limit));
706
+ if (history.length === 0) {
707
+ console.log("No history found for this memory.");
708
+ return;
709
+ }
710
+ console.log(`History for ${memory.frontmatter.title}:\n`);
711
+ for (const entry of history) {
712
+ console.log(` ${entry.commitHash.substring(0, 7)} ${entry.date} ${entry.message}`);
713
+ }
714
+ });
715
+ // ─── gnosys rollback <path> <hash> ──────────────────────────────────────
716
+ program
717
+ .command("rollback <memoryPath> <commitHash>")
718
+ .description("Rollback a memory to its state at a specific commit")
719
+ .action(async (memPath, commitHash) => {
720
+ const resolver = await getResolver();
721
+ const memory = await resolver.readMemory(memPath);
722
+ if (!memory) {
723
+ console.error(`Memory not found: ${memPath}`);
724
+ process.exit(1);
725
+ }
726
+ const sourceStore = resolver.getStores().find((s) => s.label === memory.sourceLabel);
727
+ if (!sourceStore?.writable) {
728
+ console.error("Cannot rollback: store is read-only.");
729
+ process.exit(1);
730
+ }
731
+ const success = rollbackToCommit(sourceStore.path, memory.relativePath, commitHash);
732
+ if (success) {
733
+ console.log(`Rolled back ${memory.frontmatter.title} to commit ${commitHash.substring(0, 7)}.`);
734
+ }
735
+ else {
736
+ console.error(`Rollback failed. Check that the commit hash is valid.`);
737
+ process.exit(1);
738
+ }
739
+ });
740
+ // ─── gnosys timeline ────────────────────────────────────────────────────
741
+ program
742
+ .command("timeline")
743
+ .description("Show when memories were created and modified over time")
744
+ .option("-p, --period <period>", "Group by: day, week, month (default), year", "month")
745
+ .action(async (opts) => {
746
+ const resolver = await getResolver();
747
+ const allMemories = await resolver.getAllMemories();
748
+ if (allMemories.length === 0) {
749
+ console.log("No memories found.");
750
+ return;
751
+ }
752
+ const entries = groupByPeriod(allMemories, opts.period);
753
+ console.log(`Knowledge Timeline (by ${opts.period}):\n`);
754
+ for (const entry of entries) {
755
+ const parts = [];
756
+ if (entry.created > 0)
757
+ parts.push(`${entry.created} created`);
758
+ if (entry.modified > 0)
759
+ parts.push(`${entry.modified} modified`);
760
+ console.log(` ${entry.period}: ${parts.join(", ")}`);
761
+ if (entry.titles.length > 0 && entry.titles.length <= 5) {
762
+ for (const t of entry.titles) {
763
+ console.log(` + ${t}`);
764
+ }
765
+ }
766
+ }
767
+ });
768
+ // ─── gnosys stats ───────────────────────────────────────────────────────
769
+ program
770
+ .command("stats")
771
+ .description("Show summary statistics for the memory store")
772
+ .action(async () => {
773
+ const resolver = await getResolver();
774
+ const allMemories = await resolver.getAllMemories();
775
+ if (allMemories.length === 0) {
776
+ console.log("No memories found.");
777
+ return;
778
+ }
779
+ const stats = computeStats(allMemories);
780
+ console.log(`Gnosys Store Statistics:\n`);
781
+ console.log(` Total memories: ${stats.totalCount}`);
782
+ console.log(` Average confidence: ${stats.averageConfidence}`);
783
+ console.log(` Date range: ${stats.oldestCreated} → ${stats.newestCreated}`);
784
+ console.log(` Last modified: ${stats.lastModified}`);
785
+ console.log(`\n By category:`);
786
+ for (const [cat, count] of Object.entries(stats.byCategory).sort((a, b) => b[1] - a[1])) {
787
+ console.log(` ${cat}: ${count}`);
788
+ }
789
+ console.log(`\n By status:`);
790
+ for (const [st, count] of Object.entries(stats.byStatus)) {
791
+ console.log(` ${st}: ${count}`);
792
+ }
793
+ console.log(`\n By author:`);
794
+ for (const [author, count] of Object.entries(stats.byAuthor)) {
795
+ console.log(` ${author}: ${count}`);
796
+ }
797
+ });
798
+ // ─── gnosys links <path> ─────────────────────────────────────────────────
799
+ program
800
+ .command("links <memoryPath>")
801
+ .description("Show wikilinks for a memory — both outgoing [[links]] and backlinks from other memories")
802
+ .action(async (memPath) => {
803
+ const resolver = await getResolver();
804
+ const memory = await resolver.readMemory(memPath);
805
+ if (!memory) {
806
+ console.error(`Memory not found: ${memPath}`);
807
+ process.exit(1);
808
+ }
809
+ const allMemories = await resolver.getAllMemories();
810
+ const outgoing = getOutgoingLinks(allMemories, memory.relativePath);
811
+ const backlinks = getBacklinks(allMemories, memory.relativePath);
812
+ console.log(`Links for ${memory.frontmatter.title}:\n`);
813
+ if (outgoing.length > 0) {
814
+ console.log(` Outgoing (${outgoing.length}):`);
815
+ for (const link of outgoing) {
816
+ const display = link.displayText ? ` (${link.displayText})` : "";
817
+ console.log(` → [[${link.target}]]${display}`);
818
+ }
819
+ }
820
+ else {
821
+ console.log(" No outgoing links.");
822
+ }
823
+ console.log();
824
+ if (backlinks.length > 0) {
825
+ console.log(` Backlinks (${backlinks.length}):`);
826
+ for (const link of backlinks) {
827
+ console.log(` ← ${link.sourceTitle} (${link.sourcePath})`);
828
+ }
829
+ }
830
+ else {
831
+ console.log(" No backlinks.");
832
+ }
833
+ });
834
+ // ─── gnosys graph ───────────────────────────────────────────────────────
835
+ program
836
+ .command("graph")
837
+ .description("Show the full cross-reference graph across all memories")
838
+ .action(async () => {
839
+ const resolver = await getResolver();
840
+ const allMemories = await resolver.getAllMemories();
841
+ if (allMemories.length === 0) {
842
+ console.log("No memories found.");
843
+ return;
844
+ }
845
+ const graph = buildLinkGraph(allMemories);
846
+ console.log(formatGraphSummary(graph));
847
+ });
848
+ // ─── gnosys bootstrap <sourceDir> ────────────────────────────────────────
849
+ program
850
+ .command("bootstrap <sourceDir>")
851
+ .description("Batch-import existing documents into the memory store")
852
+ .option("-p, --pattern <patterns...>", "File patterns to match (default: **/*.md)")
853
+ .option("--skip-existing", "Skip files whose titles already exist in the store")
854
+ .option("-c, --category <category>", "Default category (default: imported)", "imported")
855
+ .option("-a, --author <author>", "Default author", "human")
856
+ .option("--authority <authority>", "Default authority", "imported")
857
+ .option("--confidence <n>", "Default confidence (0-1)", "0.7")
858
+ .option("--preserve-frontmatter", "Preserve existing YAML frontmatter if present")
859
+ .option("--dry-run", "Show what would be imported without writing")
860
+ .option("-s, --store <store>", "Target store (project|personal|global)", undefined)
861
+ .action(async (sourceDir, opts) => {
862
+ const resolver = await getResolver();
863
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
864
+ if (!writeTarget) {
865
+ console.error("No writable store found.");
866
+ process.exit(1);
867
+ }
868
+ // Show what we'll scan
869
+ const files = await discoverFiles(sourceDir, opts.pattern);
870
+ console.log(`Found ${files.length} files in ${sourceDir}\n`);
871
+ if (files.length === 0) {
872
+ console.log("Nothing to import.");
873
+ return;
874
+ }
875
+ const result = await bootstrap(writeTarget.store, {
876
+ sourceDir,
877
+ patterns: opts.pattern,
878
+ skipExisting: opts.skipExisting,
879
+ defaultCategory: opts.category,
880
+ defaultAuthor: opts.author,
881
+ defaultAuthority: opts.authority,
882
+ defaultConfidence: parseFloat(opts.confidence),
883
+ preserveFrontmatter: opts.preserveFrontmatter,
884
+ dryRun: opts.dryRun,
885
+ });
886
+ const mode = opts.dryRun ? "DRY RUN" : "COMPLETE";
887
+ console.log(`\nBootstrap ${mode}:`);
888
+ console.log(` Scanned: ${result.totalScanned}`);
889
+ console.log(` ${opts.dryRun ? "Would import" : "Imported"}: ${result.imported.length}`);
890
+ console.log(` Skipped: ${result.skipped.length}`);
891
+ console.log(` Failed: ${result.failed.length}`);
892
+ if (result.imported.length > 0) {
893
+ console.log(`\n${opts.dryRun ? "Would import" : "Imported"}:`);
894
+ for (const f of result.imported) {
895
+ console.log(` + ${f}`);
896
+ }
897
+ }
898
+ if (result.skipped.length > 0) {
899
+ console.log(`\nSkipped (already exist):`);
900
+ for (const f of result.skipped) {
901
+ console.log(` ⏭ ${f}`);
902
+ }
903
+ }
904
+ if (result.failed.length > 0) {
905
+ console.log(`\nFailed:`);
906
+ for (const f of result.failed) {
907
+ console.log(` ❌ ${f.path}: ${f.error}`);
908
+ }
909
+ }
910
+ });
911
+ // ─── gnosys stores ───────────────────────────────────────────────────────
912
+ program
913
+ .command("stores")
914
+ .description("Show all active stores, their layers, paths, and permissions")
915
+ .action(async () => {
916
+ const resolver = await getResolver();
917
+ console.log(resolver.getSummary());
918
+ });
919
+ // ─── gnosys serve ────────────────────────────────────────────────────────
920
+ program
921
+ .command("serve")
922
+ .description("Start the MCP server (stdio mode)")
923
+ .action(async () => {
924
+ await import("./index.js");
925
+ });
926
+ program.parse();
927
+ //# sourceMappingURL=cli.js.map