gnosys 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1387 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +3753 -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 +2267 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/archive.d.ts +95 -0
  12. package/dist/lib/archive.d.ts.map +1 -0
  13. package/dist/lib/archive.js +311 -0
  14. package/dist/lib/archive.js.map +1 -0
  15. package/dist/lib/ask.d.ts +77 -0
  16. package/dist/lib/ask.d.ts.map +1 -0
  17. package/dist/lib/ask.js +316 -0
  18. package/dist/lib/ask.js.map +1 -0
  19. package/dist/lib/audit.d.ts +47 -0
  20. package/dist/lib/audit.d.ts.map +1 -0
  21. package/dist/lib/audit.js +136 -0
  22. package/dist/lib/audit.js.map +1 -0
  23. package/dist/lib/bootstrap.d.ts +56 -0
  24. package/dist/lib/bootstrap.d.ts.map +1 -0
  25. package/dist/lib/bootstrap.js +163 -0
  26. package/dist/lib/bootstrap.js.map +1 -0
  27. package/dist/lib/config.d.ts +239 -0
  28. package/dist/lib/config.d.ts.map +1 -0
  29. package/dist/lib/config.js +371 -0
  30. package/dist/lib/config.js.map +1 -0
  31. package/dist/lib/dashboard.d.ts +81 -0
  32. package/dist/lib/dashboard.d.ts.map +1 -0
  33. package/dist/lib/dashboard.js +314 -0
  34. package/dist/lib/dashboard.js.map +1 -0
  35. package/dist/lib/db.d.ts +182 -0
  36. package/dist/lib/db.d.ts.map +1 -0
  37. package/dist/lib/db.js +620 -0
  38. package/dist/lib/db.js.map +1 -0
  39. package/dist/lib/dbSearch.d.ts +65 -0
  40. package/dist/lib/dbSearch.d.ts.map +1 -0
  41. package/dist/lib/dbSearch.js +239 -0
  42. package/dist/lib/dbSearch.js.map +1 -0
  43. package/dist/lib/dbWrite.d.ts +56 -0
  44. package/dist/lib/dbWrite.d.ts.map +1 -0
  45. package/dist/lib/dbWrite.js +171 -0
  46. package/dist/lib/dbWrite.js.map +1 -0
  47. package/dist/lib/dream.d.ts +170 -0
  48. package/dist/lib/dream.d.ts.map +1 -0
  49. package/dist/lib/dream.js +706 -0
  50. package/dist/lib/dream.js.map +1 -0
  51. package/dist/lib/embeddings.d.ts +84 -0
  52. package/dist/lib/embeddings.d.ts.map +1 -0
  53. package/dist/lib/embeddings.js +226 -0
  54. package/dist/lib/embeddings.js.map +1 -0
  55. package/dist/lib/export.d.ts +92 -0
  56. package/dist/lib/export.d.ts.map +1 -0
  57. package/dist/lib/export.js +362 -0
  58. package/dist/lib/export.js.map +1 -0
  59. package/dist/lib/federated.d.ts +113 -0
  60. package/dist/lib/federated.d.ts.map +1 -0
  61. package/dist/lib/federated.js +346 -0
  62. package/dist/lib/federated.js.map +1 -0
  63. package/dist/lib/graph.d.ts +50 -0
  64. package/dist/lib/graph.d.ts.map +1 -0
  65. package/dist/lib/graph.js +118 -0
  66. package/dist/lib/graph.js.map +1 -0
  67. package/dist/lib/history.d.ts +39 -0
  68. package/dist/lib/history.d.ts.map +1 -0
  69. package/dist/lib/history.js +112 -0
  70. package/dist/lib/history.js.map +1 -0
  71. package/dist/lib/hybridSearch.d.ts +80 -0
  72. package/dist/lib/hybridSearch.d.ts.map +1 -0
  73. package/dist/lib/hybridSearch.js +296 -0
  74. package/dist/lib/hybridSearch.js.map +1 -0
  75. package/dist/lib/import.d.ts +52 -0
  76. package/dist/lib/import.d.ts.map +1 -0
  77. package/dist/lib/import.js +365 -0
  78. package/dist/lib/import.js.map +1 -0
  79. package/dist/lib/ingest.d.ts +51 -0
  80. package/dist/lib/ingest.d.ts.map +1 -0
  81. package/dist/lib/ingest.js +144 -0
  82. package/dist/lib/ingest.js.map +1 -0
  83. package/dist/lib/lensing.d.ts +35 -0
  84. package/dist/lib/lensing.d.ts.map +1 -0
  85. package/dist/lib/lensing.js +85 -0
  86. package/dist/lib/lensing.js.map +1 -0
  87. package/dist/lib/llm.d.ts +84 -0
  88. package/dist/lib/llm.d.ts.map +1 -0
  89. package/dist/lib/llm.js +386 -0
  90. package/dist/lib/llm.js.map +1 -0
  91. package/dist/lib/lock.d.ts +28 -0
  92. package/dist/lib/lock.d.ts.map +1 -0
  93. package/dist/lib/lock.js +145 -0
  94. package/dist/lib/lock.js.map +1 -0
  95. package/dist/lib/maintenance.d.ts +124 -0
  96. package/dist/lib/maintenance.d.ts.map +1 -0
  97. package/dist/lib/maintenance.js +587 -0
  98. package/dist/lib/maintenance.js.map +1 -0
  99. package/dist/lib/migrate.d.ts +19 -0
  100. package/dist/lib/migrate.d.ts.map +1 -0
  101. package/dist/lib/migrate.js +260 -0
  102. package/dist/lib/migrate.js.map +1 -0
  103. package/dist/lib/preferences.d.ts +49 -0
  104. package/dist/lib/preferences.d.ts.map +1 -0
  105. package/dist/lib/preferences.js +149 -0
  106. package/dist/lib/preferences.js.map +1 -0
  107. package/dist/lib/projectIdentity.d.ts +66 -0
  108. package/dist/lib/projectIdentity.d.ts.map +1 -0
  109. package/dist/lib/projectIdentity.js +148 -0
  110. package/dist/lib/projectIdentity.js.map +1 -0
  111. package/dist/lib/recall.d.ts +82 -0
  112. package/dist/lib/recall.d.ts.map +1 -0
  113. package/dist/lib/recall.js +289 -0
  114. package/dist/lib/recall.js.map +1 -0
  115. package/dist/lib/resolver.d.ts +116 -0
  116. package/dist/lib/resolver.d.ts.map +1 -0
  117. package/dist/lib/resolver.js +372 -0
  118. package/dist/lib/resolver.js.map +1 -0
  119. package/dist/lib/retry.d.ts +24 -0
  120. package/dist/lib/retry.d.ts.map +1 -0
  121. package/dist/lib/retry.js +60 -0
  122. package/dist/lib/retry.js.map +1 -0
  123. package/dist/lib/rulesGen.d.ts +51 -0
  124. package/dist/lib/rulesGen.d.ts.map +1 -0
  125. package/dist/lib/rulesGen.js +167 -0
  126. package/dist/lib/rulesGen.js.map +1 -0
  127. package/dist/lib/search.d.ts +51 -0
  128. package/dist/lib/search.d.ts.map +1 -0
  129. package/dist/lib/search.js +190 -0
  130. package/dist/lib/search.js.map +1 -0
  131. package/dist/lib/staticSearch.d.ts +70 -0
  132. package/dist/lib/staticSearch.d.ts.map +1 -0
  133. package/dist/lib/staticSearch.js +162 -0
  134. package/dist/lib/staticSearch.js.map +1 -0
  135. package/dist/lib/store.d.ts +79 -0
  136. package/dist/lib/store.d.ts.map +1 -0
  137. package/dist/lib/store.js +227 -0
  138. package/dist/lib/store.js.map +1 -0
  139. package/dist/lib/structuredIngest.d.ts +37 -0
  140. package/dist/lib/structuredIngest.d.ts.map +1 -0
  141. package/dist/lib/structuredIngest.js +208 -0
  142. package/dist/lib/structuredIngest.js.map +1 -0
  143. package/dist/lib/tags.d.ts +26 -0
  144. package/dist/lib/tags.d.ts.map +1 -0
  145. package/dist/lib/tags.js +109 -0
  146. package/dist/lib/tags.js.map +1 -0
  147. package/dist/lib/timeline.d.ts +34 -0
  148. package/dist/lib/timeline.d.ts.map +1 -0
  149. package/dist/lib/timeline.js +116 -0
  150. package/dist/lib/timeline.js.map +1 -0
  151. package/dist/lib/trace.d.ts +42 -0
  152. package/dist/lib/trace.d.ts.map +1 -0
  153. package/dist/lib/trace.js +338 -0
  154. package/dist/lib/trace.js.map +1 -0
  155. package/dist/lib/webIndex.d.ts +28 -0
  156. package/dist/lib/webIndex.d.ts.map +1 -0
  157. package/dist/lib/webIndex.js +208 -0
  158. package/dist/lib/webIndex.js.map +1 -0
  159. package/dist/lib/webIngest.d.ts +51 -0
  160. package/dist/lib/webIngest.d.ts.map +1 -0
  161. package/dist/lib/webIngest.js +533 -0
  162. package/dist/lib/webIngest.js.map +1 -0
  163. package/dist/lib/wikilinks.d.ts +63 -0
  164. package/dist/lib/wikilinks.d.ts.map +1 -0
  165. package/dist/lib/wikilinks.js +146 -0
  166. package/dist/lib/wikilinks.js.map +1 -0
  167. package/dist/sandbox/client.d.ts +82 -0
  168. package/dist/sandbox/client.d.ts.map +1 -0
  169. package/dist/sandbox/client.js +128 -0
  170. package/dist/sandbox/client.js.map +1 -0
  171. package/dist/sandbox/helper-template.d.ts +14 -0
  172. package/dist/sandbox/helper-template.d.ts.map +1 -0
  173. package/dist/sandbox/helper-template.js +285 -0
  174. package/dist/sandbox/helper-template.js.map +1 -0
  175. package/dist/sandbox/index.d.ts +10 -0
  176. package/dist/sandbox/index.d.ts.map +1 -0
  177. package/dist/sandbox/index.js +10 -0
  178. package/dist/sandbox/index.js.map +1 -0
  179. package/dist/sandbox/manager.d.ts +40 -0
  180. package/dist/sandbox/manager.d.ts.map +1 -0
  181. package/dist/sandbox/manager.js +220 -0
  182. package/dist/sandbox/manager.js.map +1 -0
  183. package/dist/sandbox/server.d.ts +44 -0
  184. package/dist/sandbox/server.d.ts.map +1 -0
  185. package/dist/sandbox/server.js +661 -0
  186. package/dist/sandbox/server.js.map +1 -0
  187. package/package.json +103 -0
  188. package/prompts/synthesize.md +21 -0
