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.
- package/LICENSE +21 -0
- package/README.md +1387 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3753 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2267 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/archive.d.ts +95 -0
- package/dist/lib/archive.d.ts.map +1 -0
- package/dist/lib/archive.js +311 -0
- package/dist/lib/archive.js.map +1 -0
- package/dist/lib/ask.d.ts +77 -0
- package/dist/lib/ask.d.ts.map +1 -0
- package/dist/lib/ask.js +316 -0
- package/dist/lib/ask.js.map +1 -0
- package/dist/lib/audit.d.ts +47 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +136 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/bootstrap.d.ts +56 -0
- package/dist/lib/bootstrap.d.ts.map +1 -0
- package/dist/lib/bootstrap.js +163 -0
- package/dist/lib/bootstrap.js.map +1 -0
- package/dist/lib/config.d.ts +239 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +371 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/dashboard.d.ts +81 -0
- package/dist/lib/dashboard.d.ts.map +1 -0
- package/dist/lib/dashboard.js +314 -0
- package/dist/lib/dashboard.js.map +1 -0
- package/dist/lib/db.d.ts +182 -0
- package/dist/lib/db.d.ts.map +1 -0
- package/dist/lib/db.js +620 -0
- package/dist/lib/db.js.map +1 -0
- package/dist/lib/dbSearch.d.ts +65 -0
- package/dist/lib/dbSearch.d.ts.map +1 -0
- package/dist/lib/dbSearch.js +239 -0
- package/dist/lib/dbSearch.js.map +1 -0
- package/dist/lib/dbWrite.d.ts +56 -0
- package/dist/lib/dbWrite.d.ts.map +1 -0
- package/dist/lib/dbWrite.js +171 -0
- package/dist/lib/dbWrite.js.map +1 -0
- package/dist/lib/dream.d.ts +170 -0
- package/dist/lib/dream.d.ts.map +1 -0
- package/dist/lib/dream.js +706 -0
- package/dist/lib/dream.js.map +1 -0
- package/dist/lib/embeddings.d.ts +84 -0
- package/dist/lib/embeddings.d.ts.map +1 -0
- package/dist/lib/embeddings.js +226 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/export.d.ts +92 -0
- package/dist/lib/export.d.ts.map +1 -0
- package/dist/lib/export.js +362 -0
- package/dist/lib/export.js.map +1 -0
- package/dist/lib/federated.d.ts +113 -0
- package/dist/lib/federated.d.ts.map +1 -0
- package/dist/lib/federated.js +346 -0
- package/dist/lib/federated.js.map +1 -0
- package/dist/lib/graph.d.ts +50 -0
- package/dist/lib/graph.d.ts.map +1 -0
- package/dist/lib/graph.js +118 -0
- package/dist/lib/graph.js.map +1 -0
- package/dist/lib/history.d.ts +39 -0
- package/dist/lib/history.d.ts.map +1 -0
- package/dist/lib/history.js +112 -0
- package/dist/lib/history.js.map +1 -0
- package/dist/lib/hybridSearch.d.ts +80 -0
- package/dist/lib/hybridSearch.d.ts.map +1 -0
- package/dist/lib/hybridSearch.js +296 -0
- package/dist/lib/hybridSearch.js.map +1 -0
- package/dist/lib/import.d.ts +52 -0
- package/dist/lib/import.d.ts.map +1 -0
- package/dist/lib/import.js +365 -0
- package/dist/lib/import.js.map +1 -0
- package/dist/lib/ingest.d.ts +51 -0
- package/dist/lib/ingest.d.ts.map +1 -0
- package/dist/lib/ingest.js +144 -0
- package/dist/lib/ingest.js.map +1 -0
- package/dist/lib/lensing.d.ts +35 -0
- package/dist/lib/lensing.d.ts.map +1 -0
- package/dist/lib/lensing.js +85 -0
- package/dist/lib/lensing.js.map +1 -0
- package/dist/lib/llm.d.ts +84 -0
- package/dist/lib/llm.d.ts.map +1 -0
- package/dist/lib/llm.js +386 -0
- package/dist/lib/llm.js.map +1 -0
- package/dist/lib/lock.d.ts +28 -0
- package/dist/lib/lock.d.ts.map +1 -0
- package/dist/lib/lock.js +145 -0
- package/dist/lib/lock.js.map +1 -0
- package/dist/lib/maintenance.d.ts +124 -0
- package/dist/lib/maintenance.d.ts.map +1 -0
- package/dist/lib/maintenance.js +587 -0
- package/dist/lib/maintenance.js.map +1 -0
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.d.ts.map +1 -0
- package/dist/lib/migrate.js +260 -0
- package/dist/lib/migrate.js.map +1 -0
- package/dist/lib/preferences.d.ts +49 -0
- package/dist/lib/preferences.d.ts.map +1 -0
- package/dist/lib/preferences.js +149 -0
- package/dist/lib/preferences.js.map +1 -0
- package/dist/lib/projectIdentity.d.ts +66 -0
- package/dist/lib/projectIdentity.d.ts.map +1 -0
- package/dist/lib/projectIdentity.js +148 -0
- package/dist/lib/projectIdentity.js.map +1 -0
- package/dist/lib/recall.d.ts +82 -0
- package/dist/lib/recall.d.ts.map +1 -0
- package/dist/lib/recall.js +289 -0
- package/dist/lib/recall.js.map +1 -0
- package/dist/lib/resolver.d.ts +116 -0
- package/dist/lib/resolver.d.ts.map +1 -0
- package/dist/lib/resolver.js +372 -0
- package/dist/lib/resolver.js.map +1 -0
- package/dist/lib/retry.d.ts +24 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/lib/retry.js +60 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/lib/rulesGen.d.ts +51 -0
- package/dist/lib/rulesGen.d.ts.map +1 -0
- package/dist/lib/rulesGen.js +167 -0
- package/dist/lib/rulesGen.js.map +1 -0
- package/dist/lib/search.d.ts +51 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +190 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/staticSearch.d.ts +70 -0
- package/dist/lib/staticSearch.d.ts.map +1 -0
- package/dist/lib/staticSearch.js +162 -0
- package/dist/lib/staticSearch.js.map +1 -0
- package/dist/lib/store.d.ts +79 -0
- package/dist/lib/store.d.ts.map +1 -0
- package/dist/lib/store.js +227 -0
- package/dist/lib/store.js.map +1 -0
- package/dist/lib/structuredIngest.d.ts +37 -0
- package/dist/lib/structuredIngest.d.ts.map +1 -0
- package/dist/lib/structuredIngest.js +208 -0
- package/dist/lib/structuredIngest.js.map +1 -0
- package/dist/lib/tags.d.ts +26 -0
- package/dist/lib/tags.d.ts.map +1 -0
- package/dist/lib/tags.js +109 -0
- package/dist/lib/tags.js.map +1 -0
- package/dist/lib/timeline.d.ts +34 -0
- package/dist/lib/timeline.d.ts.map +1 -0
- package/dist/lib/timeline.js +116 -0
- package/dist/lib/timeline.js.map +1 -0
- package/dist/lib/trace.d.ts +42 -0
- package/dist/lib/trace.d.ts.map +1 -0
- package/dist/lib/trace.js +338 -0
- package/dist/lib/trace.js.map +1 -0
- package/dist/lib/webIndex.d.ts +28 -0
- package/dist/lib/webIndex.d.ts.map +1 -0
- package/dist/lib/webIndex.js +208 -0
- package/dist/lib/webIndex.js.map +1 -0
- package/dist/lib/webIngest.d.ts +51 -0
- package/dist/lib/webIngest.d.ts.map +1 -0
- package/dist/lib/webIngest.js +533 -0
- package/dist/lib/webIngest.js.map +1 -0
- package/dist/lib/wikilinks.d.ts +63 -0
- package/dist/lib/wikilinks.d.ts.map +1 -0
- package/dist/lib/wikilinks.js +146 -0
- package/dist/lib/wikilinks.js.map +1 -0
- package/dist/sandbox/client.d.ts +82 -0
- package/dist/sandbox/client.d.ts.map +1 -0
- package/dist/sandbox/client.js +128 -0
- package/dist/sandbox/client.js.map +1 -0
- package/dist/sandbox/helper-template.d.ts +14 -0
- package/dist/sandbox/helper-template.d.ts.map +1 -0
- package/dist/sandbox/helper-template.js +285 -0
- package/dist/sandbox/helper-template.js.map +1 -0
- package/dist/sandbox/index.d.ts +10 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +10 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/manager.d.ts +40 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +220 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/server.d.ts +44 -0
- package/dist/sandbox/server.d.ts.map +1 -0
- package/dist/sandbox/server.js +661 -0
- package/dist/sandbox/server.js.map +1 -0
- package/package.json +103 -0
- 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
|