package/dist/cli.js ADDED
@@ -0,0 +1,3753 @@
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 { fileURLToPath } from "url";
10
+ import dotenv from "dotenv";
11
+ import { readFileSync, existsSync, copyFileSync } from "fs";
12
+ import { GnosysResolver } from "./lib/resolver.js";
13
+ import { GnosysSearch } from "./lib/search.js";
14
+ import { GnosysTagRegistry } from "./lib/tags.js";
15
+ import { GnosysIngestion } from "./lib/ingest.js";
16
+ import { applyLens } from "./lib/lensing.js";
17
+ import { getFileHistory, rollbackToCommit, hasGitHistory, getFileDiff } from "./lib/history.js";
18
+ import { groupByPeriod, computeStats } from "./lib/timeline.js";
19
+ import { buildLinkGraph, getBacklinks, getOutgoingLinks, formatGraphSummary } from "./lib/wikilinks.js";
20
+ import { bootstrap, discoverFiles } from "./lib/bootstrap.js";
21
+ import { performImport, formatImportSummary } from "./lib/import.js";
22
+ import { loadConfig, generateConfigTemplate, DEFAULT_CONFIG, writeConfig, resolveTaskModel, ALL_PROVIDERS } from "./lib/config.js";
23
+ import { GnosysEmbeddings } from "./lib/embeddings.js";
24
+ import { GnosysHybridSearch } from "./lib/hybridSearch.js";
25
+ import { GnosysAsk } from "./lib/ask.js";
26
+ import { getLLMProvider, isProviderAvailable } from "./lib/llm.js";
27
+ import { GnosysDB } from "./lib/db.js";
28
+ import { migrate, formatMigrationReport } from "./lib/migrate.js";
29
+ import { createProjectIdentity, readProjectIdentity, findProjectIdentity } from "./lib/projectIdentity.js";
30
+ import { setPreference, getPreference, getAllPreferences, deletePreference } from "./lib/preferences.js";
31
+ import { syncRules } from "./lib/rulesGen.js";
32
+ // Load API keys from ~/.config/gnosys/.env (same as MCP server)
33
+ // IMPORTANT: We use dotenv.parse() instead of dotenv.config() because
34
+ // dotenv v17+ writes injection notices to stdout, which corrupts
35
+ // --json output and piped usage. parse() is a pure function with no side effects.
36
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
37
+ try {
38
+ const envFile = readFileSync(path.join(home, ".config", "gnosys", ".env"), "utf8");
39
+ const parsed = dotenv.parse(envFile);
40
+ for (const [key, val] of Object.entries(parsed)) {
41
+ if (!(key in process.env))
42
+ process.env[key] = val;
43
+ }
44
+ }
45
+ catch {
46
+ // .env file not found — that's fine, env vars may be set elsewhere
47
+ }
48
+ // Also try .env from current directory as fallback
49
+ try {
50
+ const localEnv = readFileSync(".env", "utf8");
51
+ const localParsed = dotenv.parse(localEnv);
52
+ for (const [key, val] of Object.entries(localParsed)) {
53
+ if (!(key in process.env))
54
+ process.env[key] = val;
55
+ }
56
+ }
57
+ catch {
58
+ // No local .env — fine
59
+ }
60
+ // Read version from package.json at build time
61
+ const __filename = fileURLToPath(import.meta.url);
62
+ const __dirname = path.dirname(__filename);
63
+ const pkgPath = path.resolve(__dirname, "..", "package.json");
64
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
65
+ const program = new Command();
66
+ async function getResolver() {
67
+ const resolver = new GnosysResolver();
68
+ await resolver.resolve();
69
+ return resolver;
70
+ }
71
+ /**
72
+ * v3.0: Resolve projectId from nearest .gnosys/gnosys.json.
73
+ * Used by CLI write commands to tag memories with the correct project.
74
+ */
75
+ async function resolveProjectId(dir) {
76
+ const result = await findProjectIdentity(dir || process.cwd());
77
+ return result?.identity.projectId || null;
78
+ }
79
+ /**
80
+ * Output helper: if --json flag is set, output JSON; otherwise call the
81
+ * human-readable formatter function.
82
+ */
83
+ function outputResult(json, data, humanFn) {
84
+ if (json) {
85
+ console.log(JSON.stringify(data, null, 2));
86
+ }
87
+ else {
88
+ humanFn();
89
+ }
90
+ }
91
+ program
92
+ .name("gnosys")
93
+ .description("Gnosys — Persistent memory for AI agents. Sandbox-first runtime, central SQLite brain, federated search, reflection API, process tracing, preferences, Dream Mode, Obsidian export. Also runs as a full MCP server.")
94
+ .version(pkg.version);
95
+ // ─── gnosys read <path> ──────────────────────────────────────────────────
96
+ program
97
+ .command("read <memoryPath>")
98
+ .description("Read a specific memory. Supports layer prefix (e.g., project:decisions/auth.md)")
99
+ .option("--json", "Output as JSON")
100
+ .action(async (memoryPath, opts) => {
101
+ const resolver = await getResolver();
102
+ const memory = await resolver.readMemory(memoryPath);
103
+ if (!memory) {
104
+ console.error(`Memory not found: ${memoryPath}`);
105
+ process.exit(1);
106
+ }
107
+ const raw = await fs.readFile(memory.filePath, "utf-8");
108
+ outputResult(!!opts.json, { path: memoryPath, source: memory.sourceLabel, content: raw }, () => {
109
+ console.log(`[Source: ${memory.sourceLabel}]\n`);
110
+ console.log(raw);
111
+ });
112
+ });
113
+ // ─── gnosys discover <query> ─────────────────────────────────────────────
114
+ program
115
+ .command("discover <query>")
116
+ .description("Discover relevant memories by keyword. Use --federated for tier-boosted cross-scope discovery.")
117
+ .option("-n, --limit <number>", "Max results", "20")
118
+ .option("--json", "Output as JSON")
119
+ .option("--federated", "Use federated discovery with tier boosting (project > user > global)")
120
+ .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated for multiple)")
121
+ .option("-d, --directory <dir>", "Project directory for context")
122
+ .action(async (query, opts) => {
123
+ // Federated discover path
124
+ if (opts.federated || opts.scope) {
125
+ let centralDb = null;
126
+ try {
127
+ centralDb = GnosysDB.openCentral();
128
+ if (!centralDb.isAvailable()) {
129
+ console.error("Central DB not available.");
130
+ process.exit(1);
131
+ }
132
+ const { federatedDiscover, detectCurrentProject } = await import("./lib/federated.js");
133
+ const projectId = await detectCurrentProject(centralDb, opts.directory || undefined);
134
+ const scopeFilter = opts.scope ? opts.scope.split(",").map(s => s.trim()) : undefined;
135
+ const results = federatedDiscover(centralDb, query, {
136
+ limit: parseInt(opts.limit, 10),
137
+ projectId,
138
+ scopeFilter,
139
+ });
140
+ outputResult(!!opts.json, { query, projectId, count: results.length, results }, () => {
141
+ if (results.length === 0) {
142
+ console.log(`No memories found for "${query}".`);
143
+ return;
144
+ }
145
+ for (const [i, r] of results.entries()) {
146
+ const proj = r.projectName ? ` [${r.projectName}]` : "";
147
+ console.log(`${i + 1}. ${r.title} (${r.category})${proj}`);
148
+ console.log(` scope: ${r.scope} | score: ${r.score.toFixed(4)}`);
149
+ }
150
+ });
151
+ }
152
+ catch (err) {
153
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
154
+ process.exit(1);
155
+ }
156
+ finally {
157
+ centralDb?.close();
158
+ }
159
+ return;
160
+ }
161
+ // Legacy file-based discover path
162
+ const resolver = await getResolver();
163
+ const stores = resolver.getStores();
164
+ if (stores.length === 0) {
165
+ console.error("No Gnosys stores found.");
166
+ process.exit(1);
167
+ }
168
+ const search = new GnosysSearch(stores[0].path);
169
+ search.clearIndex();
170
+ for (const s of stores) {
171
+ await search.addStoreMemories(s.store, s.label);
172
+ }
173
+ const results = search.discover(query, parseInt(opts.limit));
174
+ if (results.length === 0) {
175
+ outputResult(!!opts.json, { query, results: [] }, () => {
176
+ console.log(`No memories found for "${query}". Try gnosys search for full-text.`);
177
+ });
178
+ search.close();
179
+ return;
180
+ }
181
+ outputResult(!!opts.json, { query, count: results.length, results }, () => {
182
+ console.log(`Found ${results.length} relevant memories for "${query}":\n`);
183
+ for (const r of results) {
184
+ console.log(` ${r.title}`);
185
+ console.log(` ${r.relative_path}`);
186
+ if (r.relevance)
187
+ console.log(` Relevance: ${r.relevance}`);
188
+ console.log();
189
+ }
190
+ });
191
+ search.close();
192
+ });
193
+ // ─── gnosys search <query> ───────────────────────────────────────────────
194
+ program
195
+ .command("search <query>")
196
+ .description("Search memories by keyword. Use --federated for tier-boosted cross-scope search.")
197
+ .option("-n, --limit <number>", "Max results", "20")
198
+ .option("--json", "Output as JSON")
199
+ .option("--federated", "Use federated search with tier boosting (project > user > global)")
200
+ .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated for multiple)")
201
+ .option("-d, --directory <dir>", "Project directory for context")
202
+ .action(async (query, opts) => {
203
+ // Federated search path — uses central DB with tier boosting
204
+ if (opts.federated || opts.scope) {
205
+ let centralDb = null;
206
+ try {
207
+ centralDb = GnosysDB.openCentral();
208
+ if (!centralDb.isAvailable()) {
209
+ console.error("Central DB not available. Run 'gnosys migrate --to-central' first.");
210
+ process.exit(1);
211
+ }
212
+ const { federatedSearch, detectCurrentProject } = await import("./lib/federated.js");
213
+ const projectId = await detectCurrentProject(centralDb, opts.directory || undefined);
214
+ const scopeFilter = opts.scope ? opts.scope.split(",").map(s => s.trim()) : undefined;
215
+ const results = federatedSearch(centralDb, query, {
216
+ limit: parseInt(opts.limit, 10),
217
+ projectId,
218
+ scopeFilter,
219
+ });
220
+ outputResult(!!opts.json, { query, projectId, count: results.length, results }, () => {
221
+ if (results.length === 0) {
222
+ console.log(`No results for "${query}".`);
223
+ return;
224
+ }
225
+ const ctx = projectId ? `Context: project ${projectId}` : "No project detected";
226
+ console.log(ctx);
227
+ for (const [i, r] of results.entries()) {
228
+ const proj = r.projectName ? ` [${r.projectName}]` : "";
229
+ console.log(`\n${i + 1}. ${r.title} (${r.category})${proj}`);
230
+ console.log(` scope: ${r.scope} | score: ${r.score.toFixed(4)} | boosts: ${r.boosts.join(", ")}`);
231
+ if (r.snippet)
232
+ console.log(` ${r.snippet.substring(0, 120)}`);
233
+ }
234
+ });
235
+ }
236
+ catch (err) {
237
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
238
+ process.exit(1);
239
+ }
240
+ finally {
241
+ centralDb?.close();
242
+ }
243
+ return;
244
+ }
245
+ // Legacy file-based search path
246
+ const resolver = await getResolver();
247
+ const stores = resolver.getStores();
248
+ if (stores.length === 0) {
249
+ console.error("No Gnosys stores found.");
250
+ process.exit(1);
251
+ }
252
+ const search = new GnosysSearch(stores[0].path);
253
+ search.clearIndex();
254
+ for (const s of stores) {
255
+ await search.addStoreMemories(s.store, s.label);
256
+ }
257
+ const results = search.search(query, parseInt(opts.limit));
258
+ if (results.length === 0) {
259
+ outputResult(!!opts.json, { query, results: [] }, () => {
260
+ console.log(`No results for "${query}".`);
261
+ });
262
+ search.close();
263
+ return;
264
+ }
265
+ outputResult(!!opts.json, { query, count: results.length, results }, () => {
266
+ console.log(`Found ${results.length} results for "${query}":\n`);
267
+ for (const r of results) {
268
+ console.log(` ${r.title}`);
269
+ console.log(` ${r.relative_path}`);
270
+ console.log(` ${r.snippet.replace(/>>>/g, "").replace(/<<</g, "")}`);
271
+ console.log();
272
+ }
273
+ });
274
+ search.close();
275
+ });
276
+ // ─── gnosys list ─────────────────────────────────────────────────────────
277
+ program
278
+ .command("list")
279
+ .description("List all memories across all stores")
280
+ .option("-c, --category <category>", "Filter by category")
281
+ .option("-t, --tag <tag>", "Filter by tag")
282
+ .option("-s, --store <store>", "Filter by store layer")
283
+ .option("--json", "Output as JSON")
284
+ .action(async (opts) => {
285
+ const resolver = await getResolver();
286
+ let memories = await resolver.getAllMemories();
287
+ if (opts.store) {
288
+ memories = memories.filter((m) => m.sourceLayer === opts.store || m.sourceLabel === opts.store);
289
+ }
290
+ if (opts.category) {
291
+ memories = memories.filter((m) => m.frontmatter.category === opts.category);
292
+ }
293
+ if (opts.tag) {
294
+ memories = memories.filter((m) => {
295
+ const tags = Array.isArray(m.frontmatter.tags)
296
+ ? m.frontmatter.tags
297
+ : Object.values(m.frontmatter.tags).flat();
298
+ return tags.includes(opts.tag);
299
+ });
300
+ }
301
+ outputResult(!!opts.json, {
302
+ count: memories.length,
303
+ memories: memories.map((m) => ({
304
+ id: m.frontmatter.id,
305
+ title: m.frontmatter.title,
306
+ category: m.frontmatter.category,
307
+ status: m.frontmatter.status,
308
+ source: m.sourceLabel,
309
+ path: `${m.sourceLabel}:${m.relativePath}`,
310
+ })),
311
+ }, () => {
312
+ console.log(`${memories.length} memories:\n`);
313
+ for (const m of memories) {
314
+ console.log(` [${m.sourceLabel}] [${m.frontmatter.status}] ${m.frontmatter.title}`);
315
+ console.log(` ${m.sourceLabel}:${m.relativePath}`);
316
+ console.log();
317
+ }
318
+ });
319
+ });
320
+ // ─── gnosys add <input> ──────────────────────────────────────────────────
321
+ program
322
+ .command("add <input>")
323
+ .description("Add a new memory (uses LLM to structure raw input)")
324
+ .option("-a, --author <author>", "Author (human|ai|human+ai)", "human")
325
+ .option("--authority <authority>", "Authority level (declared|observed|imported|inferred)", "declared")
326
+ .option("-s, --store <store>", "Target store (project|personal|global)", undefined)
327
+ .action(async (input, opts) => {
328
+ const resolver = await getResolver();
329
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
330
+ if (!writeTarget) {
331
+ console.error("No writable store found. Create a .gnosys/ directory or set GNOSYS_PERSONAL.");
332
+ process.exit(1);
333
+ }
334
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
335
+ await tagRegistry.load();
336
+ const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
337
+ if (!ingestion.isLLMAvailable) {
338
+ console.error("Error: No LLM provider available. Set ANTHROPIC_API_KEY or switch to Ollama: gnosys config set provider ollama");
339
+ process.exit(1);
340
+ }
341
+ console.log("Structuring memory via LLM...");
342
+ const result = await ingestion.ingest(input);
343
+ const id = await writeTarget.store.generateId(result.category);
344
+ const today = new Date().toISOString().split("T")[0];
345
+ const frontmatter = {
346
+ id,
347
+ title: result.title,
348
+ category: result.category,
349
+ tags: result.tags,
350
+ relevance: result.relevance,
351
+ author: opts.author,
352
+ authority: opts.authority,
353
+ confidence: result.confidence,
354
+ created: today,
355
+ modified: today,
356
+ last_reviewed: today,
357
+ status: "active",
358
+ supersedes: null,
359
+ };
360
+ const content = `# ${result.title}\n\n${result.content}`;
361
+ const relPath = await writeTarget.store.writeMemory(result.category, `${result.filename}.md`, frontmatter, content);
362
+ console.log(`\nMemory added to [${writeTarget.label}]: ${result.title}`);
363
+ console.log(`Path: ${writeTarget.label}:${relPath}`);
364
+ console.log(`Category: ${result.category}`);
365
+ console.log(`Confidence: ${result.confidence}`);
366
+ if (result.proposedNewTags && result.proposedNewTags.length > 0) {
367
+ console.log("\nProposed new tags (not yet in registry):");
368
+ for (const t of result.proposedNewTags) {
369
+ console.log(` ${t.category}:${t.tag}`);
370
+ }
371
+ }
372
+ });
373
+ // ─── gnosys init ─────────────────────────────────────────────────────────
374
+ program
375
+ .command("init")
376
+ .description("Initialize Gnosys in the current directory. Creates project identity, registers in central DB, and sets up store.")
377
+ .option("-d, --directory <dir>", "Target directory (default: cwd)")
378
+ .option("-n, --name <name>", "Project name (default: directory basename)")
379
+ .action(async (opts) => {
380
+ const targetDir = opts.directory
381
+ ? path.resolve(opts.directory)
382
+ : process.cwd();
383
+ const storePath = path.join(targetDir, ".gnosys");
384
+ // Check if already exists — re-sync identity instead of failing
385
+ let isResync = false;
386
+ try {
387
+ await fs.stat(storePath);
388
+ isResync = true;
389
+ }
390
+ catch {
391
+ // Good — fresh init
392
+ }
393
+ if (!isResync) {
394
+ await fs.mkdir(storePath, { recursive: true });
395
+ await fs.mkdir(path.join(storePath, ".config"), { recursive: true });
396
+ const defaultRegistry = {
397
+ domain: [
398
+ "architecture", "api", "auth", "database", "devops",
399
+ "frontend", "backend", "testing", "security", "performance",
400
+ ],
401
+ type: [
402
+ "decision", "concept", "convention", "requirement",
403
+ "observation", "fact", "question",
404
+ ],
405
+ concern: ["dx", "scalability", "maintainability", "reliability"],
406
+ status_tag: ["draft", "stable", "deprecated", "experimental"],
407
+ };
408
+ await fs.writeFile(path.join(storePath, ".config", "tags.json"), JSON.stringify(defaultRegistry, null, 2), "utf-8");
409
+ // Write default gnosys.json config (LLM settings)
410
+ await fs.writeFile(path.join(storePath, ".config", "gnosys-config.json"), generateConfigTemplate() + "\n", "utf-8");
411
+ const changelog = `# Gnosys Changelog\n\n## ${new Date().toISOString().split("T")[0]}\n\n- Store initialized\n`;
412
+ await fs.writeFile(path.join(storePath, "CHANGELOG.md"), changelog, "utf-8");
413
+ try {
414
+ const { execSync } = await import("child_process");
415
+ execSync("git init", { cwd: storePath, stdio: "pipe" });
416
+ try {
417
+ execSync("git config user.name", { cwd: storePath, stdio: "pipe" });
418
+ }
419
+ catch {
420
+ execSync('git config user.name "Gnosys"', { cwd: storePath, stdio: "pipe" });
421
+ execSync('git config user.email "gnosys@local"', { cwd: storePath, stdio: "pipe" });
422
+ }
423
+ execSync("git add -A && git add -f .config/", { cwd: storePath, stdio: "pipe" });
424
+ execSync('git commit -m "Initialize Gnosys store"', {
425
+ cwd: storePath,
426
+ stdio: "pipe",
427
+ });
428
+ }
429
+ catch {
430
+ // Git not available
431
+ }
432
+ }
433
+ // v3.0: Create/update project identity and register in central DB
434
+ let centralDb = null;
435
+ try {
436
+ centralDb = GnosysDB.openCentral();
437
+ if (!centralDb.isAvailable())
438
+ centralDb = null;
439
+ }
440
+ catch {
441
+ centralDb = null;
442
+ }
443
+ const identity = await createProjectIdentity(targetDir, {
444
+ projectName: opts.name,
445
+ centralDb: centralDb || undefined,
446
+ });
447
+ if (centralDb)
448
+ centralDb.close();
449
+ const action = isResync ? "re-synced" : "initialized";
450
+ console.log(`Gnosys store ${action} at ${storePath}`);
451
+ console.log(`\nProject Identity:`);
452
+ console.log(` ID: ${identity.projectId}`);
453
+ console.log(` Name: ${identity.projectName}`);
454
+ console.log(` Directory: ${identity.workingDirectory}`);
455
+ console.log(` Agent: ${identity.agentRulesTarget || "none detected"}`);
456
+ console.log(` Central DB: ${centralDb ? "registered ✓" : "not available"}`);
457
+ if (!isResync) {
458
+ console.log(`\nCreated:`);
459
+ console.log(` gnosys.json (project identity)`);
460
+ console.log(` .config/ (internal config)`);
461
+ console.log(` tags.json (tag registry)`);
462
+ console.log(` CHANGELOG.md`);
463
+ console.log(` git repo`);
464
+ }
465
+ console.log(`\nStart adding memories with: gnosys add "your knowledge here"`);
466
+ });
467
+ // ─── gnosys stale ───────────────────────────────────────────────────────
468
+ program
469
+ .command("stale")
470
+ .description("Find memories not modified within a given number of days")
471
+ .option("-d, --days <number>", "Days threshold", "90")
472
+ .option("-n, --limit <number>", "Max results", "20")
473
+ .action(async (opts) => {
474
+ const resolver = await getResolver();
475
+ const threshold = parseInt(opts.days);
476
+ const cutoff = new Date();
477
+ cutoff.setDate(cutoff.getDate() - threshold);
478
+ const cutoffStr = cutoff.toISOString().split("T")[0];
479
+ const allMemories = await resolver.getAllMemories();
480
+ const stale = allMemories
481
+ .filter((m) => {
482
+ const lastTouched = m.frontmatter.last_reviewed || m.frontmatter.modified;
483
+ return lastTouched && lastTouched < cutoffStr;
484
+ })
485
+ .sort((a, b) => {
486
+ const aDate = a.frontmatter.last_reviewed || a.frontmatter.modified;
487
+ const bDate = b.frontmatter.last_reviewed || b.frontmatter.modified;
488
+ return (aDate || "").localeCompare(bDate || "");
489
+ })
490
+ .slice(0, parseInt(opts.limit));
491
+ if (stale.length === 0) {
492
+ console.log(`No memories older than ${threshold} days.`);
493
+ return;
494
+ }
495
+ console.log(`${stale.length} memories not touched in ${threshold}+ days:\n`);
496
+ for (const m of stale) {
497
+ const lr = m.frontmatter.last_reviewed;
498
+ console.log(` ${m.frontmatter.title}`);
499
+ console.log(` ${m.sourceLabel}:${m.relativePath}`);
500
+ console.log(` Modified: ${m.frontmatter.modified}${lr ? `, Reviewed: ${lr}` : ""}`);
501
+ console.log();
502
+ }
503
+ });
504
+ // ─── gnosys tags ─────────────────────────────────────────────────────────
505
+ program
506
+ .command("tags")
507
+ .description("List all tags in the registry")
508
+ .action(async () => {
509
+ const resolver = await getResolver();
510
+ const writeTarget = resolver.getWriteTarget();
511
+ if (!writeTarget) {
512
+ console.error("No store found.");
513
+ process.exit(1);
514
+ }
515
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
516
+ await tagRegistry.load();
517
+ const registry = tagRegistry.getRegistry();
518
+ for (const [category, tags] of Object.entries(registry)) {
519
+ console.log(`\n${category}:`);
520
+ console.log(` ${tags.sort().join(", ")}`);
521
+ }
522
+ });
523
+ // ─── gnosys update <path> ────────────────────────────────────────────────
524
+ program
525
+ .command("update <memoryPath>")
526
+ .description("Update an existing memory's frontmatter and/or content")
527
+ .option("--title <title>", "New title")
528
+ .option("--status <status>", "New status (active|archived|superseded)")
529
+ .option("--confidence <n>", "New confidence (0-1)")
530
+ .option("--relevance <keywords>", "Updated relevance keyword cloud")
531
+ .option("--supersedes <id>", "ID of memory this supersedes")
532
+ .option("--superseded-by <id>", "ID of memory that supersedes this one")
533
+ .option("--content <content>", "New markdown content (replaces body)")
534
+ .action(async (memPath, opts) => {
535
+ const resolver = await getResolver();
536
+ const memory = await resolver.readMemory(memPath);
537
+ if (!memory) {
538
+ console.error(`Memory not found: ${memPath}`);
539
+ process.exit(1);
540
+ }
541
+ const sourceStore = resolver
542
+ .getStores()
543
+ .find((s) => s.label === memory.sourceLabel);
544
+ if (!sourceStore?.writable) {
545
+ console.error(`Cannot update: store [${memory.sourceLabel}] is read-only.`);
546
+ process.exit(1);
547
+ }
548
+ const updates = {};
549
+ if (opts.title !== undefined)
550
+ updates.title = opts.title;
551
+ if (opts.status !== undefined)
552
+ updates.status = opts.status;
553
+ if (opts.confidence !== undefined)
554
+ updates.confidence = parseFloat(opts.confidence);
555
+ if (opts.relevance !== undefined)
556
+ updates.relevance = opts.relevance;
557
+ if (opts.supersedes !== undefined)
558
+ updates.supersedes = opts.supersedes;
559
+ if (opts.supersededBy !== undefined)
560
+ updates.superseded_by = opts.supersededBy;
561
+ const fullContent = opts.content
562
+ ? `# ${opts.title || memory.frontmatter.title}\n\n${opts.content}`
563
+ : undefined;
564
+ const updated = await sourceStore.store.updateMemory(memory.relativePath, updates, fullContent);
565
+ if (!updated) {
566
+ console.error(`Failed to update: ${memPath}`);
567
+ process.exit(1);
568
+ }
569
+ // Supersession cross-linking
570
+ if (opts.supersedes && updated.frontmatter.id) {
571
+ const allMemories = await resolver.getAllMemories();
572
+ const supersededMemory = allMemories.find((m) => m.frontmatter.id === opts.supersedes);
573
+ if (supersededMemory) {
574
+ const supersededStore = resolver
575
+ .getStores()
576
+ .find((s) => s.label === supersededMemory.sourceLabel);
577
+ if (supersededStore?.writable) {
578
+ await supersededStore.store.updateMemory(supersededMemory.relativePath, { superseded_by: updated.frontmatter.id, status: "superseded" });
579
+ console.log(`Cross-linked: ${supersededMemory.frontmatter.title} marked as superseded.`);
580
+ }
581
+ }
582
+ }
583
+ const changedFields = Object.keys(updates);
584
+ if (opts.content)
585
+ changedFields.push("content");
586
+ console.log(`Memory updated: ${updated.frontmatter.title}`);
587
+ console.log(`Path: ${memory.sourceLabel}:${memory.relativePath}`);
588
+ console.log(`Changed: ${changedFields.join(", ")}`);
589
+ });
590
+ // ─── gnosys reinforce <memoryId> ────────────────────────────────────────
591
+ program
592
+ .command("reinforce <memoryId>")
593
+ .description("Signal whether a memory was useful, not relevant, or outdated")
594
+ .requiredOption("--signal <signal>", "Reinforcement signal (useful|not_relevant|outdated)")
595
+ .option("--context <context>", "Why this signal was given")
596
+ .action(async (memoryId, opts) => {
597
+ const resolver = await getResolver();
598
+ const writeTarget = resolver.getWriteTarget();
599
+ if (!writeTarget) {
600
+ console.error("No writable store found.");
601
+ process.exit(1);
602
+ }
603
+ // Log reinforcement
604
+ const logDir = path.join(writeTarget.store.getStorePath(), ".config");
605
+ await fs.mkdir(logDir, { recursive: true });
606
+ const logPath = path.join(logDir, "reinforcement.log");
607
+ const entry = JSON.stringify({
608
+ memory_id: memoryId,
609
+ signal: opts.signal,
610
+ context: opts.context,
611
+ timestamp: new Date().toISOString(),
612
+ });
613
+ await fs.appendFile(logPath, entry + "\n", "utf-8");
614
+ // If 'useful', update the memory's modified date (reset decay)
615
+ if (opts.signal === "useful") {
616
+ const allMemories = await resolver.getAllMemories();
617
+ const memory = allMemories.find((m) => m.frontmatter.id === memoryId);
618
+ if (memory) {
619
+ const sourceStore = resolver
620
+ .getStores()
621
+ .find((s) => s.label === memory.sourceLabel);
622
+ if (sourceStore?.writable) {
623
+ await sourceStore.store.updateMemory(memory.relativePath, {
624
+ modified: new Date().toISOString().split("T")[0],
625
+ });
626
+ }
627
+ }
628
+ }
629
+ const messages = {
630
+ useful: `Memory ${memoryId} reinforced. Decay clock reset.`,
631
+ not_relevant: `Routing feedback logged for ${memoryId}. Memory unchanged.`,
632
+ outdated: `Memory ${memoryId} flagged for review as outdated.`,
633
+ };
634
+ console.log(messages[opts.signal] || `Signal '${opts.signal}' logged for ${memoryId}.`);
635
+ });
636
+ // ─── gnosys add-structured ──────────────────────────────────────────────
637
+ program
638
+ .command("add-structured")
639
+ .description("Add a memory with structured input (no LLM needed)")
640
+ .requiredOption("--title <title>", "Memory title")
641
+ .requiredOption("--category <category>", "Category directory name")
642
+ .requiredOption("--content <content>", "Memory content as markdown")
643
+ .option("--tags <json>", "Tags as JSON object", "{}")
644
+ .option("--relevance <keywords>", "Keyword cloud for discovery search", "")
645
+ .option("-a, --author <author>", "Author", "human")
646
+ .option("--authority <authority>", "Authority level", "declared")
647
+ .option("--confidence <n>", "Confidence 0-1", "0.8")
648
+ .option("-s, --store <store>", "Target store", undefined)
649
+ .option("--user", "Store as user-scoped memory (scope: user)")
650
+ .option("--global", "Store as global-scoped memory (scope: global)")
651
+ .action(async (opts) => {
652
+ // ─── Phase 9b: --user / --global route through central DB ─────
653
+ if (opts.user || opts.global) {
654
+ let centralDb = null;
655
+ try {
656
+ centralDb = GnosysDB.openCentral();
657
+ if (!centralDb.isAvailable()) {
658
+ console.error("Central DB not available.");
659
+ process.exit(1);
660
+ }
661
+ const scope = opts.global ? "global" : "user";
662
+ const now = new Date().toISOString();
663
+ const id = `mem-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
664
+ const projectId = opts.global ? null : await resolveProjectId();
665
+ centralDb.insertMemory({
666
+ id,
667
+ title: opts.title,
668
+ category: opts.category,
669
+ content: `# ${opts.title}\n\n${opts.content}`,
670
+ summary: null,
671
+ tags: opts.tags,
672
+ relevance: opts.relevance || opts.content.slice(0, 200),
673
+ author: opts.author,
674
+ authority: opts.authority,
675
+ confidence: parseFloat(opts.confidence),
676
+ reinforcement_count: 0,
677
+ content_hash: "",
678
+ status: "active",
679
+ tier: "active",
680
+ supersedes: null,
681
+ superseded_by: null,
682
+ last_reinforced: null,
683
+ created: now,
684
+ modified: now,
685
+ embedding: null,
686
+ source_path: null,
687
+ project_id: projectId,
688
+ scope,
689
+ });
690
+ console.log(`Memory added (scope: ${scope}): ${opts.title}`);
691
+ console.log(`ID: ${id}`);
692
+ return;
693
+ }
694
+ catch (err) {
695
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
696
+ process.exit(1);
697
+ }
698
+ finally {
699
+ centralDb?.close();
700
+ }
701
+ }
702
+ // ─── Default: file-based store (original behavior) ────────────
703
+ const resolver = await getResolver();
704
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
705
+ if (!writeTarget) {
706
+ console.error("No writable store found.");
707
+ process.exit(1);
708
+ }
709
+ let tags;
710
+ try {
711
+ tags = JSON.parse(opts.tags);
712
+ }
713
+ catch {
714
+ console.error("Invalid --tags JSON. Example: '{\"domain\":[\"auth\"],\"type\":[\"decision\"]}'");
715
+ process.exit(1);
716
+ }
717
+ const id = await writeTarget.store.generateId(opts.category);
718
+ const slug = opts.title
719
+ .toLowerCase()
720
+ .replace(/[^a-z0-9]+/g, "-")
721
+ .replace(/^-|-$/g, "")
722
+ .substring(0, 60);
723
+ const today = new Date().toISOString().split("T")[0];
724
+ const frontmatter = {
725
+ id,
726
+ title: opts.title,
727
+ category: opts.category,
728
+ tags,
729
+ relevance: opts.relevance,
730
+ author: opts.author,
731
+ authority: opts.authority,
732
+ confidence: parseFloat(opts.confidence),
733
+ created: today,
734
+ modified: today,
735
+ last_reviewed: today,
736
+ status: "active",
737
+ supersedes: null,
738
+ };
739
+ const content = `# ${opts.title}\n\n${opts.content}`;
740
+ const relPath = await writeTarget.store.writeMemory(opts.category, `${slug}.md`, frontmatter, content);
741
+ console.log(`Memory added to [${writeTarget.label}]: ${opts.title}`);
742
+ console.log(`Path: ${writeTarget.label}:${relPath}`);
743
+ });
744
+ // ─── gnosys tags-add ────────────────────────────────────────────────────
745
+ program
746
+ .command("tags-add")
747
+ .description("Add a new tag to the registry")
748
+ .requiredOption("--category <category>", "Tag category (domain, type, concern, status_tag)")
749
+ .requiredOption("--tag <tag>", "The new tag to add")
750
+ .action(async (opts) => {
751
+ const resolver = await getResolver();
752
+ const writeTarget = resolver.getWriteTarget();
753
+ if (!writeTarget) {
754
+ console.error("No store found.");
755
+ process.exit(1);
756
+ }
757
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
758
+ await tagRegistry.load();
759
+ const added = await tagRegistry.addTag(opts.category, opts.tag);
760
+ if (added) {
761
+ console.log(`Tag '${opts.tag}' added to category '${opts.category}'.`);
762
+ }
763
+ else {
764
+ console.log(`Tag '${opts.tag}' already exists in '${opts.category}'.`);
765
+ }
766
+ });
767
+ // ─── gnosys commit-context <context> ─────────────────────────────────────
768
+ program
769
+ .command("commit-context <context>")
770
+ .description("Pre-compaction sweep: extract atomic memories from a context string, check novelty, commit novel ones")
771
+ .option("--dry-run", "Show what would be committed without writing")
772
+ .option("-s, --store <store>", "Target store (project|personal|global)", undefined)
773
+ .action(async (context, opts) => {
774
+ const resolver = await getResolver();
775
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
776
+ if (!writeTarget) {
777
+ console.error("No writable store found.");
778
+ process.exit(1);
779
+ }
780
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
781
+ await tagRegistry.load();
782
+ const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
783
+ if (!ingestion.isLLMAvailable) {
784
+ console.error("Error: No LLM provider available. Set ANTHROPIC_API_KEY or switch to Ollama: gnosys config set provider ollama");
785
+ process.exit(1);
786
+ }
787
+ // Build search index
788
+ const stores = resolver.getStores();
789
+ const search = new GnosysSearch(stores[0].path);
790
+ search.clearIndex();
791
+ for (const s of stores) {
792
+ await search.addStoreMemories(s.store, s.label);
793
+ }
794
+ // Step 1: Extract candidates via LLM abstraction
795
+ console.log("Extracting knowledge candidates from context...");
796
+ // Load config for the write target store
797
+ const ccConfig = await loadConfig(writeTarget.store.getStorePath());
798
+ let extractProvider;
799
+ try {
800
+ extractProvider = getLLMProvider(ccConfig, "structuring");
801
+ }
802
+ catch (err) {
803
+ console.error(`LLM not available: ${err instanceof Error ? err.message : String(err)}`);
804
+ search.close();
805
+ process.exit(1);
806
+ }
807
+ const extractText = await extractProvider.generate(`Extract atomic knowledge items from this context:\n\n${context}`, {
808
+ system: `You extract atomic knowledge items from conversations. Each item should be ONE decision, fact, insight, or observation — not compound.
809
+
810
+ Output a JSON array of objects, each with:
811
+ - summary: One-sentence description of the knowledge
812
+ - type: "decision" | "insight" | "fact" | "observation" | "requirement"
813
+ - search_terms: 3-5 keywords someone would search for to find if this already exists
814
+
815
+ 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.
816
+
817
+ Output ONLY the JSON array, no markdown fences.`,
818
+ maxTokens: 4000,
819
+ });
820
+ let candidates;
821
+ try {
822
+ const jsonMatch = extractText.match(/```json\s*([\s\S]*?)```/) ||
823
+ extractText.match(/```\s*([\s\S]*?)```/) || [null, extractText];
824
+ candidates = JSON.parse(jsonMatch[1] || extractText);
825
+ }
826
+ catch {
827
+ console.error("Failed to extract candidates — LLM output was not valid JSON.");
828
+ search.close();
829
+ process.exit(1);
830
+ }
831
+ if (!Array.isArray(candidates) || candidates.length === 0) {
832
+ console.log("No extractable knowledge found in the provided context.");
833
+ search.close();
834
+ return;
835
+ }
836
+ console.log(`Found ${candidates.length} candidates. Checking novelty...\n`);
837
+ // Step 2: Check novelty and commit
838
+ let added = 0;
839
+ let skipped = 0;
840
+ for (const candidate of candidates) {
841
+ const searchTerms = candidate.search_terms.join(" ");
842
+ const existing = search.discover(searchTerms, 3);
843
+ if (existing.length > 0) {
844
+ console.log(` ⏭ SKIP: "${candidate.summary}"`);
845
+ console.log(` Overlaps with: ${existing[0].title}`);
846
+ skipped++;
847
+ }
848
+ else if (opts.dryRun) {
849
+ console.log(` ➕ WOULD ADD: "${candidate.summary}" [${candidate.type}]`);
850
+ added++;
851
+ }
852
+ else {
853
+ try {
854
+ const result = await ingestion.ingest(candidate.summary);
855
+ const id = await writeTarget.store.generateId(result.category);
856
+ const today = new Date().toISOString().split("T")[0];
857
+ const frontmatter = {
858
+ id,
859
+ title: result.title,
860
+ category: result.category,
861
+ tags: result.tags,
862
+ relevance: result.relevance,
863
+ author: "ai",
864
+ authority: "observed",
865
+ confidence: result.confidence,
866
+ created: today,
867
+ modified: today,
868
+ last_reviewed: today,
869
+ status: "active",
870
+ supersedes: null,
871
+ };
872
+ const content = `# ${result.title}\n\n${result.content}`;
873
+ const relPath = await writeTarget.store.writeMemory(result.category, `${result.filename}.md`, frontmatter, content);
874
+ console.log(` ➕ ADDED: "${result.title}"`);
875
+ console.log(` Path: ${writeTarget.label}:${relPath}`);
876
+ added++;
877
+ }
878
+ catch (err) {
879
+ console.error(` ❌ FAILED: "${candidate.summary}": ${err instanceof Error ? err.message : String(err)}`);
880
+ }
881
+ }
882
+ console.log();
883
+ }
884
+ search.close();
885
+ const mode = opts.dryRun ? "DRY RUN" : "COMMITTED";
886
+ console.log(`\n${mode}: ${candidates.length} candidates, ${added} ${opts.dryRun ? "would be added" : "added"}, ${skipped} duplicates skipped.`);
887
+ });
888
+ // ─── gnosys lens ────────────────────────────────────────────────────────
889
+ program
890
+ .command("lens")
891
+ .description("Filtered view of memories. Combine criteria to focus on what matters.")
892
+ .option("-c, --category <category>", "Filter by category")
893
+ .option("-t, --tag <tags...>", "Filter by tag(s)")
894
+ .option("--match <mode>", "Tag match mode: any (default) or all", "any")
895
+ .option("--status <statuses...>", "Filter by status (active, archived, superseded)")
896
+ .option("--author <authors...>", "Filter by author (human, ai, human+ai)")
897
+ .option("--authority <authorities...>", "Filter by authority (declared, observed, imported, inferred)")
898
+ .option("--min-confidence <n>", "Minimum confidence (0-1)")
899
+ .option("--max-confidence <n>", "Maximum confidence (0-1)")
900
+ .option("--created-after <date>", "Created after ISO date")
901
+ .option("--created-before <date>", "Created before ISO date")
902
+ .option("--modified-after <date>", "Modified after ISO date")
903
+ .option("--modified-before <date>", "Modified before ISO date")
904
+ .option("--or", "Combine filters with OR instead of AND (default: AND)")
905
+ .action(async (opts) => {
906
+ const resolver = await getResolver();
907
+ const allMemories = await resolver.getAllMemories();
908
+ const lens = {};
909
+ if (opts.category)
910
+ lens.category = opts.category;
911
+ if (opts.tag) {
912
+ lens.tags = opts.tag;
913
+ lens.tagMatchMode = opts.match;
914
+ }
915
+ if (opts.status)
916
+ lens.status = opts.status;
917
+ if (opts.author)
918
+ lens.author = opts.author;
919
+ if (opts.authority)
920
+ lens.authority = opts.authority;
921
+ if (opts.minConfidence)
922
+ lens.minConfidence = parseFloat(opts.minConfidence);
923
+ if (opts.maxConfidence)
924
+ lens.maxConfidence = parseFloat(opts.maxConfidence);
925
+ if (opts.createdAfter)
926
+ lens.createdAfter = opts.createdAfter;
927
+ if (opts.createdBefore)
928
+ lens.createdBefore = opts.createdBefore;
929
+ if (opts.modifiedAfter)
930
+ lens.modifiedAfter = opts.modifiedAfter;
931
+ if (opts.modifiedBefore)
932
+ lens.modifiedBefore = opts.modifiedBefore;
933
+ const result = applyLens(allMemories, lens);
934
+ if (result.length === 0) {
935
+ console.log("No memories match the lens filter.");
936
+ return;
937
+ }
938
+ console.log(`${result.length} memories match:\n`);
939
+ for (const m of result) {
940
+ const src = m.sourceLabel || "";
941
+ console.log(` [${m.frontmatter.status}] ${m.frontmatter.title} (${m.frontmatter.confidence})`);
942
+ console.log(` ${src ? src + ":" : ""}${m.relativePath}`);
943
+ console.log();
944
+ }
945
+ });
946
+ // ─── gnosys history <path> ───────────────────────────────────────────────
947
+ program
948
+ .command("history <memoryPath>")
949
+ .description("Show version history for a memory (git-backed)")
950
+ .option("-n, --limit <number>", "Max entries", "20")
951
+ .option("--diff <hash>", "Show diff from this commit to current")
952
+ .action(async (memPath, opts) => {
953
+ const resolver = await getResolver();
954
+ const memory = await resolver.readMemory(memPath);
955
+ if (!memory) {
956
+ console.error(`Memory not found: ${memPath}`);
957
+ process.exit(1);
958
+ }
959
+ const sourceStore = resolver.getStores().find((s) => s.label === memory.sourceLabel);
960
+ if (!sourceStore) {
961
+ console.error("Could not locate source store.");
962
+ process.exit(1);
963
+ }
964
+ if (!hasGitHistory(sourceStore.path)) {
965
+ console.error("No git history available for this store.");
966
+ process.exit(1);
967
+ }
968
+ if (opts.diff) {
969
+ const diff = getFileDiff(sourceStore.path, memory.relativePath, opts.diff, "HEAD");
970
+ if (!diff) {
971
+ console.error("Could not generate diff.");
972
+ process.exit(1);
973
+ }
974
+ console.log(diff);
975
+ return;
976
+ }
977
+ const history = getFileHistory(sourceStore.path, memory.relativePath, parseInt(opts.limit));
978
+ if (history.length === 0) {
979
+ console.log("No history found for this memory.");
980
+ return;
981
+ }
982
+ console.log(`History for ${memory.frontmatter.title}:\n`);
983
+ for (const entry of history) {
984
+ console.log(` ${entry.commitHash.substring(0, 7)} ${entry.date} ${entry.message}`);
985
+ }
986
+ });
987
+ // ─── gnosys rollback <path> <hash> ──────────────────────────────────────
988
+ program
989
+ .command("rollback <memoryPath> <commitHash>")
990
+ .description("Rollback a memory to its state at a specific commit")
991
+ .action(async (memPath, commitHash) => {
992
+ const resolver = await getResolver();
993
+ const memory = await resolver.readMemory(memPath);
994
+ if (!memory) {
995
+ console.error(`Memory not found: ${memPath}`);
996
+ process.exit(1);
997
+ }
998
+ const sourceStore = resolver.getStores().find((s) => s.label === memory.sourceLabel);
999
+ if (!sourceStore?.writable) {
1000
+ console.error("Cannot rollback: store is read-only.");
1001
+ process.exit(1);
1002
+ }
1003
+ const success = rollbackToCommit(sourceStore.path, memory.relativePath, commitHash);
1004
+ if (success) {
1005
+ console.log(`Rolled back ${memory.frontmatter.title} to commit ${commitHash.substring(0, 7)}.`);
1006
+ }
1007
+ else {
1008
+ console.error(`Rollback failed. Check that the commit hash is valid.`);
1009
+ process.exit(1);
1010
+ }
1011
+ });
1012
+ // ─── gnosys timeline ────────────────────────────────────────────────────
1013
+ program
1014
+ .command("timeline")
1015
+ .description("Show when memories were created and modified over time")
1016
+ .option("-p, --period <period>", "Group by: day, week, month (default), year", "month")
1017
+ .action(async (opts) => {
1018
+ const resolver = await getResolver();
1019
+ const allMemories = await resolver.getAllMemories();
1020
+ if (allMemories.length === 0) {
1021
+ console.log("No memories found.");
1022
+ return;
1023
+ }
1024
+ const entries = groupByPeriod(allMemories, opts.period);
1025
+ console.log(`Knowledge Timeline (by ${opts.period}):\n`);
1026
+ for (const entry of entries) {
1027
+ const parts = [];
1028
+ if (entry.created > 0)
1029
+ parts.push(`${entry.created} created`);
1030
+ if (entry.modified > 0)
1031
+ parts.push(`${entry.modified} modified`);
1032
+ console.log(` ${entry.period}: ${parts.join(", ")}`);
1033
+ if (entry.titles.length > 0 && entry.titles.length <= 5) {
1034
+ for (const t of entry.titles) {
1035
+ console.log(` + ${t}`);
1036
+ }
1037
+ }
1038
+ }
1039
+ });
1040
+ // ─── gnosys stats ───────────────────────────────────────────────────────
1041
+ program
1042
+ .command("stats")
1043
+ .description("Show summary statistics for the memory store")
1044
+ .option("--json", "Output as JSON")
1045
+ .action(async (opts) => {
1046
+ const resolver = await getResolver();
1047
+ const allMemories = await resolver.getAllMemories();
1048
+ if (allMemories.length === 0) {
1049
+ outputResult(!!opts.json, { totalCount: 0 }, () => {
1050
+ console.log("No memories found.");
1051
+ });
1052
+ return;
1053
+ }
1054
+ const stats = computeStats(allMemories);
1055
+ outputResult(!!opts.json, stats, () => {
1056
+ console.log(`Gnosys Store Statistics:\n`);
1057
+ console.log(` Total memories: ${stats.totalCount}`);
1058
+ console.log(` Average confidence: ${stats.averageConfidence}`);
1059
+ console.log(` Date range: ${stats.oldestCreated} → ${stats.newestCreated}`);
1060
+ console.log(` Last modified: ${stats.lastModified}`);
1061
+ console.log(`\n By category:`);
1062
+ for (const [cat, count] of Object.entries(stats.byCategory).sort((a, b) => b[1] - a[1])) {
1063
+ console.log(` ${cat}: ${count}`);
1064
+ }
1065
+ console.log(`\n By status:`);
1066
+ for (const [st, count] of Object.entries(stats.byStatus)) {
1067
+ console.log(` ${st}: ${count}`);
1068
+ }
1069
+ console.log(`\n By author:`);
1070
+ for (const [author, count] of Object.entries(stats.byAuthor)) {
1071
+ console.log(` ${author}: ${count}`);
1072
+ }
1073
+ });
1074
+ });
1075
+ // ─── gnosys links <path> ─────────────────────────────────────────────────
1076
+ program
1077
+ .command("links <memoryPath>")
1078
+ .description("Show wikilinks for a memory — both outgoing [[links]] and backlinks from other memories")
1079
+ .action(async (memPath) => {
1080
+ const resolver = await getResolver();
1081
+ const memory = await resolver.readMemory(memPath);
1082
+ if (!memory) {
1083
+ console.error(`Memory not found: ${memPath}`);
1084
+ process.exit(1);
1085
+ }
1086
+ const allMemories = await resolver.getAllMemories();
1087
+ const outgoing = getOutgoingLinks(allMemories, memory.relativePath);
1088
+ const backlinks = getBacklinks(allMemories, memory.relativePath);
1089
+ console.log(`Links for ${memory.frontmatter.title}:\n`);
1090
+ if (outgoing.length > 0) {
1091
+ console.log(` Outgoing (${outgoing.length}):`);
1092
+ for (const link of outgoing) {
1093
+ const display = link.displayText ? ` (${link.displayText})` : "";
1094
+ console.log(` → [[${link.target}]]${display}`);
1095
+ }
1096
+ }
1097
+ else {
1098
+ console.log(" No outgoing links.");
1099
+ }
1100
+ console.log();
1101
+ if (backlinks.length > 0) {
1102
+ console.log(` Backlinks (${backlinks.length}):`);
1103
+ for (const link of backlinks) {
1104
+ console.log(` ← ${link.sourceTitle} (${link.sourcePath})`);
1105
+ }
1106
+ }
1107
+ else {
1108
+ console.log(" No backlinks.");
1109
+ }
1110
+ });
1111
+ // ─── gnosys graph ───────────────────────────────────────────────────────
1112
+ program
1113
+ .command("graph")
1114
+ .description("Show the full cross-reference graph across all memories")
1115
+ .action(async () => {
1116
+ const resolver = await getResolver();
1117
+ const allMemories = await resolver.getAllMemories();
1118
+ if (allMemories.length === 0) {
1119
+ console.log("No memories found.");
1120
+ return;
1121
+ }
1122
+ const graph = buildLinkGraph(allMemories);
1123
+ console.log(formatGraphSummary(graph));
1124
+ });
1125
+ // ─── gnosys bootstrap <sourceDir> ────────────────────────────────────────
1126
+ program
1127
+ .command("bootstrap <sourceDir>")
1128
+ .description("Batch-import existing documents into the memory store")
1129
+ .option("-p, --pattern <patterns...>", "File patterns to match (default: **/*.md)")
1130
+ .option("--skip-existing", "Skip files whose titles already exist in the store")
1131
+ .option("-c, --category <category>", "Default category (default: imported)", "imported")
1132
+ .option("-a, --author <author>", "Default author", "human")
1133
+ .option("--authority <authority>", "Default authority", "imported")
1134
+ .option("--confidence <n>", "Default confidence (0-1)", "0.7")
1135
+ .option("--preserve-frontmatter", "Preserve existing YAML frontmatter if present")
1136
+ .option("--dry-run", "Show what would be imported without writing")
1137
+ .option("-s, --store <store>", "Target store (project|personal|global)", undefined)
1138
+ .action(async (sourceDir, opts) => {
1139
+ const resolver = await getResolver();
1140
+ const writeTarget = resolver.getWriteTarget(opts.store || undefined);
1141
+ if (!writeTarget) {
1142
+ console.error("No writable store found.");
1143
+ process.exit(1);
1144
+ }
1145
+ // Show what we'll scan
1146
+ const files = await discoverFiles(sourceDir, opts.pattern);
1147
+ console.log(`Found ${files.length} files in ${sourceDir}\n`);
1148
+ if (files.length === 0) {
1149
+ console.log("Nothing to import.");
1150
+ return;
1151
+ }
1152
+ const result = await bootstrap(writeTarget.store, {
1153
+ sourceDir,
1154
+ patterns: opts.pattern,
1155
+ skipExisting: opts.skipExisting,
1156
+ defaultCategory: opts.category,
1157
+ defaultAuthor: opts.author,
1158
+ defaultAuthority: opts.authority,
1159
+ defaultConfidence: parseFloat(opts.confidence),
1160
+ preserveFrontmatter: opts.preserveFrontmatter,
1161
+ dryRun: opts.dryRun,
1162
+ });
1163
+ const mode = opts.dryRun ? "DRY RUN" : "COMPLETE";
1164
+ console.log(`\nBootstrap ${mode}:`);
1165
+ console.log(` Scanned: ${result.totalScanned}`);
1166
+ console.log(` ${opts.dryRun ? "Would import" : "Imported"}: ${result.imported.length}`);
1167
+ console.log(` Skipped: ${result.skipped.length}`);
1168
+ console.log(` Failed: ${result.failed.length}`);
1169
+ if (result.imported.length > 0) {
1170
+ console.log(`\n${opts.dryRun ? "Would import" : "Imported"}:`);
1171
+ for (const f of result.imported) {
1172
+ console.log(` + ${f}`);
1173
+ }
1174
+ }
1175
+ if (result.skipped.length > 0) {
1176
+ console.log(`\nSkipped (already exist):`);
1177
+ for (const f of result.skipped) {
1178
+ console.log(` ⏭ ${f}`);
1179
+ }
1180
+ }
1181
+ if (result.failed.length > 0) {
1182
+ console.log(`\nFailed:`);
1183
+ for (const f of result.failed) {
1184
+ console.log(` ❌ ${f.path}: ${f.error}`);
1185
+ }
1186
+ }
1187
+ });
1188
+ // ─── gnosys import <file> ────────────────────────────────────────────────
1189
+ program
1190
+ .command("import <fileOrUrl>")
1191
+ .description("Bulk import structured data (CSV, JSON, JSONL) into Gnosys memories")
1192
+ .requiredOption("--format <format>", "Data format: csv, json, jsonl")
1193
+ .requiredOption("--mapping <json>", 'Field mapping as JSON: \'{"source_field":"gnosys_field"}\'. Valid targets: title, category, content, tags, relevance')
1194
+ .option("--mode <mode>", "Processing mode: llm or structured", "structured")
1195
+ .option("--limit <n>", "Max records to import", parseInt)
1196
+ .option("--offset <n>", "Skip first N records", parseInt)
1197
+ .option("--skip-existing", "Skip records whose titles already exist")
1198
+ .option("--batch-commit", "Single git commit for all imports (default)", true)
1199
+ .option("--no-batch-commit", "Commit each record individually")
1200
+ .option("--concurrency <n>", "Parallel LLM calls (default: 5)", parseInt)
1201
+ .option("--dry-run", "Preview without writing")
1202
+ .option("--store <store>", "Target store: project, personal, global", "project")
1203
+ .action(async (fileOrUrl, opts) => {
1204
+ // Parse mapping JSON
1205
+ let mapping;
1206
+ try {
1207
+ mapping = JSON.parse(opts.mapping);
1208
+ }
1209
+ catch {
1210
+ console.error("Error: --mapping must be valid JSON. Example: '{\"name\":\"title\",\"group\":\"category\"}'");
1211
+ process.exit(1);
1212
+ }
1213
+ const resolver = await getResolver();
1214
+ const writeTarget = resolver.getWriteTarget(opts.store);
1215
+ if (!writeTarget) {
1216
+ console.error("No writable store found.");
1217
+ process.exit(1);
1218
+ }
1219
+ const tagRegistry = new GnosysTagRegistry(writeTarget.store.getStorePath());
1220
+ await tagRegistry.load();
1221
+ const ingestion = new GnosysIngestion(writeTarget.store, tagRegistry);
1222
+ const format = opts.format;
1223
+ const mode = opts.mode;
1224
+ const concurrency = opts.concurrency || 5;
1225
+ // Show estimate for LLM mode
1226
+ if (mode === "llm") {
1227
+ console.error(`Mode: LLM (concurrency: ${concurrency})`);
1228
+ }
1229
+ else {
1230
+ console.error("Mode: structured (no LLM calls)");
1231
+ }
1232
+ if (opts.dryRun) {
1233
+ console.error("DRY RUN — no files will be written\n");
1234
+ }
1235
+ // Progress tracking
1236
+ let lastLine = "";
1237
+ const onProgress = (p) => {
1238
+ const pct = p.total > 0 ? Math.round((p.processed / p.total) * 100) : 0;
1239
+ const bar = "█".repeat(Math.floor(pct / 5)) +
1240
+ "░".repeat(20 - Math.floor(pct / 5));
1241
+ const line = `[${bar}] ${p.processed}/${p.total} | ${p.current.substring(0, 40)}`;
1242
+ if (line !== lastLine) {
1243
+ process.stderr.write(`\r${line}`);
1244
+ lastLine = line;
1245
+ }
1246
+ };
1247
+ try {
1248
+ const result = await performImport(writeTarget.store, ingestion, {
1249
+ format,
1250
+ data: fileOrUrl,
1251
+ mapping,
1252
+ mode,
1253
+ limit: opts.limit,
1254
+ offset: opts.offset,
1255
+ dryRun: opts.dryRun,
1256
+ skipExisting: opts.skipExisting,
1257
+ batchCommit: opts.batchCommit,
1258
+ concurrency,
1259
+ onProgress,
1260
+ });
1261
+ // Clear progress line
1262
+ process.stderr.write("\r" + " ".repeat(80) + "\r");
1263
+ // Reindex search after import
1264
+ if (!opts.dryRun && result.imported.length > 0) {
1265
+ const search = new (await import("./lib/search.js")).GnosysSearch(writeTarget.store.getStorePath());
1266
+ for (const s of resolver.getStores()) {
1267
+ await search.addStoreMemories(s.store, s.label);
1268
+ }
1269
+ }
1270
+ console.log((opts.dryRun ? "DRY RUN — " : "✓ ") +
1271
+ formatImportSummary(result));
1272
+ }
1273
+ catch (err) {
1274
+ console.error(`\nImport failed: ${err instanceof Error ? err.message : String(err)}`);
1275
+ process.exit(1);
1276
+ }
1277
+ });
1278
+ // ─── gnosys reindex ──────────────────────────────────────────────────────
1279
+ program
1280
+ .command("reindex")
1281
+ .description("Rebuild all semantic embeddings from every memory file. Downloads the model (~80 MB) on first run.")
1282
+ .action(async () => {
1283
+ const resolver = await getResolver();
1284
+ const stores = resolver.getStores();
1285
+ if (stores.length === 0) {
1286
+ console.error("No stores found. Run gnosys init first.");
1287
+ process.exit(1);
1288
+ }
1289
+ const storePath = stores[0].path;
1290
+ const search = new GnosysSearch(storePath);
1291
+ search.clearIndex();
1292
+ for (const s of stores) {
1293
+ await search.addStoreMemories(s.store, s.label);
1294
+ }
1295
+ const embeddings = new GnosysEmbeddings(storePath);
1296
+ const hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, storePath);
1297
+ console.log("Building semantic embeddings (downloading model on first run)...");
1298
+ const count = await hybridSearch.reindex((current, total, filePath) => {
1299
+ process.stdout.write(`\r Indexing: ${current}/${total} — ${filePath.substring(0, 60)}`);
1300
+ });
1301
+ console.log(`\n\nReindex complete: ${count} memories embedded.`);
1302
+ console.log("Hybrid and semantic search are now available.");
1303
+ search.close();
1304
+ embeddings.close();
1305
+ });
1306
+ // ─── gnosys hybrid-search <query> ───────────────────────────────────────
1307
+ program
1308
+ .command("hybrid-search <query>")
1309
+ .description("Search using hybrid keyword + semantic fusion (RRF). Use --federated for cross-scope.")
1310
+ .option("-l, --limit <n>", "Max results", "15")
1311
+ .option("-m, --mode <mode>", "Search mode: keyword | semantic | hybrid", "hybrid")
1312
+ .option("--json", "Output as JSON")
1313
+ .option("--federated", "Use federated search with tier boosting (project > user > global)")
1314
+ .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated)")
1315
+ .option("-d, --directory <dir>", "Project directory for context")
1316
+ .action(async (query, opts) => {
1317
+ // Federated path — uses central DB
1318
+ if (opts.federated || opts.scope) {
1319
+ let centralDb = null;
1320
+ try {
1321
+ centralDb = GnosysDB.openCentral();
1322
+ if (!centralDb.isAvailable()) {
1323
+ console.error("Central DB not available.");
1324
+ process.exit(1);
1325
+ }
1326
+ const { federatedSearch, detectCurrentProject } = await import("./lib/federated.js");
1327
+ const projectId = await detectCurrentProject(centralDb, opts.directory || undefined);
1328
+ const scopeFilter = opts.scope ? opts.scope.split(",").map(s => s.trim()) : undefined;
1329
+ const results = federatedSearch(centralDb, query, {
1330
+ limit: parseInt(opts.limit, 10),
1331
+ projectId,
1332
+ scopeFilter,
1333
+ });
1334
+ outputResult(!!opts.json, { query, projectId, mode: "federated", count: results.length, results }, () => {
1335
+ if (results.length === 0) {
1336
+ console.log(`No results for "${query}".`);
1337
+ return;
1338
+ }
1339
+ console.log(`Found ${results.length} results for "${query}" (mode: federated):\n`);
1340
+ for (const [i, r] of results.entries()) {
1341
+ const proj = r.projectName ? ` [${r.projectName}]` : "";
1342
+ console.log(`${i + 1}. ${r.title} (${r.category})${proj}`);
1343
+ console.log(` scope: ${r.scope} | score: ${r.score.toFixed(4)} | boosts: ${r.boosts.join(", ")}`);
1344
+ if (r.snippet)
1345
+ console.log(` ${r.snippet.substring(0, 120)}`);
1346
+ }
1347
+ });
1348
+ }
1349
+ catch (err) {
1350
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
1351
+ process.exit(1);
1352
+ }
1353
+ finally {
1354
+ centralDb?.close();
1355
+ }
1356
+ return;
1357
+ }
1358
+ // Legacy file-based hybrid search
1359
+ const resolver = await getResolver();
1360
+ const stores = resolver.getStores();
1361
+ if (stores.length === 0) {
1362
+ console.error("No stores found.");
1363
+ process.exit(1);
1364
+ }
1365
+ const storePath = stores[0].path;
1366
+ const search = new GnosysSearch(storePath);
1367
+ search.clearIndex();
1368
+ for (const s of stores) {
1369
+ await search.addStoreMemories(s.store, s.label);
1370
+ }
1371
+ const embeddings = new GnosysEmbeddings(storePath);
1372
+ const hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, storePath);
1373
+ const mode = opts.mode;
1374
+ const results = await hybridSearch.hybridSearch(query, parseInt(opts.limit), mode);
1375
+ if (results.length === 0) {
1376
+ outputResult(!!opts.json, { query, mode, results: [] }, () => {
1377
+ console.log(`No results for "${query}". Try gnosys reindex to build embeddings.`);
1378
+ });
1379
+ }
1380
+ else {
1381
+ outputResult(!!opts.json, { query, mode, count: results.length, results }, () => {
1382
+ console.log(`Found ${results.length} results for "${query}" (mode: ${mode}):\n`);
1383
+ for (const r of results) {
1384
+ console.log(` ${r.title}`);
1385
+ console.log(` Path: ${r.relativePath}`);
1386
+ console.log(` Score: ${r.score.toFixed(4)} (via: ${r.sources.join("+")})`);
1387
+ console.log(` ${r.snippet.substring(0, 120)}...\n`);
1388
+ }
1389
+ });
1390
+ // Reinforce used memories (best-effort)
1391
+ const writeTarget = resolver.getWriteTarget();
1392
+ if (writeTarget) {
1393
+ const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
1394
+ await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, results.map((r) => r.relativePath)).catch(() => { });
1395
+ }
1396
+ }
1397
+ search.close();
1398
+ embeddings.close();
1399
+ });
1400
+ // ─── gnosys semantic-search <query> ─────────────────────────────────────
1401
+ program
1402
+ .command("semantic-search <query>")
1403
+ .description("Search using semantic similarity only (requires embeddings)")
1404
+ .option("-l, --limit <n>", "Max results", "15")
1405
+ .action(async (query, opts) => {
1406
+ const resolver = await getResolver();
1407
+ const stores = resolver.getStores();
1408
+ if (stores.length === 0) {
1409
+ console.error("No stores found.");
1410
+ process.exit(1);
1411
+ }
1412
+ const storePath = stores[0].path;
1413
+ const search = new GnosysSearch(storePath);
1414
+ search.clearIndex();
1415
+ for (const s of stores) {
1416
+ await search.addStoreMemories(s.store, s.label);
1417
+ }
1418
+ const embeddings = new GnosysEmbeddings(storePath);
1419
+ const hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, storePath);
1420
+ const results = await hybridSearch.hybridSearch(query, parseInt(opts.limit), "semantic");
1421
+ if (results.length === 0) {
1422
+ console.log(`No semantic results for "${query}". Run gnosys reindex first.`);
1423
+ }
1424
+ else {
1425
+ console.log(`Found ${results.length} semantic results for "${query}":\n`);
1426
+ for (const r of results) {
1427
+ console.log(` ${r.title}`);
1428
+ console.log(` Path: ${r.relativePath}`);
1429
+ console.log(` Similarity: ${r.score.toFixed(4)}`);
1430
+ console.log(` ${r.snippet.substring(0, 120)}...\n`);
1431
+ }
1432
+ }
1433
+ search.close();
1434
+ embeddings.close();
1435
+ });
1436
+ // ─── gnosys ask <question> ──────────────────────────────────────────────
1437
+ program
1438
+ .command("ask <question>")
1439
+ .description("Ask a natural-language question and get a synthesized answer with citations. Use --federated for cross-scope.")
1440
+ .option("-l, --limit <n>", "Max memories to retrieve", "15")
1441
+ .option("-m, --mode <mode>", "Search mode: keyword | semantic | hybrid", "hybrid")
1442
+ .option("--no-stream", "Disable streaming output")
1443
+ .option("--federated", "Use federated search with tier boosting (project > user > global)")
1444
+ .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated)")
1445
+ .option("-d, --directory <dir>", "Project directory for context")
1446
+ .action(async (question, opts) => {
1447
+ const resolver = await getResolver();
1448
+ const stores = resolver.getStores();
1449
+ if (stores.length === 0) {
1450
+ console.error("No stores found. Run gnosys init first.");
1451
+ process.exit(1);
1452
+ }
1453
+ const storePath = stores[0].path;
1454
+ let cliConfig;
1455
+ try {
1456
+ cliConfig = await loadConfig(storePath);
1457
+ }
1458
+ catch {
1459
+ cliConfig = (await import("./lib/config.js")).DEFAULT_CONFIG;
1460
+ }
1461
+ const search = new GnosysSearch(storePath);
1462
+ search.clearIndex();
1463
+ for (const s of stores) {
1464
+ await search.addStoreMemories(s.store, s.label);
1465
+ }
1466
+ const embeddings = new GnosysEmbeddings(storePath);
1467
+ const hybridSearch = new GnosysHybridSearch(search, embeddings, resolver, storePath);
1468
+ const ask = new GnosysAsk(hybridSearch, cliConfig, resolver, storePath);
1469
+ if (!ask.isLLMAvailable) {
1470
+ console.error("No LLM provider available. Set ANTHROPIC_API_KEY or switch to Ollama: gnosys config set provider ollama");
1471
+ process.exit(1);
1472
+ }
1473
+ // If --federated, pre-retrieve from central DB and inject as context
1474
+ let federatedContext;
1475
+ if (opts.federated || opts.scope) {
1476
+ let centralDb = null;
1477
+ try {
1478
+ centralDb = GnosysDB.openCentral();
1479
+ if (centralDb?.isAvailable()) {
1480
+ const { federatedSearch: fSearch, detectCurrentProject } = await import("./lib/federated.js");
1481
+ const projectId = await detectCurrentProject(centralDb, opts.directory || undefined);
1482
+ const scopeFilter = opts.scope ? opts.scope.split(",").map(s => s.trim()) : undefined;
1483
+ const fResults = fSearch(centralDb, question, {
1484
+ limit: parseInt(opts.limit, 10),
1485
+ projectId,
1486
+ scopeFilter,
1487
+ });
1488
+ if (fResults.length > 0) {
1489
+ federatedContext = fResults.map(r => {
1490
+ const mem = centralDb.getMemory(r.id);
1491
+ return `## ${r.title} [scope:${r.scope}, score:${r.score.toFixed(3)}]\n${mem?.content || r.snippet}`;
1492
+ }).join("\n\n");
1493
+ console.error(`[federated] Found ${fResults.length} cross-scope memories as additional context`);
1494
+ }
1495
+ }
1496
+ }
1497
+ catch { /* Central DB not available — fall through to normal ask */ }
1498
+ finally {
1499
+ centralDb?.close();
1500
+ }
1501
+ }
1502
+ const mode = opts.mode;
1503
+ const useStream = opts.stream !== false;
1504
+ try {
1505
+ const result = await ask.ask(question, {
1506
+ limit: parseInt(opts.limit),
1507
+ mode,
1508
+ stream: useStream,
1509
+ additionalContext: federatedContext,
1510
+ callbacks: useStream
1511
+ ? {
1512
+ onToken: (token) => process.stdout.write(token),
1513
+ onSearchComplete: (count, searchMode) => {
1514
+ console.log(`\n Found ${count} relevant memories (${searchMode} search)\n`);
1515
+ },
1516
+ onDeepQuery: (refined) => {
1517
+ console.log(`\n Deep query: searching for "${refined}"...\n`);
1518
+ },
1519
+ }
1520
+ : undefined,
1521
+ });
1522
+ if (!useStream) {
1523
+ console.log(result.answer);
1524
+ }
1525
+ // Print sources
1526
+ if (result.sources.length > 0) {
1527
+ console.log("\n\n--- Sources ---");
1528
+ for (const s of result.sources) {
1529
+ console.log(` [[${s.relativePath.split("/").pop()}]] — ${s.title}`);
1530
+ }
1531
+ // Reinforce used memories (best-effort)
1532
+ const writeTarget = resolver.getWriteTarget();
1533
+ if (writeTarget) {
1534
+ const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
1535
+ await GnosysMaintenanceEngine.reinforceBatch(writeTarget.store, result.sources.map((s) => s.relativePath)).catch(() => { });
1536
+ }
1537
+ }
1538
+ if (result.deepQueryUsed) {
1539
+ console.log("\n(Deep query was used — a follow-up search expanded the context)");
1540
+ }
1541
+ }
1542
+ catch (err) {
1543
+ console.error(`Ask failed: ${err instanceof Error ? err.message : String(err)}`);
1544
+ process.exit(1);
1545
+ }
1546
+ search.close();
1547
+ embeddings.close();
1548
+ });
1549
+ // ─── gnosys stores ───────────────────────────────────────────────────────
1550
+ program
1551
+ .command("stores")
1552
+ .description("Show all active stores, their layers, paths, and permissions")
1553
+ .action(async () => {
1554
+ const resolver = await getResolver();
1555
+ console.log(resolver.getSummary());
1556
+ });
1557
+ // ─── gnosys config ──────────────────────────────────────────────────────
1558
+ const configCmd = program
1559
+ .command("config")
1560
+ .description("View and manage LLM provider configuration");
1561
+ configCmd
1562
+ .command("show")
1563
+ .description("Show current LLM configuration")
1564
+ .action(async () => {
1565
+ const resolver = await getResolver();
1566
+ const stores = resolver.getStores();
1567
+ if (stores.length === 0) {
1568
+ console.error("No stores found. Run gnosys init first.");
1569
+ process.exit(1);
1570
+ }
1571
+ const cfg = await loadConfig(stores[0].path);
1572
+ console.log("System of Cognition (SOC) — LLM Configuration:");
1573
+ console.log(` Default provider: ${cfg.llm.defaultProvider}`);
1574
+ console.log("");
1575
+ console.log(" Providers:");
1576
+ console.log(` Anthropic: model=${cfg.llm.anthropic.model}, apiKey=${cfg.llm.anthropic.apiKey ? "config" : (process.env.ANTHROPIC_API_KEY ? "env" : "—")}`);
1577
+ console.log(` Ollama: model=${cfg.llm.ollama.model}, url=${cfg.llm.ollama.baseUrl}`);
1578
+ console.log(` Groq: model=${cfg.llm.groq.model}, apiKey=${cfg.llm.groq.apiKey ? "config" : (process.env.GROQ_API_KEY ? "env" : "—")}`);
1579
+ console.log(` OpenAI: model=${cfg.llm.openai.model}, apiKey=${cfg.llm.openai.apiKey ? "config" : (process.env.OPENAI_API_KEY ? "env" : "—")}, url=${cfg.llm.openai.baseUrl}`);
1580
+ console.log(` LM Studio: model=${cfg.llm.lmstudio.model}, url=${cfg.llm.lmstudio.baseUrl}`);
1581
+ console.log("");
1582
+ const structuring = resolveTaskModel(cfg, "structuring");
1583
+ const synthesis = resolveTaskModel(cfg, "synthesis");
1584
+ console.log(" Task Routing:");
1585
+ console.log(` Structuring: ${structuring.provider}/${structuring.model}${cfg.taskModels?.structuring ? " (override)" : " (default)"}`);
1586
+ console.log(` Synthesis: ${synthesis.provider}/${synthesis.model}${cfg.taskModels?.synthesis ? " (override)" : " (default)"}`);
1587
+ });
1588
+ configCmd
1589
+ .command("set <key> <value> [extra...]")
1590
+ .description("Set a config value. Keys: provider, model, ollama-url, groq-model, openai-model, lmstudio-url, task <task> <provider> <model>")
1591
+ .action(async (key, value, extra) => {
1592
+ const resolver = await getResolver();
1593
+ const writeTarget = resolver.getWriteTarget();
1594
+ if (!writeTarget) {
1595
+ console.error("No writable store found.");
1596
+ process.exit(1);
1597
+ }
1598
+ const storePath = writeTarget.store.getStorePath();
1599
+ const cfg = await loadConfig(storePath);
1600
+ const validProviders = ALL_PROVIDERS.join(", ");
1601
+ switch (key) {
1602
+ case "provider":
1603
+ if (!ALL_PROVIDERS.includes(value)) {
1604
+ console.error(`Invalid provider: "${value}". Supported: ${validProviders}`);
1605
+ process.exit(1);
1606
+ }
1607
+ cfg.llm.defaultProvider = value;
1608
+ console.log(`Default provider set to: ${value}`);
1609
+ break;
1610
+ case "model": {
1611
+ // Set model for current default provider
1612
+ const p = cfg.llm.defaultProvider;
1613
+ if (p === "anthropic")
1614
+ cfg.llm.anthropic.model = value;
1615
+ else if (p === "ollama")
1616
+ cfg.llm.ollama.model = value;
1617
+ else if (p === "groq")
1618
+ cfg.llm.groq.model = value;
1619
+ else if (p === "openai")
1620
+ cfg.llm.openai.model = value;
1621
+ else if (p === "lmstudio")
1622
+ cfg.llm.lmstudio.model = value;
1623
+ console.log(`Model set to: ${value} (for ${p})`);
1624
+ break;
1625
+ }
1626
+ case "task": {
1627
+ // gnosys config set task <taskName> <provider> <model>
1628
+ const taskName = value;
1629
+ const taskProvider = extra[0];
1630
+ const taskModel = extra[1];
1631
+ if (!taskName || !taskProvider || !taskModel) {
1632
+ console.error("Usage: gnosys config set task <structuring|synthesis> <provider> <model>");
1633
+ process.exit(1);
1634
+ }
1635
+ if (taskName !== "structuring" && taskName !== "synthesis") {
1636
+ console.error(`Invalid task: "${taskName}". Valid: structuring, synthesis`);
1637
+ process.exit(1);
1638
+ }
1639
+ if (!ALL_PROVIDERS.includes(taskProvider)) {
1640
+ console.error(`Invalid provider: "${taskProvider}". Supported: ${validProviders}`);
1641
+ process.exit(1);
1642
+ }
1643
+ if (!cfg.taskModels)
1644
+ cfg.taskModels = {};
1645
+ cfg.taskModels[taskName] = { provider: taskProvider, model: taskModel };
1646
+ console.log(`Task "${taskName}" routed to: ${taskProvider}/${taskModel}`);
1647
+ break;
1648
+ }
1649
+ case "ollama-url":
1650
+ cfg.llm.ollama.baseUrl = value;
1651
+ console.log(`Ollama base URL set to: ${value}`);
1652
+ break;
1653
+ case "ollama-model":
1654
+ cfg.llm.ollama.model = value;
1655
+ console.log(`Ollama model set to: ${value}`);
1656
+ break;
1657
+ case "anthropic-model":
1658
+ cfg.llm.anthropic.model = value;
1659
+ console.log(`Anthropic model set to: ${value}`);
1660
+ break;
1661
+ case "groq-model":
1662
+ cfg.llm.groq.model = value;
1663
+ console.log(`Groq model set to: ${value}`);
1664
+ break;
1665
+ case "openai-model":
1666
+ cfg.llm.openai.model = value;
1667
+ console.log(`OpenAI model set to: ${value}`);
1668
+ break;
1669
+ case "openai-url":
1670
+ cfg.llm.openai.baseUrl = value;
1671
+ console.log(`OpenAI base URL set to: ${value}`);
1672
+ break;
1673
+ case "lmstudio-url":
1674
+ cfg.llm.lmstudio.baseUrl = value;
1675
+ console.log(`LM Studio base URL set to: ${value}`);
1676
+ break;
1677
+ case "lmstudio-model":
1678
+ cfg.llm.lmstudio.model = value;
1679
+ console.log(`LM Studio model set to: ${value}`);
1680
+ break;
1681
+ case "recall": {
1682
+ // gnosys config set recall <field> <value>
1683
+ // Supported: recall aggressive true/false, recall maxMemories <n>, recall minRelevance <n>
1684
+ const recallField = value;
1685
+ const recallValue = extra[0];
1686
+ if (!recallField || !recallValue) {
1687
+ console.error("Usage: gnosys config set recall <aggressive|maxMemories|minRelevance> <value>");
1688
+ process.exit(1);
1689
+ }
1690
+ if (!cfg.recall)
1691
+ cfg.recall = { aggressive: true, maxMemories: 8, minRelevance: 0.4 };
1692
+ switch (recallField) {
1693
+ case "aggressive":
1694
+ if (recallValue !== "true" && recallValue !== "false") {
1695
+ console.error(`Invalid value: "${recallValue}". Use "true" or "false".`);
1696
+ process.exit(1);
1697
+ }
1698
+ cfg.recall.aggressive = recallValue === "true";
1699
+ console.log(`Recall aggressive mode: ${cfg.recall.aggressive ? "enabled" : "disabled"}`);
1700
+ break;
1701
+ case "maxMemories": {
1702
+ const n = parseInt(recallValue, 10);
1703
+ if (isNaN(n) || n < 1 || n > 20) {
1704
+ console.error("maxMemories must be between 1 and 20");
1705
+ process.exit(1);
1706
+ }
1707
+ cfg.recall.maxMemories = n;
1708
+ console.log(`Recall maxMemories set to: ${n}`);
1709
+ break;
1710
+ }
1711
+ case "minRelevance": {
1712
+ const f = parseFloat(recallValue);
1713
+ if (isNaN(f) || f < 0 || f > 1) {
1714
+ console.error("minRelevance must be between 0 and 1");
1715
+ process.exit(1);
1716
+ }
1717
+ cfg.recall.minRelevance = f;
1718
+ console.log(`Recall minRelevance set to: ${f}`);
1719
+ break;
1720
+ }
1721
+ default:
1722
+ console.error(`Unknown recall field: "${recallField}". Valid: aggressive, maxMemories, minRelevance`);
1723
+ process.exit(1);
1724
+ }
1725
+ break;
1726
+ }
1727
+ default:
1728
+ console.error(`Unknown config key: "${key}". Valid: provider, model, task, ollama-url, ollama-model, anthropic-model, groq-model, openai-model, openai-url, lmstudio-url, lmstudio-model, recall`);
1729
+ process.exit(1);
1730
+ }
1731
+ await writeConfig(storePath, cfg);
1732
+ console.log("Configuration saved to gnosys.json");
1733
+ });
1734
+ configCmd
1735
+ .command("init")
1736
+ .description("Generate a default gnosys.json with LLM settings")
1737
+ .action(async () => {
1738
+ const resolver = await getResolver();
1739
+ const writeTarget = resolver.getWriteTarget();
1740
+ if (!writeTarget) {
1741
+ console.error("No writable store found.");
1742
+ process.exit(1);
1743
+ }
1744
+ const storePath = writeTarget.store.getStorePath();
1745
+ const configPath = path.join(storePath, "gnosys.json");
1746
+ try {
1747
+ await fs.access(configPath);
1748
+ console.error("gnosys.json already exists. Use 'gnosys config set' to modify.");
1749
+ process.exit(1);
1750
+ }
1751
+ catch {
1752
+ // File doesn't exist — good
1753
+ }
1754
+ await fs.writeFile(configPath, generateConfigTemplate() + "\n", "utf-8");
1755
+ console.log(`Created ${configPath}`);
1756
+ });
1757
+ // ─── gnosys reindex-graph ───────────────────────────────────────────────
1758
+ program
1759
+ .command("reindex-graph")
1760
+ .description("Build or rebuild the wikilink graph (.gnosys/graph.json)")
1761
+ .action(async () => {
1762
+ const { reindexGraph, formatGraphStats } = await import("./lib/graph.js");
1763
+ const resolver = await getResolver();
1764
+ const stores = resolver.getStores();
1765
+ if (stores.length === 0) {
1766
+ console.error("No Gnosys stores found. Run gnosys init first.");
1767
+ process.exit(1);
1768
+ }
1769
+ const stats = await reindexGraph(resolver, (msg) => console.log(msg));
1770
+ console.log("");
1771
+ console.log(formatGraphStats(stats));
1772
+ });
1773
+ // ─── gnosys dashboard ───────────────────────────────────────────────────
1774
+ program
1775
+ .command("dashboard")
1776
+ .description("Show system dashboard: memory count, health, graph stats, LLM status")
1777
+ .option("--json", "Output as JSON instead of pretty table")
1778
+ .action(async (opts) => {
1779
+ const { collectDashboardData, formatDashboard, formatDashboardJSON } = await import("./lib/dashboard.js");
1780
+ const resolver = await getResolver();
1781
+ const stores = resolver.getStores();
1782
+ if (stores.length === 0) {
1783
+ console.error("No Gnosys stores found. Run gnosys init first.");
1784
+ process.exit(1);
1785
+ }
1786
+ const cfg = await loadConfig(stores[0].path);
1787
+ // v2.0: Try to open GnosysDB for dashboard stats
1788
+ let dashDb;
1789
+ try {
1790
+ const { GnosysDB: DbClass } = await import("./lib/db.js");
1791
+ const db = new DbClass(stores[0].path);
1792
+ if (db.isAvailable() && db.isMigrated()) {
1793
+ dashDb = db;
1794
+ }
1795
+ }
1796
+ catch {
1797
+ // GnosysDB not available — legacy dashboard only
1798
+ }
1799
+ const data = await collectDashboardData(resolver, cfg, pkg.version, dashDb);
1800
+ if (opts.json) {
1801
+ console.log(formatDashboardJSON(data));
1802
+ }
1803
+ else {
1804
+ console.log(formatDashboard(data));
1805
+ }
1806
+ });
1807
+ // ─── gnosys maintain ─────────────────────────────────────────────────────
1808
+ program
1809
+ .command("maintain")
1810
+ .description("Run vault maintenance: detect duplicates, apply confidence decay, consolidate similar memories")
1811
+ .option("--dry-run", "Show what would change without modifying anything")
1812
+ .option("--auto-apply", "Automatically apply all changes (no prompts)")
1813
+ .action(async (opts) => {
1814
+ const { GnosysMaintenanceEngine, formatMaintenanceReport } = await import("./lib/maintenance.js");
1815
+ const resolver = await getResolver();
1816
+ const stores = resolver.getStores();
1817
+ if (stores.length === 0) {
1818
+ console.error("No Gnosys stores found. Run gnosys init first.");
1819
+ process.exit(1);
1820
+ }
1821
+ const cfg = await loadConfig(stores[0].path);
1822
+ const engine = new GnosysMaintenanceEngine(resolver, cfg);
1823
+ const report = await engine.maintain({
1824
+ dryRun: opts.dryRun,
1825
+ autoApply: opts.autoApply,
1826
+ onLog: (level, message) => {
1827
+ if (level === "warn") {
1828
+ console.error(`⚠ ${message}`);
1829
+ }
1830
+ else if (level === "action") {
1831
+ console.log(`→ ${message}`);
1832
+ }
1833
+ else {
1834
+ console.log(message);
1835
+ }
1836
+ },
1837
+ onProgress: (step, current, total) => {
1838
+ process.stdout.write(`\r[${current}/${total}] ${step}...`);
1839
+ if (current === total)
1840
+ process.stdout.write("\n");
1841
+ },
1842
+ });
1843
+ console.log("");
1844
+ console.log(formatMaintenanceReport(report));
1845
+ });
1846
+ // ─── gnosys dearchive ───────────────────────────────────────────────────
1847
+ program
1848
+ .command("dearchive <query>")
1849
+ .description("Force-dearchive memories matching a query from archive.db back to active")
1850
+ .option("--limit <n>", "Max memories to dearchive", "5")
1851
+ .action(async (query, opts) => {
1852
+ const { GnosysArchive } = await import("./lib/archive.js");
1853
+ const resolver = await getResolver();
1854
+ const stores = resolver.getStores();
1855
+ if (stores.length === 0) {
1856
+ console.error("No Gnosys stores found. Run gnosys init first.");
1857
+ process.exit(1);
1858
+ }
1859
+ const writeTarget = resolver.getWriteTarget();
1860
+ if (!writeTarget) {
1861
+ console.error("No writable store found.");
1862
+ process.exit(1);
1863
+ }
1864
+ const archive = new GnosysArchive(writeTarget.path);
1865
+ if (!archive.isAvailable()) {
1866
+ console.error("Archive not available. Is better-sqlite3 installed?");
1867
+ process.exit(1);
1868
+ }
1869
+ const results = archive.searchArchive(query, parseInt(opts.limit));
1870
+ if (results.length === 0) {
1871
+ console.log(`No archived memories found matching "${query}".`);
1872
+ archive.close();
1873
+ return;
1874
+ }
1875
+ console.log(`Found ${results.length} archived memories matching "${query}":\n`);
1876
+ for (const r of results) {
1877
+ console.log(` • ${r.title} (${r.id})`);
1878
+ }
1879
+ console.log("");
1880
+ // Dearchive all found
1881
+ const ids = results.map((r) => r.id);
1882
+ const restored = await archive.dearchiveBatch(ids, writeTarget.store);
1883
+ archive.close();
1884
+ console.log(`Dearchived ${restored.length} memories back to active:`);
1885
+ for (const rp of restored) {
1886
+ console.log(` → ${rp}`);
1887
+ }
1888
+ });
1889
+ // NOTE: gnosys migrate is defined below (near the end) with --to-central support
1890
+ // ─── gnosys doctor ──────────────────────────────────────────────────────
1891
+ program
1892
+ .command("doctor")
1893
+ .description("Check system health: stores, LLM connectivity, embeddings, archive")
1894
+ .action(async () => {
1895
+ const resolver = await getResolver();
1896
+ const stores = resolver.getStores();
1897
+ console.log("Gnosys Doctor");
1898
+ console.log("=============\n");
1899
+ // Check gnosys.db (v2.0 agent-native store)
1900
+ if (stores.length > 0) {
1901
+ console.log("Agent-Native Store (gnosys.db):");
1902
+ try {
1903
+ const db = new GnosysDB(stores[0].path);
1904
+ if (db.isAvailable() && db.isMigrated()) {
1905
+ const counts = db.getMemoryCount();
1906
+ console.log(` Status: ✓ migrated (schema v${db.getSchemaVersion()})`);
1907
+ console.log(` Active: ${counts.active} | Archived: ${counts.archived} | Total: ${counts.total}`);
1908
+ }
1909
+ else if (db.isAvailable()) {
1910
+ console.log(" Status: ✗ not migrated (run gnosys migrate)");
1911
+ }
1912
+ else {
1913
+ console.log(" Status: — not available (better-sqlite3 not installed)");
1914
+ }
1915
+ db.close();
1916
+ }
1917
+ catch {
1918
+ console.log(" Status: — not initialized");
1919
+ }
1920
+ console.log("");
1921
+ }
1922
+ // Check stores
1923
+ console.log("Stores:");
1924
+ if (stores.length === 0) {
1925
+ console.log(" No stores found. Run gnosys init first.");
1926
+ }
1927
+ else {
1928
+ for (const s of stores) {
1929
+ const memories = await s.store.getAllMemories();
1930
+ console.log(` ${s.label}: ${memories.length} memories (${s.path})`);
1931
+ }
1932
+ }
1933
+ console.log("");
1934
+ // Check archive
1935
+ if (stores.length > 0) {
1936
+ console.log("Archive (Two-Tier Memory):");
1937
+ try {
1938
+ const { GnosysArchive } = await import("./lib/archive.js");
1939
+ const archive = new GnosysArchive(stores[0].path);
1940
+ if (archive.isAvailable()) {
1941
+ const stats = archive.getStats();
1942
+ console.log(` Archived memories: ${stats.totalArchived}`);
1943
+ if (stats.totalArchived > 0) {
1944
+ console.log(` Archive DB size: ${stats.dbSizeMB.toFixed(2)} MB`);
1945
+ console.log(` Oldest archived: ${stats.oldestArchived}`);
1946
+ console.log(` Newest archived: ${stats.newestArchived}`);
1947
+ }
1948
+ archive.close();
1949
+ }
1950
+ else {
1951
+ console.log(" Not available (better-sqlite3 not installed)");
1952
+ }
1953
+ }
1954
+ catch {
1955
+ console.log(" Not initialized");
1956
+ }
1957
+ console.log("");
1958
+ }
1959
+ // Check config — SOC routing + recall
1960
+ const cfg = stores.length > 0 ? await loadConfig(stores[0].path) : DEFAULT_CONFIG;
1961
+ console.log("Recall (Automatic Memory Injection):");
1962
+ const recallMode = cfg.recall?.aggressive !== false ? "aggressive" : "filtered";
1963
+ console.log(` Mode: ${recallMode}`);
1964
+ console.log(` Max memories per turn: ${cfg.recall?.maxMemories ?? 8}`);
1965
+ console.log(` Min relevance: ${cfg.recall?.minRelevance ?? 0.4}`);
1966
+ console.log("");
1967
+ console.log("System of Cognition (SOC):");
1968
+ console.log(` Default provider: ${cfg.llm.defaultProvider}`);
1969
+ const structuring = resolveTaskModel(cfg, "structuring");
1970
+ const synthesis = resolveTaskModel(cfg, "synthesis");
1971
+ console.log(` Structuring → ${structuring.provider}/${structuring.model}`);
1972
+ console.log(` Synthesis → ${synthesis.provider}/${synthesis.model}`);
1973
+ console.log("");
1974
+ // Check all LLM providers
1975
+ console.log("LLM Connectivity:");
1976
+ // Check Anthropic
1977
+ const anthropicStatus = isProviderAvailable(cfg, "anthropic");
1978
+ if (anthropicStatus.available) {
1979
+ try {
1980
+ const provider = getLLMProvider({ ...cfg, llm: { ...cfg.llm, defaultProvider: "anthropic" } });
1981
+ await provider.testConnection();
1982
+ console.log(` Anthropic: ✓ connected (${cfg.llm.anthropic.model})`);
1983
+ }
1984
+ catch (err) {
1985
+ console.log(` Anthropic: ✗ ${err instanceof Error ? err.message : String(err)}`);
1986
+ }
1987
+ }
1988
+ else {
1989
+ console.log(` Anthropic: — ${anthropicStatus.error}`);
1990
+ }
1991
+ // Check Ollama
1992
+ try {
1993
+ const ollamaProvider = getLLMProvider({ ...cfg, llm: { ...cfg.llm, defaultProvider: "ollama" } });
1994
+ await ollamaProvider.testConnection();
1995
+ console.log(` Ollama: ✓ connected (${cfg.llm.ollama.model} at ${cfg.llm.ollama.baseUrl})`);
1996
+ }
1997
+ catch (err) {
1998
+ console.log(` Ollama: ✗ ${err instanceof Error ? err.message : String(err)}`);
1999
+ }
2000
+ // Check Groq
2001
+ const groqStatus = isProviderAvailable(cfg, "groq");
2002
+ if (groqStatus.available) {
2003
+ try {
2004
+ const provider = getLLMProvider({ ...cfg, llm: { ...cfg.llm, defaultProvider: "groq" } });
2005
+ await provider.testConnection();
2006
+ console.log(` Groq: ✓ connected (${cfg.llm.groq.model})`);
2007
+ }
2008
+ catch (err) {
2009
+ console.log(` Groq: ✗ ${err instanceof Error ? err.message : String(err)}`);
2010
+ }
2011
+ }
2012
+ else {
2013
+ console.log(` Groq: — ${groqStatus.error}`);
2014
+ }
2015
+ // Check OpenAI
2016
+ const openaiStatus = isProviderAvailable(cfg, "openai");
2017
+ if (openaiStatus.available) {
2018
+ try {
2019
+ const provider = getLLMProvider({ ...cfg, llm: { ...cfg.llm, defaultProvider: "openai" } });
2020
+ await provider.testConnection();
2021
+ console.log(` OpenAI: ✓ connected (${cfg.llm.openai.model})`);
2022
+ }
2023
+ catch (err) {
2024
+ console.log(` OpenAI: ✗ ${err instanceof Error ? err.message : String(err)}`);
2025
+ }
2026
+ }
2027
+ else {
2028
+ console.log(` OpenAI: — ${openaiStatus.error}`);
2029
+ }
2030
+ // Check LM Studio
2031
+ try {
2032
+ const lmsProvider = getLLMProvider({ ...cfg, llm: { ...cfg.llm, defaultProvider: "lmstudio" } });
2033
+ await lmsProvider.testConnection();
2034
+ console.log(` LM Studio: ✓ connected (${cfg.llm.lmstudio.model} at ${cfg.llm.lmstudio.baseUrl})`);
2035
+ }
2036
+ catch (err) {
2037
+ console.log(` LM Studio: ✗ ${err instanceof Error ? err.message : String(err)}`);
2038
+ }
2039
+ console.log("");
2040
+ // Check embeddings
2041
+ if (stores.length > 0) {
2042
+ console.log("Embeddings:");
2043
+ const embeddings = new GnosysEmbeddings(stores[0].path);
2044
+ try {
2045
+ const stats = embeddings.getStats();
2046
+ if (stats.count > 0) {
2047
+ console.log(` Index: ${stats.count} embeddings (${stats.dbSizeMB.toFixed(1)} MB)`);
2048
+ }
2049
+ else {
2050
+ console.log(" Index: empty (run gnosys reindex to build)");
2051
+ }
2052
+ }
2053
+ catch {
2054
+ console.log(" Index: not initialized (run gnosys reindex to build)");
2055
+ }
2056
+ // Maintenance health
2057
+ console.log("");
2058
+ console.log("Maintenance Health:");
2059
+ try {
2060
+ const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
2061
+ const engine = new GnosysMaintenanceEngine(resolver, cfg);
2062
+ const health = await engine.getHealthReport();
2063
+ console.log(` Active memories: ${health.totalActive}`);
2064
+ console.log(` Stale (confidence < 0.3): ${health.staleCount}`);
2065
+ console.log(` Average confidence: ${health.avgConfidence.toFixed(3)} (decayed: ${health.avgDecayedConfidence.toFixed(3)})`);
2066
+ console.log(` Never reinforced: ${health.neverReinforced}`);
2067
+ console.log(` Total reinforcements: ${health.totalReinforcements}`);
2068
+ }
2069
+ catch (err) {
2070
+ console.log(` Error: ${err instanceof Error ? err.message : String(err)}`);
2071
+ }
2072
+ }
2073
+ });
2074
+ // ─── gnosys dream ────────────────────────────────────────────────────────
2075
+ program
2076
+ .command("dream")
2077
+ .description("Run a Dream Mode cycle — idle-time consolidation (decay, summaries, self-critique, relationships)")
2078
+ .option("--max-runtime <minutes>", "Max runtime in minutes (default: 30)")
2079
+ .option("--no-critique", "Skip self-critique phase")
2080
+ .option("--no-summaries", "Skip summary generation")
2081
+ .option("--no-relationships", "Skip relationship discovery")
2082
+ .option("--json", "Output raw JSON report")
2083
+ .action(async (opts) => {
2084
+ const resolver = new GnosysResolver();
2085
+ await resolver.resolve();
2086
+ const stores = resolver.getStores();
2087
+ if (stores.length === 0) {
2088
+ console.error("No Gnosys stores found. Run 'gnosys init' first.");
2089
+ process.exit(1);
2090
+ }
2091
+ const { GnosysDB: DbClass } = await import("./lib/db.js");
2092
+ const { GnosysDreamEngine, formatDreamReport } = await import("./lib/dream.js");
2093
+ const storePath = stores[0].path;
2094
+ const cfg = await loadConfig(storePath);
2095
+ const db = new DbClass(storePath);
2096
+ if (!db.isAvailable() || !db.isMigrated()) {
2097
+ console.error("Dream Mode requires gnosys.db (v2.0). Run 'gnosys migrate' first.");
2098
+ process.exit(1);
2099
+ }
2100
+ const dreamConfig = {
2101
+ enabled: true,
2102
+ idleMinutes: 0,
2103
+ maxRuntimeMinutes: opts.maxRuntime ? parseInt(opts.maxRuntime, 10) : 30,
2104
+ selfCritique: opts.critique !== false,
2105
+ generateSummaries: opts.summaries !== false,
2106
+ discoverRelationships: opts.relationships !== false,
2107
+ minMemories: 1,
2108
+ provider: cfg.dream?.provider || "ollama",
2109
+ model: cfg.dream?.model,
2110
+ };
2111
+ console.error("Starting Dream Mode cycle...");
2112
+ const engine = new GnosysDreamEngine(db, cfg, dreamConfig);
2113
+ const report = await engine.dream((phase, detail) => {
2114
+ console.error(` [${phase}] ${detail}`);
2115
+ });
2116
+ if (opts.json) {
2117
+ console.log(JSON.stringify(report, null, 2));
2118
+ }
2119
+ else {
2120
+ console.log(formatDreamReport(report));
2121
+ }
2122
+ db.close();
2123
+ });
2124
+ // ─── gnosys export ───────────────────────────────────────────────────────
2125
+ program
2126
+ .command("export")
2127
+ .description("Export gnosys.db to Obsidian-compatible vault (one-way)")
2128
+ .requiredOption("--to <dir>", "Target directory for export")
2129
+ .option("--all", "Export all memories (active + archived)")
2130
+ .option("--overwrite", "Overwrite existing files")
2131
+ .option("--no-summaries", "Skip category summaries")
2132
+ .option("--no-reviews", "Skip review suggestions")
2133
+ .option("--no-graph", "Skip relationship graph")
2134
+ .option("--json", "Output raw JSON report")
2135
+ .action(async (opts) => {
2136
+ const resolver = new GnosysResolver();
2137
+ await resolver.resolve();
2138
+ const stores = resolver.getStores();
2139
+ if (stores.length === 0) {
2140
+ console.error("No Gnosys stores found. Run 'gnosys init' first.");
2141
+ process.exit(1);
2142
+ }
2143
+ const { GnosysDB: DbClass } = await import("./lib/db.js");
2144
+ const { GnosysExporter, formatExportReport } = await import("./lib/export.js");
2145
+ const storePath = stores[0].path;
2146
+ const db = new DbClass(storePath);
2147
+ if (!db.isAvailable() || !db.isMigrated()) {
2148
+ console.error("Export requires gnosys.db (v2.0). Run 'gnosys migrate' first.");
2149
+ process.exit(1);
2150
+ }
2151
+ const targetDir = path.resolve(opts.to);
2152
+ console.error(`Exporting to: ${targetDir}`);
2153
+ const exporter = new GnosysExporter(db);
2154
+ const report = await exporter.export({
2155
+ targetDir,
2156
+ activeOnly: !opts.all,
2157
+ includeSummaries: opts.summaries !== false,
2158
+ includeReviews: opts.reviews !== false,
2159
+ includeGraph: opts.graph !== false,
2160
+ overwrite: opts.overwrite || false,
2161
+ onProgress: (current, total, file) => {
2162
+ if (current % 10 === 0 || current === total) {
2163
+ console.error(` [${current}/${total}] ${file}`);
2164
+ }
2165
+ },
2166
+ });
2167
+ if (opts.json) {
2168
+ console.log(JSON.stringify(report, null, 2));
2169
+ }
2170
+ else {
2171
+ console.log(formatExportReport(report));
2172
+ }
2173
+ db.close();
2174
+ });
2175
+ // ─── gnosys serve ────────────────────────────────────────────────────────
2176
+ program
2177
+ .command("serve")
2178
+ .description("Start the MCP server (stdio mode)")
2179
+ .option("--with-maintenance", "Run maintenance every 6 hours in background")
2180
+ .action(async (opts) => {
2181
+ if (opts.withMaintenance) {
2182
+ // Start background maintenance loop
2183
+ const SIX_HOURS = 6 * 60 * 60 * 1000;
2184
+ const runMaintenance = async () => {
2185
+ try {
2186
+ const { GnosysMaintenanceEngine } = await import("./lib/maintenance.js");
2187
+ const resolver = new (await import("./lib/resolver.js")).GnosysResolver();
2188
+ await resolver.resolve();
2189
+ const stores = resolver.getStores();
2190
+ if (stores.length > 0) {
2191
+ const cfg = await loadConfig(stores[0].path);
2192
+ const engine = new GnosysMaintenanceEngine(resolver, cfg);
2193
+ const report = await engine.maintain({ autoApply: true });
2194
+ console.error(`[maintenance] Completed: ${report.actions.length} action(s), ${report.duplicates.length} duplicate(s), ${report.staleMemories.length} stale`);
2195
+ }
2196
+ }
2197
+ catch (err) {
2198
+ console.error(`[maintenance] Error: ${err instanceof Error ? err.message : String(err)}`);
2199
+ }
2200
+ };
2201
+ // Run immediately on start, then every 6 hours
2202
+ setTimeout(runMaintenance, 30000); // 30s after server start
2203
+ setInterval(runMaintenance, SIX_HOURS);
2204
+ console.error("[maintenance] Background maintenance enabled (every 6 hours)");
2205
+ }
2206
+ await import("./index.js");
2207
+ });
2208
+ // ─── gnosys recall ───────────────────────────────────────────────────────
2209
+ program
2210
+ .command("recall <query>")
2211
+ .description("Always-on memory recall — injects most relevant memories as context. Use --federated for cross-scope.")
2212
+ .option("--limit <n>", "Max memories to return (default from config)")
2213
+ .option("--aggressive", "Force aggressive mode (inject even medium-relevance memories)")
2214
+ .option("--no-aggressive", "Force filtered mode (hard cutoff at minRelevance)")
2215
+ .option("--trace-id <id>", "Trace ID for audit correlation")
2216
+ .option("--json", "Output raw JSON instead of formatted text")
2217
+ .option("--host", "Output in host-friendly <gnosys-recall> format (default for MCP)")
2218
+ .option("--federated", "Use federated search with tier boosting (project > user > global)")
2219
+ .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated)")
2220
+ .option("-d, --directory <dir>", "Project directory for context")
2221
+ .action(async (query, opts) => {
2222
+ // Federated recall path — returns tier-boosted results from central DB
2223
+ if (opts.federated || opts.scope) {
2224
+ let centralDb = null;
2225
+ try {
2226
+ centralDb = GnosysDB.openCentral();
2227
+ if (!centralDb.isAvailable()) {
2228
+ console.error("Central DB not available.");
2229
+ process.exit(1);
2230
+ }
2231
+ const { federatedSearch, detectCurrentProject } = await import("./lib/federated.js");
2232
+ const projectId = await detectCurrentProject(centralDb, opts.directory || undefined);
2233
+ const scopeFilter = opts.scope ? opts.scope.split(",").map(s => s.trim()) : undefined;
2234
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 10;
2235
+ const results = federatedSearch(centralDb, query, { limit, projectId, scopeFilter });
2236
+ // Format as recall-like output with scope info
2237
+ const recallResult = {
2238
+ query,
2239
+ projectId,
2240
+ mode: "federated",
2241
+ count: results.length,
2242
+ memories: results.map(r => ({
2243
+ id: r.id,
2244
+ title: r.title,
2245
+ category: r.category,
2246
+ scope: r.scope,
2247
+ score: r.score,
2248
+ boosts: r.boosts,
2249
+ snippet: r.snippet,
2250
+ projectName: r.projectName,
2251
+ })),
2252
+ };
2253
+ if (opts.json) {
2254
+ console.log(JSON.stringify(recallResult, null, 2));
2255
+ }
2256
+ else if (opts.host) {
2257
+ const lines = [`<gnosys-recall query="${query}" mode="federated" count="${results.length}">`];
2258
+ for (const r of results) {
2259
+ lines.push(` <memory id="${r.id}" scope="${r.scope}" score="${r.score.toFixed(4)}">`);
2260
+ lines.push(` ${r.title}: ${r.snippet?.substring(0, 200) || ""}`);
2261
+ lines.push(` </memory>`);
2262
+ }
2263
+ lines.push(`</gnosys-recall>`);
2264
+ console.log(lines.join("\n"));
2265
+ }
2266
+ else {
2267
+ if (results.length === 0) {
2268
+ console.log(`No memories found for "${query}".`);
2269
+ }
2270
+ else {
2271
+ console.log(`Recall: ${results.length} memories for "${query}" (federated)\n`);
2272
+ for (const r of results) {
2273
+ const proj = r.projectName ? ` [${r.projectName}]` : "";
2274
+ console.log(` ${r.title}${proj} (${r.scope}, ${r.score.toFixed(4)})`);
2275
+ if (r.snippet)
2276
+ console.log(` ${r.snippet.substring(0, 100)}`);
2277
+ }
2278
+ }
2279
+ }
2280
+ }
2281
+ catch (err) {
2282
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2283
+ process.exit(1);
2284
+ }
2285
+ finally {
2286
+ centralDb?.close();
2287
+ }
2288
+ return;
2289
+ }
2290
+ // Legacy file-based recall
2291
+ const resolver = new GnosysResolver();
2292
+ await resolver.resolve();
2293
+ const stores = resolver.getStores();
2294
+ if (stores.length === 0) {
2295
+ console.error("No Gnosys stores found. Run 'gnosys init' first.");
2296
+ process.exit(1);
2297
+ }
2298
+ const { recall, formatRecall, formatRecallCLI } = await import("./lib/recall.js");
2299
+ const { initAudit, closeAudit } = await import("./lib/audit.js");
2300
+ const storePath = stores[0].path;
2301
+ initAudit(storePath);
2302
+ // Load config for recall settings
2303
+ const cfg = await loadConfig(storePath);
2304
+ const recallConfig = {
2305
+ ...cfg.recall,
2306
+ ...(opts.aggressive !== undefined ? { aggressive: opts.aggressive } : {}),
2307
+ };
2308
+ // Build search index
2309
+ const search = new GnosysSearch(storePath);
2310
+ await search.addStoreMemories(stores[0].store);
2311
+ const result = await recall(query, {
2312
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
2313
+ search,
2314
+ resolver,
2315
+ storePath,
2316
+ traceId: opts.traceId,
2317
+ recallConfig,
2318
+ });
2319
+ if (opts.json) {
2320
+ console.log(JSON.stringify(result, null, 2));
2321
+ }
2322
+ else if (opts.host) {
2323
+ console.log(formatRecall(result));
2324
+ }
2325
+ else {
2326
+ console.log(formatRecallCLI(result));
2327
+ }
2328
+ closeAudit();
2329
+ });
2330
+ // ─── gnosys audit ────────────────────────────────────────────────────────
2331
+ program
2332
+ .command("audit")
2333
+ .description("View the structured audit trail of memory operations")
2334
+ .option("--days <n>", "Show entries from the last N days", "7")
2335
+ .option("--operation <op>", "Filter by operation type (read, write, recall, etc.)")
2336
+ .option("--limit <n>", "Max entries to show")
2337
+ .option("--json", "Output raw JSON instead of formatted timeline")
2338
+ .action(async (opts) => {
2339
+ const resolver = new GnosysResolver();
2340
+ await resolver.resolve();
2341
+ const stores = resolver.getStores();
2342
+ if (stores.length === 0) {
2343
+ console.error("No Gnosys stores found. Run 'gnosys init' first.");
2344
+ process.exit(1);
2345
+ }
2346
+ const { readAuditLog, formatAuditTimeline } = await import("./lib/audit.js");
2347
+ const storePath = stores[0].path;
2348
+ const entries = readAuditLog(storePath, {
2349
+ days: parseInt(opts.days, 10),
2350
+ operation: opts.operation,
2351
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
2352
+ });
2353
+ if (opts.json) {
2354
+ console.log(JSON.stringify(entries, null, 2));
2355
+ }
2356
+ else {
2357
+ console.log(formatAuditTimeline(entries));
2358
+ }
2359
+ });
2360
+ // ─── gnosys backup ──────────────────────────────────────────────────────
2361
+ program
2362
+ .command("backup")
2363
+ .description("Create a backup of the central Gnosys database and config")
2364
+ .option("-o, --output <dir>", "Backup output directory (default: ~/.gnosys/)")
2365
+ .option("--to <dir>", "Alias for --output")
2366
+ .option("--json", "Output as JSON")
2367
+ .action(async (opts) => {
2368
+ let centralDb = null;
2369
+ try {
2370
+ centralDb = GnosysDB.openCentral();
2371
+ if (!centralDb.isAvailable()) {
2372
+ console.error("Central DB not available (better-sqlite3 missing).");
2373
+ process.exit(1);
2374
+ }
2375
+ const outputDir = opts.to || opts.output;
2376
+ const backupPath = await centralDb.backup(outputDir);
2377
+ const counts = centralDb.getMemoryCount();
2378
+ const projectCount = centralDb.getAllProjects().length;
2379
+ // Copy sandbox log if it exists
2380
+ const centralDir = GnosysDB.getCentralDbDir();
2381
+ const copiedFiles = [backupPath];
2382
+ const backupDir = path.dirname(backupPath);
2383
+ const sandboxLog = path.join(centralDir, "sandbox", "sandbox.log");
2384
+ if (existsSync(sandboxLog)) {
2385
+ const logDest = path.join(backupDir, "sandbox.log.bak");
2386
+ copyFileSync(sandboxLog, logDest);
2387
+ copiedFiles.push(logDest);
2388
+ }
2389
+ if (opts.json) {
2390
+ console.log(JSON.stringify({
2391
+ ok: true, backupPath, memories: counts.total,
2392
+ active: counts.active, archived: counts.archived,
2393
+ projects: projectCount, files: copiedFiles,
2394
+ }));
2395
+ }
2396
+ else {
2397
+ console.log(`Backup created: ${backupPath}`);
2398
+ console.log(` Memories: ${counts.total} (${counts.active} active, ${counts.archived} archived)`);
2399
+ console.log(` Projects: ${projectCount}`);
2400
+ if (copiedFiles.length > 1)
2401
+ console.log(` Additional files: ${copiedFiles.length - 1}`);
2402
+ }
2403
+ }
2404
+ catch (err) {
2405
+ if (opts.json) {
2406
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
2407
+ }
2408
+ else {
2409
+ console.error(`Backup failed: ${err instanceof Error ? err.message : err}`);
2410
+ }
2411
+ process.exit(1);
2412
+ }
2413
+ finally {
2414
+ centralDb?.close();
2415
+ }
2416
+ });
2417
+ // ─── gnosys restore ─────────────────────────────────────────────────────
2418
+ program
2419
+ .command("restore <backupFile>")
2420
+ .description("Restore the central Gnosys database from a backup")
2421
+ .option("--from <file>", "Alias: backup file to restore from")
2422
+ .option("--json", "Output as JSON")
2423
+ .action(async (backupFile, opts) => {
2424
+ const resolved = path.resolve(opts.from || backupFile);
2425
+ try {
2426
+ const db = GnosysDB.restore(resolved);
2427
+ const counts = db.getMemoryCount();
2428
+ const projectCount = db.getAllProjects().length;
2429
+ if (opts.json) {
2430
+ console.log(JSON.stringify({
2431
+ ok: true, source: resolved, memories: counts.total,
2432
+ active: counts.active, archived: counts.archived, projects: projectCount,
2433
+ }));
2434
+ }
2435
+ else {
2436
+ console.log(`Database restored from ${resolved}`);
2437
+ console.log(` Memories: ${counts.total} (${counts.active} active, ${counts.archived} archived)`);
2438
+ console.log(` Projects: ${projectCount}`);
2439
+ }
2440
+ db.close();
2441
+ }
2442
+ catch (err) {
2443
+ if (opts.json) {
2444
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
2445
+ }
2446
+ else {
2447
+ console.error(`Restore failed: ${err instanceof Error ? err.message : err}`);
2448
+ }
2449
+ process.exit(1);
2450
+ }
2451
+ });
2452
+ // ─── gnosys migrate --to-central ────────────────────────────────────────
2453
+ program
2454
+ .command("migrate")
2455
+ .description("Migrate data. Use --to-central to move per-project stores into the central DB.")
2456
+ .option("--to-central", "Migrate all discovered per-project stores into ~/.gnosys/gnosys.db")
2457
+ .option("-v, --verbose", "Verbose output")
2458
+ .action(async (opts) => {
2459
+ if (!opts.toCentral) {
2460
+ // Legacy v1→v2 migration (existing behavior)
2461
+ const resolver = await getResolver();
2462
+ const writeTarget = resolver.getWriteTarget();
2463
+ if (!writeTarget) {
2464
+ console.error("No writable store found. Run 'gnosys init' first.");
2465
+ process.exit(1);
2466
+ }
2467
+ const stats = await migrate(writeTarget.store.getStorePath(), { verbose: opts.verbose });
2468
+ console.log(formatMigrationReport(stats));
2469
+ return;
2470
+ }
2471
+ // v3.0: Migrate per-project stores into central DB
2472
+ console.log("Migrating per-project stores to central DB (~/.gnosys/gnosys.db)...\n");
2473
+ let centralDb = null;
2474
+ try {
2475
+ centralDb = GnosysDB.openCentral();
2476
+ if (!centralDb.isAvailable()) {
2477
+ console.error("Central DB not available (better-sqlite3 missing).");
2478
+ process.exit(1);
2479
+ }
2480
+ }
2481
+ catch (err) {
2482
+ console.error(`Cannot open central DB: ${err instanceof Error ? err.message : err}`);
2483
+ process.exit(1);
2484
+ }
2485
+ // Discover all registered project stores
2486
+ const resolver = await getResolver();
2487
+ const detectedStores = await resolver.detectAllStores();
2488
+ const projectDirs = detectedStores
2489
+ .filter(s => s.hasGnosys)
2490
+ .map(s => s.path);
2491
+ if (projectDirs.length === 0) {
2492
+ console.log("No per-project stores found to migrate.");
2493
+ centralDb.close();
2494
+ return;
2495
+ }
2496
+ console.log(`Found ${projectDirs.length} project store(s) to migrate:\n`);
2497
+ let totalMemories = 0;
2498
+ let totalProjects = 0;
2499
+ for (const projectDir of projectDirs) {
2500
+ const storePath = path.join(projectDir, ".gnosys");
2501
+ const log = opts.verbose ? console.log : () => { };
2502
+ try {
2503
+ // Create project identity if it doesn't exist
2504
+ const identity = await createProjectIdentity(projectDir, {
2505
+ centralDb: centralDb,
2506
+ });
2507
+ log(` [${identity.projectName}] ID: ${identity.projectId}`);
2508
+ // Open per-project DB and import memories
2509
+ const projectDb = new GnosysDB(storePath);
2510
+ if (!projectDb.isAvailable() || !projectDb.isMigrated()) {
2511
+ log(` [${identity.projectName}] No migrated gnosys.db — skipping`);
2512
+ continue;
2513
+ }
2514
+ const memories = projectDb.getAllMemories();
2515
+ let count = 0;
2516
+ centralDb.transaction(() => {
2517
+ for (const mem of memories) {
2518
+ centralDb.insertMemory({
2519
+ ...mem,
2520
+ project_id: identity.projectId,
2521
+ scope: "project",
2522
+ });
2523
+ count++;
2524
+ }
2525
+ });
2526
+ projectDb.close();
2527
+ totalMemories += count;
2528
+ totalProjects++;
2529
+ console.log(` ✓ ${identity.projectName}: ${count} memories migrated`);
2530
+ }
2531
+ catch (err) {
2532
+ console.error(` ✗ ${projectDir}: ${err instanceof Error ? err.message : err}`);
2533
+ }
2534
+ }
2535
+ centralDb.close();
2536
+ console.log(`\n╔════════════════════════════════════════╗`);
2537
+ console.log(`║ Central Migration Complete ║`);
2538
+ console.log(`╚════════════════════════════════════════╝`);
2539
+ console.log(` Projects migrated: ${totalProjects}`);
2540
+ console.log(` Memories imported: ${totalMemories}`);
2541
+ console.log(`\n Per-project gnosys.db files are untouched.`);
2542
+ console.log(` Central DB: ${GnosysDB.getCentralDbPath()}`);
2543
+ });
2544
+ // ─── gnosys projects ────────────────────────────────────────────────────
2545
+ program
2546
+ .command("projects")
2547
+ .description("List all registered projects in the central DB")
2548
+ .option("--json", "Output as JSON")
2549
+ .action(async (opts) => {
2550
+ let centralDb = null;
2551
+ try {
2552
+ centralDb = GnosysDB.openCentral();
2553
+ if (!centralDb.isAvailable()) {
2554
+ console.error("Central DB not available (better-sqlite3 missing).");
2555
+ process.exit(1);
2556
+ }
2557
+ const projects = centralDb.getAllProjects();
2558
+ if (projects.length === 0) {
2559
+ console.log("No projects registered. Run 'gnosys init' in a project directory.");
2560
+ centralDb.close();
2561
+ return;
2562
+ }
2563
+ const projectData = projects.map((p) => ({
2564
+ ...p,
2565
+ memoryCount: centralDb.getMemoriesByProject(p.id).length,
2566
+ }));
2567
+ outputResult(!!opts.json, { count: projects.length, projects: projectData }, () => {
2568
+ console.log(`${projects.length} registered project(s):\n`);
2569
+ for (const p of projectData) {
2570
+ console.log(` ${p.name}`);
2571
+ console.log(` ID: ${p.id}`);
2572
+ console.log(` Directory: ${p.working_directory}`);
2573
+ console.log(` Memories: ${p.memoryCount}`);
2574
+ console.log(` Created: ${p.created}`);
2575
+ console.log();
2576
+ }
2577
+ });
2578
+ }
2579
+ catch (err) {
2580
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2581
+ process.exit(1);
2582
+ }
2583
+ finally {
2584
+ centralDb?.close();
2585
+ }
2586
+ });
2587
+ // ─── gnosys pref ─────────────────────────────────────────────────────────
2588
+ const prefCmd = program
2589
+ .command("pref")
2590
+ .description("Manage user preferences (stored in central DB, scope='user')");
2591
+ prefCmd
2592
+ .command("set <key> <value>")
2593
+ .description("Set a user preference. Key should be kebab-case (e.g. 'commit-convention').")
2594
+ .option("-t, --title <title>", "Human-readable title")
2595
+ .option("--tags <tags>", "Comma-separated tags")
2596
+ .action(async (key, value, opts) => {
2597
+ let centralDb = null;
2598
+ try {
2599
+ centralDb = GnosysDB.openCentral();
2600
+ if (!centralDb.isAvailable()) {
2601
+ console.error("Central DB not available (better-sqlite3 missing).");
2602
+ process.exit(1);
2603
+ }
2604
+ const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined;
2605
+ const pref = setPreference(centralDb, key, value, { title: opts.title, tags });
2606
+ console.log(`Preference set: ${pref.title}`);
2607
+ console.log(` Key: ${pref.key}`);
2608
+ console.log(` Value: ${pref.value}`);
2609
+ console.log(`\nRun 'gnosys sync' to update agent rules files.`);
2610
+ }
2611
+ catch (err) {
2612
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2613
+ process.exit(1);
2614
+ }
2615
+ finally {
2616
+ centralDb?.close();
2617
+ }
2618
+ });
2619
+ prefCmd
2620
+ .command("get [key]")
2621
+ .description("Get a preference by key, or list all preferences if no key given.")
2622
+ .option("--json", "Output as JSON")
2623
+ .action(async (key, opts) => {
2624
+ let centralDb = null;
2625
+ try {
2626
+ centralDb = GnosysDB.openCentral();
2627
+ if (!centralDb.isAvailable()) {
2628
+ console.error("Central DB not available (better-sqlite3 missing).");
2629
+ process.exit(1);
2630
+ }
2631
+ if (key) {
2632
+ const pref = getPreference(centralDb, key);
2633
+ if (!pref) {
2634
+ console.log(`No preference found for key "${key}".`);
2635
+ return;
2636
+ }
2637
+ outputResult(!!opts.json, pref, () => {
2638
+ console.log(`${pref.title} (${pref.key})\n`);
2639
+ console.log(pref.value);
2640
+ console.log(`\nConfidence: ${pref.confidence}`);
2641
+ console.log(`Modified: ${pref.modified}`);
2642
+ });
2643
+ }
2644
+ else {
2645
+ const prefs = getAllPreferences(centralDb);
2646
+ if (prefs.length === 0) {
2647
+ outputResult(!!opts.json, { preferences: [] }, () => {
2648
+ console.log("No preferences set. Use 'gnosys pref set <key> <value>' to add some.");
2649
+ });
2650
+ return;
2651
+ }
2652
+ outputResult(!!opts.json, { count: prefs.length, preferences: prefs }, () => {
2653
+ console.log(`${prefs.length} user preference(s):\n`);
2654
+ for (const p of prefs) {
2655
+ console.log(` ${p.title} (${p.key})`);
2656
+ console.log(` ${p.value.split("\n")[0]}`);
2657
+ console.log();
2658
+ }
2659
+ });
2660
+ }
2661
+ }
2662
+ catch (err) {
2663
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2664
+ process.exit(1);
2665
+ }
2666
+ finally {
2667
+ centralDb?.close();
2668
+ }
2669
+ });
2670
+ prefCmd
2671
+ .command("delete <key>")
2672
+ .description("Delete a user preference.")
2673
+ .action(async (key) => {
2674
+ let centralDb = null;
2675
+ try {
2676
+ centralDb = GnosysDB.openCentral();
2677
+ if (!centralDb.isAvailable()) {
2678
+ console.error("Central DB not available (better-sqlite3 missing).");
2679
+ process.exit(1);
2680
+ }
2681
+ const deleted = deletePreference(centralDb, key);
2682
+ if (!deleted) {
2683
+ console.log(`No preference found for key "${key}".`);
2684
+ return;
2685
+ }
2686
+ console.log(`Preference "${key}" deleted.`);
2687
+ console.log(`Run 'gnosys sync' to update agent rules files.`);
2688
+ }
2689
+ catch (err) {
2690
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2691
+ process.exit(1);
2692
+ }
2693
+ finally {
2694
+ centralDb?.close();
2695
+ }
2696
+ });
2697
+ // ─── gnosys sync ─────────────────────────────────────────────────────────
2698
+ program
2699
+ .command("sync")
2700
+ .description("Regenerate agent rules file from user preferences and project conventions. Injects GNOSYS:START/GNOSYS:END block.")
2701
+ .option("-d, --directory <dir>", "Project directory (default: cwd)")
2702
+ .action(async (opts) => {
2703
+ const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
2704
+ let centralDb = null;
2705
+ try {
2706
+ centralDb = GnosysDB.openCentral();
2707
+ if (!centralDb.isAvailable()) {
2708
+ console.error("Central DB not available (better-sqlite3 missing).");
2709
+ process.exit(1);
2710
+ }
2711
+ // Read project identity
2712
+ const identity = await readProjectIdentity(projectDir);
2713
+ if (!identity) {
2714
+ console.error("No project identity found. Run 'gnosys init' first.");
2715
+ process.exit(1);
2716
+ }
2717
+ if (!identity.agentRulesTarget) {
2718
+ console.error("No agent rules target detected (no .cursor/ or CLAUDE.md found).");
2719
+ console.error("Create one of these, then run 'gnosys init' to detect it.");
2720
+ process.exit(1);
2721
+ }
2722
+ const result = await syncRules(centralDb, projectDir, identity.agentRulesTarget, identity.projectId);
2723
+ if (!result) {
2724
+ console.error("Sync failed.");
2725
+ process.exit(1);
2726
+ }
2727
+ const action = result.created ? "Created" : "Updated";
2728
+ console.log(`${action} rules file: ${result.filePath}`);
2729
+ console.log(` Preferences injected: ${result.prefCount}`);
2730
+ console.log(` Project conventions: ${result.conventionCount}`);
2731
+ console.log(`\nContent is inside <!-- GNOSYS:START --> / <!-- GNOSYS:END --> markers.`);
2732
+ console.log(`User content outside these markers is preserved.`);
2733
+ }
2734
+ catch (err) {
2735
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2736
+ process.exit(1);
2737
+ }
2738
+ finally {
2739
+ centralDb?.close();
2740
+ }
2741
+ });
2742
+ // ─── gnosys fsearch (federated search) ───────────────────────────────────
2743
+ program
2744
+ .command("fsearch <query>")
2745
+ .description("Federated search across all scopes with tier boosting (project > user > global)")
2746
+ .option("-l, --limit <n>", "Max results", "20")
2747
+ .option("-d, --directory <dir>", "Project directory for context")
2748
+ .option("--no-global", "Exclude global-scope memories")
2749
+ .option("--scope <scope>", "Filter by scope: project, user, global (comma-separated)")
2750
+ .option("--json", "Output as JSON")
2751
+ .action(async (query, opts) => {
2752
+ let centralDb = null;
2753
+ try {
2754
+ centralDb = GnosysDB.openCentral();
2755
+ if (!centralDb.isAvailable()) {
2756
+ console.error("Central DB not available.");
2757
+ process.exit(1);
2758
+ }
2759
+ const { federatedSearch, detectCurrentProject } = await import("./lib/federated.js");
2760
+ const projectId = await detectCurrentProject(centralDb, opts.directory || undefined);
2761
+ const scopeFilter = opts.scope ? opts.scope.split(",").map(s => s.trim()) : undefined;
2762
+ const results = federatedSearch(centralDb, query, {
2763
+ limit: parseInt(opts.limit, 10),
2764
+ projectId,
2765
+ includeGlobal: opts.global,
2766
+ scopeFilter,
2767
+ });
2768
+ if (opts.json) {
2769
+ console.log(JSON.stringify({ query, projectId, count: results.length, results }, null, 2));
2770
+ }
2771
+ else {
2772
+ if (results.length === 0) {
2773
+ console.log(`No results for "${query}".`);
2774
+ return;
2775
+ }
2776
+ const ctx = projectId ? `Context: project ${projectId}` : "No project detected";
2777
+ console.log(ctx);
2778
+ for (const [i, r] of results.entries()) {
2779
+ const proj = r.projectName ? ` [${r.projectName}]` : "";
2780
+ console.log(`\n${i + 1}. ${r.title} (${r.category})${proj}`);
2781
+ console.log(` scope: ${r.scope} | score: ${r.score.toFixed(4)} | boosts: ${r.boosts.join(", ")}`);
2782
+ if (r.snippet)
2783
+ console.log(` ${r.snippet.substring(0, 120)}`);
2784
+ }
2785
+ }
2786
+ }
2787
+ catch (err) {
2788
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2789
+ process.exit(1);
2790
+ }
2791
+ finally {
2792
+ centralDb?.close();
2793
+ }
2794
+ });
2795
+ // ─── gnosys ambiguity ────────────────────────────────────────────────────
2796
+ program
2797
+ .command("ambiguity <query>")
2798
+ .description("Check if a query matches memories in multiple projects")
2799
+ .option("--json", "Output as JSON")
2800
+ .action(async (query, opts) => {
2801
+ let centralDb = null;
2802
+ try {
2803
+ centralDb = GnosysDB.openCentral();
2804
+ if (!centralDb.isAvailable()) {
2805
+ console.error("Central DB not available.");
2806
+ process.exit(1);
2807
+ }
2808
+ const { detectAmbiguity } = await import("./lib/federated.js");
2809
+ const ambiguity = detectAmbiguity(centralDb, query);
2810
+ if (opts.json) {
2811
+ console.log(JSON.stringify({ query, ambiguous: !!ambiguity, ...(ambiguity || {}) }, null, 2));
2812
+ }
2813
+ else if (!ambiguity) {
2814
+ console.log(`No ambiguity for "${query}" — matches at most one project.`);
2815
+ }
2816
+ else {
2817
+ console.log(ambiguity.message);
2818
+ for (const c of ambiguity.candidates) {
2819
+ console.log(`\n ${c.projectName} (${c.projectId})`);
2820
+ console.log(` Dir: ${c.workingDirectory}`);
2821
+ console.log(` Matching memories: ${c.memoryCount}`);
2822
+ }
2823
+ }
2824
+ }
2825
+ catch (err) {
2826
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2827
+ process.exit(1);
2828
+ }
2829
+ finally {
2830
+ centralDb?.close();
2831
+ }
2832
+ });
2833
+ // ─── gnosys briefing ─────────────────────────────────────────────────────
2834
+ program
2835
+ .command("briefing")
2836
+ .description("Generate project briefing — memory state summary, categories, recent activity, top tags")
2837
+ .option("-p, --project <id>", "Project ID (auto-detects if omitted)")
2838
+ .option("-a, --all", "Generate briefings for all projects")
2839
+ .option("-d, --directory <dir>", "Project directory for auto-detection")
2840
+ .option("--json", "Output as JSON")
2841
+ .action(async (opts) => {
2842
+ let centralDb = null;
2843
+ try {
2844
+ centralDb = GnosysDB.openCentral();
2845
+ if (!centralDb.isAvailable()) {
2846
+ console.error("Central DB not available.");
2847
+ process.exit(1);
2848
+ }
2849
+ const { generateBriefing, generateAllBriefings, detectCurrentProject } = await import("./lib/federated.js");
2850
+ if (opts.all) {
2851
+ const briefings = generateAllBriefings(centralDb);
2852
+ if (opts.json) {
2853
+ console.log(JSON.stringify({ count: briefings.length, briefings }, null, 2));
2854
+ }
2855
+ else {
2856
+ if (briefings.length === 0) {
2857
+ console.log("No projects registered.");
2858
+ return;
2859
+ }
2860
+ for (const b of briefings) {
2861
+ console.log(`\n## ${b.projectName}`);
2862
+ console.log(b.summary);
2863
+ }
2864
+ }
2865
+ return;
2866
+ }
2867
+ let pid = opts.project || null;
2868
+ if (!pid)
2869
+ pid = await detectCurrentProject(centralDb, opts.directory || undefined);
2870
+ if (!pid) {
2871
+ console.error("No project specified and none detected.");
2872
+ process.exit(1);
2873
+ }
2874
+ const briefing = generateBriefing(centralDb, pid);
2875
+ if (!briefing) {
2876
+ console.error(`Project not found: ${pid}`);
2877
+ process.exit(1);
2878
+ }
2879
+ if (opts.json) {
2880
+ console.log(JSON.stringify(briefing, null, 2));
2881
+ }
2882
+ else {
2883
+ console.log(`# Briefing: ${briefing.projectName}`);
2884
+ console.log(`Directory: ${briefing.workingDirectory}`);
2885
+ console.log(`Active memories: ${briefing.activeMemories} / ${briefing.totalMemories}`);
2886
+ console.log(`\nCategories:`);
2887
+ for (const [cat, count] of Object.entries(briefing.categories).sort((a, b) => b[1] - a[1])) {
2888
+ console.log(` ${cat}: ${count}`);
2889
+ }
2890
+ console.log(`\nRecent activity (7d):`);
2891
+ if (briefing.recentActivity.length === 0) {
2892
+ console.log(" None");
2893
+ }
2894
+ for (const r of briefing.recentActivity) {
2895
+ console.log(` - ${r.title} (${r.modified})`);
2896
+ }
2897
+ console.log(`\nTop tags: ${briefing.topTags.slice(0, 10).map((t) => `${t.tag}(${t.count})`).join(", ") || "None"}`);
2898
+ console.log(`\n${briefing.summary}`);
2899
+ }
2900
+ }
2901
+ catch (err) {
2902
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2903
+ process.exit(1);
2904
+ }
2905
+ finally {
2906
+ centralDb?.close();
2907
+ }
2908
+ });
2909
+ // ─── gnosys working-set ──────────────────────────────────────────────────
2910
+ program
2911
+ .command("working-set")
2912
+ .description("Show the implicit working set — recently modified memories for the current project")
2913
+ .option("-d, --directory <dir>", "Project directory")
2914
+ .option("-w, --window <hours>", "Lookback window in hours", "24")
2915
+ .option("--json", "Output as JSON")
2916
+ .action(async (opts) => {
2917
+ let centralDb = null;
2918
+ try {
2919
+ centralDb = GnosysDB.openCentral();
2920
+ if (!centralDb.isAvailable()) {
2921
+ console.error("Central DB not available.");
2922
+ process.exit(1);
2923
+ }
2924
+ const { getWorkingSet, formatWorkingSet, detectCurrentProject } = await import("./lib/federated.js");
2925
+ const pid = await detectCurrentProject(centralDb, opts.directory || undefined);
2926
+ if (!pid) {
2927
+ console.error("No project detected.");
2928
+ process.exit(1);
2929
+ }
2930
+ const windowHours = parseInt(opts.window, 10);
2931
+ const workingSet = getWorkingSet(centralDb, pid, { windowHours });
2932
+ if (opts.json) {
2933
+ console.log(JSON.stringify({
2934
+ projectId: pid,
2935
+ windowHours,
2936
+ count: workingSet.length,
2937
+ memories: workingSet.map((m) => ({ id: m.id, title: m.title, category: m.category, modified: m.modified })),
2938
+ }, null, 2));
2939
+ }
2940
+ else {
2941
+ console.log(formatWorkingSet(workingSet));
2942
+ }
2943
+ }
2944
+ catch (err) {
2945
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2946
+ process.exit(1);
2947
+ }
2948
+ finally {
2949
+ centralDb?.close();
2950
+ }
2951
+ });
2952
+ // ─── gnosys sandbox start|stop|status ─────────────────────────────────────
2953
+ const sandboxCmd = program
2954
+ .command("sandbox")
2955
+ .description("Manage the Gnosys sandbox background process");
2956
+ sandboxCmd
2957
+ .command("start")
2958
+ .description("Start the Gnosys sandbox background process")
2959
+ .option("--persistent", "Keep running across reboots (future use)")
2960
+ .option("--db-path <path>", "Custom database directory")
2961
+ .option("--json", "Output as JSON")
2962
+ .action(async (opts) => {
2963
+ try {
2964
+ const { startSandbox } = await import("./sandbox/manager.js");
2965
+ const pid = await startSandbox({
2966
+ persistent: opts.persistent,
2967
+ dbPath: opts.dbPath,
2968
+ wait: true,
2969
+ });
2970
+ if (opts.json) {
2971
+ console.log(JSON.stringify({ ok: true, pid }));
2972
+ }
2973
+ else {
2974
+ console.log(`Gnosys sandbox running (pid: ${pid})`);
2975
+ }
2976
+ }
2977
+ catch (err) {
2978
+ if (opts.json) {
2979
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
2980
+ }
2981
+ else {
2982
+ console.error(`Failed to start sandbox: ${err instanceof Error ? err.message : err}`);
2983
+ }
2984
+ process.exit(1);
2985
+ }
2986
+ });
2987
+ sandboxCmd
2988
+ .command("stop")
2989
+ .description("Stop the Gnosys sandbox background process")
2990
+ .option("--json", "Output as JSON")
2991
+ .action(async (opts) => {
2992
+ try {
2993
+ const { stopSandbox } = await import("./sandbox/manager.js");
2994
+ const wasRunning = await stopSandbox();
2995
+ if (opts.json) {
2996
+ console.log(JSON.stringify({ ok: true, wasRunning }));
2997
+ }
2998
+ else {
2999
+ console.log(wasRunning ? "Sandbox stopped." : "Sandbox was not running.");
3000
+ }
3001
+ }
3002
+ catch (err) {
3003
+ if (opts.json) {
3004
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3005
+ }
3006
+ else {
3007
+ console.error(`Failed to stop sandbox: ${err instanceof Error ? err.message : err}`);
3008
+ }
3009
+ process.exit(1);
3010
+ }
3011
+ });
3012
+ sandboxCmd
3013
+ .command("status")
3014
+ .description("Check if the Gnosys sandbox is running")
3015
+ .option("--json", "Output as JSON")
3016
+ .action(async (opts) => {
3017
+ try {
3018
+ const { sandboxStatus } = await import("./sandbox/manager.js");
3019
+ const status = await sandboxStatus();
3020
+ if (opts.json) {
3021
+ console.log(JSON.stringify(status, null, 2));
3022
+ }
3023
+ else if (status.running) {
3024
+ console.log(`Sandbox running (pid: ${status.pid}, socket: ${status.socketPath})`);
3025
+ }
3026
+ else {
3027
+ console.log("Sandbox is not running. Start with: gnosys sandbox start");
3028
+ }
3029
+ }
3030
+ catch (err) {
3031
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
3032
+ process.exit(1);
3033
+ }
3034
+ });
3035
+ // ─── gnosys helper generate ───────────────────────────────────────────────
3036
+ const helperCmd = program
3037
+ .command("helper")
3038
+ .description("Manage the Gnosys helper library for agent integration");
3039
+ helperCmd
3040
+ .command("generate")
3041
+ .description("Generate a gnosys-helper.ts file in the current directory (or specified directory)")
3042
+ .option("-d, --directory <dir>", "Target directory (default: cwd)")
3043
+ .option("--json", "Output as JSON")
3044
+ .action(async (opts) => {
3045
+ try {
3046
+ const { generateHelper } = await import("./sandbox/helper-template.js");
3047
+ const targetDir = opts.directory || process.cwd();
3048
+ const outputPath = await generateHelper(targetDir);
3049
+ if (opts.json) {
3050
+ console.log(JSON.stringify({ ok: true, path: outputPath }));
3051
+ }
3052
+ else {
3053
+ console.log(`Generated: ${outputPath}`);
3054
+ console.log();
3055
+ console.log("Usage in your agent/script:");
3056
+ console.log(' import { gnosys } from "./gnosys-helper";');
3057
+ console.log(' await gnosys.add("We use conventional commits");');
3058
+ console.log(' const ctx = await gnosys.recall("auth decisions");');
3059
+ }
3060
+ }
3061
+ catch (err) {
3062
+ if (opts.json) {
3063
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3064
+ }
3065
+ else {
3066
+ console.error(`Failed to generate helper: ${err instanceof Error ? err.message : err}`);
3067
+ }
3068
+ process.exit(1);
3069
+ }
3070
+ });
3071
+ // ─── Phase 10: gnosys trace ─────────────────────────────────────────────
3072
+ program
3073
+ .command("trace <directory>")
3074
+ .description("Trace a codebase and store procedural 'how' memories with call-chain relationships")
3075
+ .option("--max-files <n>", "Maximum number of source files to scan", "500")
3076
+ .option("--project-id <id>", "Project ID to associate memories with")
3077
+ .option("--json", "Output as JSON")
3078
+ .action(async (directory, opts) => {
3079
+ try {
3080
+ const { traceCodebase } = await import("./lib/trace.js");
3081
+ const { GnosysDB } = await import("./lib/db.js");
3082
+ const dbDir = GnosysDB.getCentralDbDir();
3083
+ const db = new GnosysDB(dbDir);
3084
+ if (!db.isAvailable()) {
3085
+ console.error("Error: GnosysDB not available. Is better-sqlite3 installed?");
3086
+ process.exit(1);
3087
+ }
3088
+ const result = traceCodebase(db, directory, {
3089
+ projectId: opts.projectId,
3090
+ maxFiles: opts.maxFiles ? parseInt(opts.maxFiles, 10) : undefined,
3091
+ });
3092
+ db.close();
3093
+ if (opts.json) {
3094
+ console.log(JSON.stringify(result, null, 2));
3095
+ }
3096
+ else {
3097
+ console.log(`Trace complete:`);
3098
+ console.log(` Files scanned: ${result.filesScanned}`);
3099
+ console.log(` Functions found: ${result.functionsFound}`);
3100
+ console.log(` Memories created: ${result.memoriesCreated}`);
3101
+ console.log(` Relationships created: ${result.relationshipsCreated}`);
3102
+ }
3103
+ }
3104
+ catch (err) {
3105
+ if (opts.json) {
3106
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3107
+ }
3108
+ else {
3109
+ console.error(`Trace failed: ${err instanceof Error ? err.message : err}`);
3110
+ }
3111
+ process.exit(1);
3112
+ }
3113
+ });
3114
+ // ─── Phase 10: gnosys reflect ───────────────────────────────────────────
3115
+ program
3116
+ .command("reflect <outcome>")
3117
+ .description("Reflect on an outcome to update memory confidence and create relationships")
3118
+ .option("--memory-ids <ids>", "Comma-separated list of memory IDs to relate to")
3119
+ .option("--failure", "Mark this as a failure (default: success)")
3120
+ .option("--notes <text>", "Additional notes about the outcome")
3121
+ .option("--confidence-delta <n>", "Custom confidence delta (e.g. 0.1 or -0.2)")
3122
+ .option("--json", "Output as JSON")
3123
+ .action(async (outcome, opts) => {
3124
+ try {
3125
+ const { GnosysDB } = await import("./lib/db.js");
3126
+ const { handleRequest } = await import("./sandbox/server.js");
3127
+ const dbDir = GnosysDB.getCentralDbDir();
3128
+ const db = new GnosysDB(dbDir);
3129
+ if (!db.isAvailable()) {
3130
+ console.error("Error: GnosysDB not available. Is better-sqlite3 installed?");
3131
+ process.exit(1);
3132
+ }
3133
+ const params = {
3134
+ outcome,
3135
+ success: !opts.failure,
3136
+ };
3137
+ if (opts.memoryIds)
3138
+ params.memory_ids = opts.memoryIds.split(",").map((s) => s.trim());
3139
+ if (opts.notes)
3140
+ params.notes = opts.notes;
3141
+ if (opts.confidenceDelta)
3142
+ params.confidence_delta = parseFloat(opts.confidenceDelta);
3143
+ const res = handleRequest(db, {
3144
+ id: "cli-reflect",
3145
+ method: "reflect",
3146
+ params,
3147
+ });
3148
+ db.close();
3149
+ if (!res.ok) {
3150
+ console.error(`Reflect failed: ${res.error}`);
3151
+ process.exit(1);
3152
+ }
3153
+ const result = res.result;
3154
+ if (opts.json) {
3155
+ console.log(JSON.stringify(result, null, 2));
3156
+ }
3157
+ else {
3158
+ console.log(`Reflection recorded:`);
3159
+ console.log(` ID: ${result.reflection_id}`);
3160
+ console.log(` Outcome: ${result.outcome}`);
3161
+ console.log(` Memories updated: ${result.memories_updated.length}`);
3162
+ console.log(` Relationships created: ${result.relationships_created}`);
3163
+ console.log(` Confidence delta: ${result.confidence_delta > 0 ? "+" : ""}${result.confidence_delta.toFixed(2)}`);
3164
+ }
3165
+ }
3166
+ catch (err) {
3167
+ if (opts.json) {
3168
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3169
+ }
3170
+ else {
3171
+ console.error(`Reflect failed: ${err instanceof Error ? err.message : err}`);
3172
+ }
3173
+ process.exit(1);
3174
+ }
3175
+ });
3176
+ // ─── Phase 10: gnosys traverse ──────────────────────────────────────────
3177
+ program
3178
+ .command("traverse <memoryId>")
3179
+ .description("Traverse relationship chains starting from a memory (BFS, depth-limited)")
3180
+ .option("-d, --depth <n>", "Maximum traversal depth (default: 3, max: 10)", "3")
3181
+ .option("--rel-types <types>", "Comma-separated relationship types to follow (e.g. leads_to,requires)")
3182
+ .option("--json", "Output as JSON")
3183
+ .action(async (memoryId, opts) => {
3184
+ try {
3185
+ const { GnosysDB } = await import("./lib/db.js");
3186
+ const { handleRequest } = await import("./sandbox/server.js");
3187
+ const dbDir = GnosysDB.getCentralDbDir();
3188
+ const db = new GnosysDB(dbDir);
3189
+ if (!db.isAvailable()) {
3190
+ console.error("Error: GnosysDB not available. Is better-sqlite3 installed?");
3191
+ process.exit(1);
3192
+ }
3193
+ const params = {
3194
+ id: memoryId,
3195
+ depth: opts.depth ? parseInt(opts.depth, 10) : 3,
3196
+ };
3197
+ if (opts.relTypes)
3198
+ params.rel_types = opts.relTypes.split(",").map((s) => s.trim());
3199
+ const res = handleRequest(db, {
3200
+ id: "cli-traverse",
3201
+ method: "traverse",
3202
+ params,
3203
+ });
3204
+ db.close();
3205
+ if (!res.ok) {
3206
+ console.error(`Traverse failed: ${res.error}`);
3207
+ process.exit(1);
3208
+ }
3209
+ const result = res.result;
3210
+ if (opts.json) {
3211
+ console.log(JSON.stringify(result, null, 2));
3212
+ }
3213
+ else {
3214
+ console.log(`Traversal from ${memoryId} (depth: ${result.depth}):`);
3215
+ console.log(` Total nodes: ${result.total}\n`);
3216
+ for (const node of result.nodes) {
3217
+ const indent = " ".repeat(node.depth + 1);
3218
+ const via = node.via_rel ? ` ← [${node.via_rel}] from ${node.via_from}` : " (root)";
3219
+ console.log(`${indent}${node.id}: ${node.title} (conf: ${node.confidence.toFixed(2)})${via}`);
3220
+ }
3221
+ }
3222
+ }
3223
+ catch (err) {
3224
+ if (opts.json) {
3225
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3226
+ }
3227
+ else {
3228
+ console.error(`Traverse failed: ${err instanceof Error ? err.message : err}`);
3229
+ }
3230
+ process.exit(1);
3231
+ }
3232
+ });
3233
+ // ─── gnosys web init|ingest|build-index|build|add|remove|update|status ──
3234
+ async function getWebStorePath() {
3235
+ const resolver = await getResolver();
3236
+ const stores = resolver.getStores();
3237
+ return stores.length > 0 ? stores[0].path : path.join(process.cwd(), ".gnosys");
3238
+ }
3239
+ const webCmd = program
3240
+ .command("web")
3241
+ .description("Web Knowledge Base — generate searchable knowledge from websites");
3242
+ webCmd
3243
+ .command("init")
3244
+ .description("Scaffold a project for web knowledge base usage")
3245
+ .option("--source <type>", "Source type: sitemap, directory, urls", "sitemap")
3246
+ .option("--output <dir>", "Output directory for knowledge files", "./knowledge")
3247
+ .option("--no-config", "Skip gnosys.json modification")
3248
+ .option("--json", "Output as JSON")
3249
+ .action(async (opts) => {
3250
+ try {
3251
+ const { mkdirSync } = await import("fs");
3252
+ const { loadConfig, updateConfig } = await import("./lib/config.js");
3253
+ const storePath = await getWebStorePath();
3254
+ // Create output directory
3255
+ mkdirSync(opts.output, { recursive: true });
3256
+ // Update gnosys.json with web config
3257
+ if (opts.config) {
3258
+ try {
3259
+ const config = await loadConfig(storePath);
3260
+ if (!config.web) {
3261
+ await updateConfig(storePath, {
3262
+ web: {
3263
+ source: opts.source,
3264
+ outputDir: opts.output,
3265
+ exclude: ["/api", "/admin", "/_next"],
3266
+ categories: {
3267
+ "/blog/*": "blog",
3268
+ "/services/*": "services",
3269
+ "/products/*": "products",
3270
+ "/about*": "company",
3271
+ "/careers*": "careers",
3272
+ "/industries/*": "industries",
3273
+ },
3274
+ llmEnrich: true,
3275
+ prune: false,
3276
+ },
3277
+ });
3278
+ }
3279
+ }
3280
+ catch {
3281
+ // No gnosys.json yet — that's OK, user can run gnosys init first
3282
+ }
3283
+ }
3284
+ if (opts.json) {
3285
+ console.log(JSON.stringify({ ok: true, outputDir: opts.output, source: opts.source }));
3286
+ }
3287
+ else {
3288
+ console.log(`Web knowledge base initialized:`);
3289
+ console.log(` Output directory: ${opts.output}`);
3290
+ console.log(` Source type: ${opts.source}`);
3291
+ console.log(`\nNext steps:`);
3292
+ console.log(` 1. Edit gnosys.json to set your sitemapUrl (or switch to directory source)`);
3293
+ console.log(` 2. Run: gnosys web build`);
3294
+ console.log(` 3. Add to package.json: "postbuild": "gnosys web build"`);
3295
+ }
3296
+ }
3297
+ catch (err) {
3298
+ if (opts.json) {
3299
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3300
+ }
3301
+ else {
3302
+ console.error(`Web init failed: ${err instanceof Error ? err.message : err}`);
3303
+ }
3304
+ process.exit(1);
3305
+ }
3306
+ });
3307
+ webCmd
3308
+ .command("ingest")
3309
+ .description("Crawl the configured source and generate knowledge markdown files")
3310
+ .option("--source <url>", "Override sitemap URL or content directory")
3311
+ .option("--prune", "Remove orphaned knowledge files")
3312
+ .option("--no-llm", "Force structured mode (no LLM)")
3313
+ .option("--concurrency <n>", "Parallel processing limit", "3")
3314
+ .option("--dry-run", "Show what would change without writing files")
3315
+ .option("--verbose", "Print per-page details")
3316
+ .option("--json", "Output results as JSON")
3317
+ .action(async (opts) => {
3318
+ try {
3319
+ const { loadConfig } = await import("./lib/config.js");
3320
+ const { ingestSite } = await import("./lib/webIngest.js");
3321
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3322
+ const webConfig = gnosysConfig.web;
3323
+ if (!webConfig) {
3324
+ throw new Error("No web configuration found in gnosys.json. Run 'gnosys web init' first.");
3325
+ }
3326
+ const result = await ingestSite({
3327
+ source: webConfig.source,
3328
+ sitemapUrl: opts.source || webConfig.sitemapUrl,
3329
+ contentDir: opts.source || webConfig.contentDir,
3330
+ urls: webConfig.urls,
3331
+ outputDir: webConfig.outputDir,
3332
+ exclude: webConfig.exclude,
3333
+ categories: webConfig.categories,
3334
+ llmEnrich: opts.llm ? webConfig.llmEnrich : false,
3335
+ prune: opts.prune || webConfig.prune,
3336
+ concurrency: parseInt(opts.concurrency) || webConfig.concurrency,
3337
+ crawlDelayMs: webConfig.crawlDelayMs,
3338
+ dryRun: opts.dryRun,
3339
+ verbose: opts.verbose,
3340
+ }, gnosysConfig);
3341
+ if (opts.json) {
3342
+ console.log(JSON.stringify(result, null, 2));
3343
+ }
3344
+ else {
3345
+ console.log(`Ingestion complete (${result.duration}ms):`);
3346
+ console.log(` Added: ${result.added.length}`);
3347
+ console.log(` Updated: ${result.updated.length}`);
3348
+ console.log(` Unchanged: ${result.unchanged.length}`);
3349
+ console.log(` Removed: ${result.removed.length}`);
3350
+ if (result.errors.length > 0) {
3351
+ console.log(` Errors: ${result.errors.length}`);
3352
+ for (const e of result.errors) {
3353
+ console.log(` ${e.url}: ${e.error}`);
3354
+ }
3355
+ }
3356
+ }
3357
+ }
3358
+ catch (err) {
3359
+ if (opts.json) {
3360
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3361
+ }
3362
+ else {
3363
+ console.error(`Ingest failed: ${err instanceof Error ? err.message : err}`);
3364
+ }
3365
+ process.exit(1);
3366
+ }
3367
+ });
3368
+ webCmd
3369
+ .command("build-index")
3370
+ .description("Generate search index JSON from the knowledge directory")
3371
+ .option("--input <dir>", "Override knowledge directory")
3372
+ .option("--output <path>", "Override output file path")
3373
+ .option("--no-stop-words", "Disable stop-word filtering")
3374
+ .option("--json", "Output index stats as JSON")
3375
+ .action(async (opts) => {
3376
+ try {
3377
+ const { loadConfig } = await import("./lib/config.js");
3378
+ const { buildIndex, writeIndex } = await import("./lib/webIndex.js");
3379
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3380
+ const knowledgeDir = opts.input || gnosysConfig.web?.outputDir || "./knowledge";
3381
+ const outputPath = opts.output || path.join(knowledgeDir, "gnosys-index.json");
3382
+ const index = await buildIndex(knowledgeDir, {
3383
+ stopWords: opts.stopWords,
3384
+ });
3385
+ await writeIndex(index, outputPath);
3386
+ if (opts.json) {
3387
+ console.log(JSON.stringify({
3388
+ ok: true,
3389
+ documentCount: index.documentCount,
3390
+ tokenCount: Object.keys(index.invertedIndex).length,
3391
+ outputPath,
3392
+ }));
3393
+ }
3394
+ else {
3395
+ console.log(`Search index built:`);
3396
+ console.log(` Documents: ${index.documentCount}`);
3397
+ console.log(` Tokens: ${Object.keys(index.invertedIndex).length}`);
3398
+ console.log(` Output: ${outputPath}`);
3399
+ }
3400
+ }
3401
+ catch (err) {
3402
+ if (opts.json) {
3403
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3404
+ }
3405
+ else {
3406
+ console.error(`Build index failed: ${err instanceof Error ? err.message : err}`);
3407
+ }
3408
+ process.exit(1);
3409
+ }
3410
+ });
3411
+ webCmd
3412
+ .command("build")
3413
+ .description("Run ingest + build-index in one shot")
3414
+ .option("--source <url>", "Override sitemap URL or content directory")
3415
+ .option("--prune", "Remove orphaned knowledge files")
3416
+ .option("--no-llm", "Force structured mode (no LLM)")
3417
+ .option("--concurrency <n>", "Parallel processing limit", "3")
3418
+ .option("--dry-run", "Show what would change without writing files")
3419
+ .option("--json", "Output results as JSON")
3420
+ .action(async (opts) => {
3421
+ try {
3422
+ const { loadConfig } = await import("./lib/config.js");
3423
+ const { ingestSite } = await import("./lib/webIngest.js");
3424
+ const { buildIndex, writeIndex } = await import("./lib/webIndex.js");
3425
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3426
+ const webConfig = gnosysConfig.web;
3427
+ if (!webConfig) {
3428
+ throw new Error("No web configuration found in gnosys.json. Run 'gnosys web init' first.");
3429
+ }
3430
+ // Step 1: Ingest
3431
+ const ingestResult = await ingestSite({
3432
+ source: webConfig.source,
3433
+ sitemapUrl: opts.source || webConfig.sitemapUrl,
3434
+ contentDir: opts.source || webConfig.contentDir,
3435
+ urls: webConfig.urls,
3436
+ outputDir: webConfig.outputDir,
3437
+ exclude: webConfig.exclude,
3438
+ categories: webConfig.categories,
3439
+ llmEnrich: opts.llm ? webConfig.llmEnrich : false,
3440
+ prune: opts.prune || webConfig.prune,
3441
+ concurrency: parseInt(opts.concurrency) || webConfig.concurrency,
3442
+ crawlDelayMs: webConfig.crawlDelayMs,
3443
+ dryRun: opts.dryRun,
3444
+ }, gnosysConfig);
3445
+ // Step 2: Build index (skip if dry run)
3446
+ let indexStats = { documentCount: 0, tokenCount: 0 };
3447
+ if (!opts.dryRun) {
3448
+ const index = await buildIndex(webConfig.outputDir);
3449
+ const indexPath = path.join(webConfig.outputDir, "gnosys-index.json");
3450
+ await writeIndex(index, indexPath);
3451
+ indexStats = {
3452
+ documentCount: index.documentCount,
3453
+ tokenCount: Object.keys(index.invertedIndex).length,
3454
+ };
3455
+ }
3456
+ if (opts.json) {
3457
+ console.log(JSON.stringify({ ...ingestResult, index: indexStats }));
3458
+ }
3459
+ else {
3460
+ console.log(`Web build complete (${ingestResult.duration}ms):`);
3461
+ console.log(` Added: ${ingestResult.added.length}`);
3462
+ console.log(` Updated: ${ingestResult.updated.length}`);
3463
+ console.log(` Unchanged: ${ingestResult.unchanged.length}`);
3464
+ console.log(` Removed: ${ingestResult.removed.length}`);
3465
+ console.log(` Index: ${indexStats.documentCount} docs, ${indexStats.tokenCount} tokens`);
3466
+ if (ingestResult.errors.length > 0) {
3467
+ console.log(` Errors: ${ingestResult.errors.length}`);
3468
+ for (const e of ingestResult.errors) {
3469
+ console.log(` ${e.url}: ${e.error}`);
3470
+ }
3471
+ }
3472
+ }
3473
+ }
3474
+ catch (err) {
3475
+ if (opts.json) {
3476
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3477
+ }
3478
+ else {
3479
+ console.error(`Web build failed: ${err instanceof Error ? err.message : err}`);
3480
+ }
3481
+ process.exit(1);
3482
+ }
3483
+ });
3484
+ webCmd
3485
+ .command("add <url>")
3486
+ .description("Ingest a single URL into the knowledge base")
3487
+ .option("--category <name>", "Override category inference")
3488
+ .option("--no-llm", "Force structured mode")
3489
+ .option("--no-reindex", "Skip index rebuild")
3490
+ .option("--json", "Output as JSON")
3491
+ .action(async (url, opts) => {
3492
+ try {
3493
+ const { loadConfig } = await import("./lib/config.js");
3494
+ const { ingestUrl } = await import("./lib/webIngest.js");
3495
+ const { buildIndex, writeIndex } = await import("./lib/webIndex.js");
3496
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3497
+ const webConfig = gnosysConfig.web;
3498
+ if (!webConfig) {
3499
+ throw new Error("No web configuration found in gnosys.json. Run 'gnosys web init' first.");
3500
+ }
3501
+ const categories = opts.category
3502
+ ? { ...webConfig.categories, "/*": opts.category }
3503
+ : webConfig.categories;
3504
+ const result = await ingestUrl(url, {
3505
+ source: "urls",
3506
+ outputDir: webConfig.outputDir,
3507
+ categories,
3508
+ llmEnrich: opts.llm ? webConfig.llmEnrich : false,
3509
+ concurrency: 1,
3510
+ crawlDelayMs: 0,
3511
+ }, gnosysConfig);
3512
+ // Rebuild index unless --no-reindex
3513
+ if (opts.reindex && result.added.length + result.updated.length > 0) {
3514
+ const index = await buildIndex(webConfig.outputDir);
3515
+ await writeIndex(index, path.join(webConfig.outputDir, "gnosys-index.json"));
3516
+ }
3517
+ if (opts.json) {
3518
+ console.log(JSON.stringify(result, null, 2));
3519
+ }
3520
+ else {
3521
+ if (result.added.length > 0) {
3522
+ console.log(`Added: ${result.added[0]}`);
3523
+ }
3524
+ else if (result.updated.length > 0) {
3525
+ console.log(`Updated: ${result.updated[0]}`);
3526
+ }
3527
+ else if (result.unchanged.length > 0) {
3528
+ console.log(`Unchanged (content identical)`);
3529
+ }
3530
+ if (result.errors.length > 0) {
3531
+ console.error(`Error: ${result.errors[0].error}`);
3532
+ }
3533
+ }
3534
+ }
3535
+ catch (err) {
3536
+ if (opts.json) {
3537
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3538
+ }
3539
+ else {
3540
+ console.error(`Web add failed: ${err instanceof Error ? err.message : err}`);
3541
+ }
3542
+ process.exit(1);
3543
+ }
3544
+ });
3545
+ webCmd
3546
+ .command("remove <filepath>")
3547
+ .description("Remove a knowledge file and rebuild the index")
3548
+ .option("--json", "Output as JSON")
3549
+ .action(async (filepath, opts) => {
3550
+ try {
3551
+ const { loadConfig } = await import("./lib/config.js");
3552
+ const { buildIndex, writeIndex } = await import("./lib/webIndex.js");
3553
+ const fsp = await import("fs/promises");
3554
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3555
+ const webConfig = gnosysConfig.web;
3556
+ const knowledgeDir = webConfig?.outputDir || "./knowledge";
3557
+ const fullPath = path.resolve(knowledgeDir, filepath);
3558
+ if (!existsSync(fullPath)) {
3559
+ throw new Error(`File not found: ${fullPath}`);
3560
+ }
3561
+ await fsp.unlink(fullPath);
3562
+ // Rebuild index
3563
+ const index = await buildIndex(knowledgeDir);
3564
+ await writeIndex(index, path.join(knowledgeDir, "gnosys-index.json"));
3565
+ if (opts.json) {
3566
+ console.log(JSON.stringify({ ok: true, removed: filepath, documentCount: index.documentCount }));
3567
+ }
3568
+ else {
3569
+ console.log(`Removed: ${filepath}`);
3570
+ console.log(`Index rebuilt: ${index.documentCount} documents`);
3571
+ }
3572
+ }
3573
+ catch (err) {
3574
+ if (opts.json) {
3575
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3576
+ }
3577
+ else {
3578
+ console.error(`Web remove failed: ${err instanceof Error ? err.message : err}`);
3579
+ }
3580
+ process.exit(1);
3581
+ }
3582
+ });
3583
+ webCmd
3584
+ .command("update <urlOrPath>")
3585
+ .description("Re-ingest a URL or refresh a knowledge file, then rebuild the index")
3586
+ .option("--no-llm", "Force structured mode (no LLM)")
3587
+ .option("--category <name>", "Override category inference")
3588
+ .option("--json", "Output as JSON")
3589
+ .action(async (urlOrPath, opts) => {
3590
+ try {
3591
+ const { loadConfig } = await import("./lib/config.js");
3592
+ const { ingestUrl } = await import("./lib/webIngest.js");
3593
+ const { buildIndex, writeIndex } = await import("./lib/webIndex.js");
3594
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3595
+ const webConfig = gnosysConfig.web;
3596
+ if (!webConfig) {
3597
+ throw new Error("No web configuration found in gnosys.json. Run 'gnosys web init' first.");
3598
+ }
3599
+ const knowledgeDir = webConfig.outputDir || "./knowledge";
3600
+ const isUrl = urlOrPath.startsWith("http://") || urlOrPath.startsWith("https://");
3601
+ if (isUrl) {
3602
+ // Re-ingest the URL
3603
+ const categories = opts.category
3604
+ ? { "/*": opts.category }
3605
+ : webConfig.categories;
3606
+ const result = await ingestUrl(urlOrPath, {
3607
+ source: "urls",
3608
+ outputDir: knowledgeDir,
3609
+ categories,
3610
+ llmEnrich: opts.llm ? webConfig.llmEnrich : false,
3611
+ prune: false,
3612
+ concurrency: 1,
3613
+ crawlDelayMs: 0,
3614
+ }, gnosysConfig);
3615
+ // Rebuild index
3616
+ const index = await buildIndex(knowledgeDir);
3617
+ await writeIndex(index, path.join(knowledgeDir, "gnosys-index.json"));
3618
+ if (opts.json) {
3619
+ console.log(JSON.stringify({ ok: true, ...result, documentCount: index.documentCount }));
3620
+ }
3621
+ else {
3622
+ console.log(`Updated: ${urlOrPath}`);
3623
+ console.log(` Added: ${result.added.length}, Updated: ${result.updated.length}`);
3624
+ console.log(`Index rebuilt: ${index.documentCount} documents`);
3625
+ }
3626
+ }
3627
+ else {
3628
+ // Refresh a local knowledge file — rebuild index
3629
+ const fullPath = path.resolve(knowledgeDir, urlOrPath);
3630
+ if (!existsSync(fullPath)) {
3631
+ throw new Error(`File not found: ${fullPath}`);
3632
+ }
3633
+ const index = await buildIndex(knowledgeDir);
3634
+ await writeIndex(index, path.join(knowledgeDir, "gnosys-index.json"));
3635
+ if (opts.json) {
3636
+ console.log(JSON.stringify({ ok: true, refreshed: urlOrPath, documentCount: index.documentCount }));
3637
+ }
3638
+ else {
3639
+ console.log(`Refreshed: ${urlOrPath}`);
3640
+ console.log(`Index rebuilt: ${index.documentCount} documents`);
3641
+ }
3642
+ }
3643
+ }
3644
+ catch (err) {
3645
+ if (opts.json) {
3646
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3647
+ }
3648
+ else {
3649
+ console.error(`Web update failed: ${err instanceof Error ? err.message : err}`);
3650
+ }
3651
+ process.exit(1);
3652
+ }
3653
+ });
3654
+ webCmd
3655
+ .command("status")
3656
+ .description("Show the current state of the web knowledge base")
3657
+ .option("--json", "Output as JSON")
3658
+ .action(async (opts) => {
3659
+ try {
3660
+ const { loadConfig } = await import("./lib/config.js");
3661
+ const { readdirSync, statSync } = await import("fs");
3662
+ const gnosysConfig = await loadConfig(await getWebStorePath());
3663
+ const webConfig = gnosysConfig.web;
3664
+ const knowledgeDir = webConfig?.outputDir || "./knowledge";
3665
+ const resolvedDir = path.resolve(knowledgeDir);
3666
+ if (!existsSync(resolvedDir)) {
3667
+ if (opts.json) {
3668
+ console.log(JSON.stringify({ ok: true, exists: false, message: "Knowledge directory not found" }));
3669
+ }
3670
+ else {
3671
+ console.log(`Knowledge directory not found: ${resolvedDir}`);
3672
+ console.log(`Run 'gnosys web init' to get started.`);
3673
+ }
3674
+ return;
3675
+ }
3676
+ // Count files by category
3677
+ const categoryCounts = {};
3678
+ let totalFiles = 0;
3679
+ function countFiles(dir) {
3680
+ const entries = readdirSync(dir, { withFileTypes: true });
3681
+ for (const entry of entries) {
3682
+ const fullPath = path.join(dir, entry.name);
3683
+ if (entry.isDirectory()) {
3684
+ countFiles(fullPath);
3685
+ }
3686
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
3687
+ const category = path.relative(resolvedDir, dir) || "root";
3688
+ categoryCounts[category] = (categoryCounts[category] || 0) + 1;
3689
+ totalFiles++;
3690
+ }
3691
+ }
3692
+ }
3693
+ countFiles(resolvedDir);
3694
+ // Check index file
3695
+ const indexPath = path.join(resolvedDir, "gnosys-index.json");
3696
+ let indexInfo = { exists: false };
3697
+ if (existsSync(indexPath)) {
3698
+ const stat = statSync(indexPath);
3699
+ try {
3700
+ const indexData = JSON.parse(readFileSync(indexPath, "utf-8"));
3701
+ indexInfo = {
3702
+ exists: true,
3703
+ documentCount: indexData.documentCount,
3704
+ size: stat.size,
3705
+ generated: indexData.generated,
3706
+ };
3707
+ }
3708
+ catch {
3709
+ indexInfo = { exists: true, size: stat.size };
3710
+ }
3711
+ }
3712
+ if (opts.json) {
3713
+ console.log(JSON.stringify({
3714
+ ok: true,
3715
+ knowledgeDir: resolvedDir,
3716
+ totalFiles,
3717
+ categoryCounts,
3718
+ index: indexInfo,
3719
+ }, null, 2));
3720
+ }
3721
+ else {
3722
+ console.log(`Web Knowledge Base Status:`);
3723
+ console.log(` Directory: ${resolvedDir}`);
3724
+ console.log(` Total files: ${totalFiles}`);
3725
+ if (Object.keys(categoryCounts).length > 0) {
3726
+ console.log(` By category:`);
3727
+ for (const [cat, count] of Object.entries(categoryCounts).sort()) {
3728
+ console.log(` ${cat}: ${count}`);
3729
+ }
3730
+ }
3731
+ if (indexInfo.exists) {
3732
+ console.log(` Index: ${indexInfo.documentCount ?? "?"} docs, ${((indexInfo.size || 0) / 1024).toFixed(1)}KB`);
3733
+ if (indexInfo.generated) {
3734
+ console.log(` Last built: ${indexInfo.generated}`);
3735
+ }
3736
+ }
3737
+ else {
3738
+ console.log(` Index: not built (run 'gnosys web build-index')`);
3739
+ }
3740
+ }
3741
+ }
3742
+ catch (err) {
3743
+ if (opts.json) {
3744
+ console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
3745
+ }
3746
+ else {
3747
+ console.error(`Web status failed: ${err instanceof Error ? err.message : err}`);
3748
+ }
3749
+ process.exit(1);
3750
+ }
3751
+ });
3752
+ program.parse();
3753
+ //# sourceMappingURL=cli.js.map