chainlesschain 0.37.9 → 0.37.11
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/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +259 -124
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Sandbox v2 commands
|
|
3
|
+
* chainlesschain sandbox create|exec|destroy|list|audit|quota|monitor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { logger } from "../lib/logger.js";
|
|
9
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
10
|
+
import {
|
|
11
|
+
createSandbox,
|
|
12
|
+
executeSandbox,
|
|
13
|
+
destroySandbox,
|
|
14
|
+
listSandboxes,
|
|
15
|
+
getAuditLog,
|
|
16
|
+
getSandbox,
|
|
17
|
+
setQuota,
|
|
18
|
+
monitorBehavior,
|
|
19
|
+
} from "../lib/sandbox-v2.js";
|
|
20
|
+
|
|
21
|
+
export function registerSandboxCommand(program) {
|
|
22
|
+
const sandbox = program
|
|
23
|
+
.command("sandbox")
|
|
24
|
+
.description("Security sandbox v2 — isolated agent execution environments");
|
|
25
|
+
|
|
26
|
+
// sandbox create <agent-id>
|
|
27
|
+
sandbox
|
|
28
|
+
.command("create")
|
|
29
|
+
.description("Create a new sandbox for an agent")
|
|
30
|
+
.argument("<agent-id>", "Agent ID to sandbox")
|
|
31
|
+
.option("--allow-read <paths>", "Comma-separated allowed read paths")
|
|
32
|
+
.option("--allow-write <paths>", "Comma-separated allowed write paths")
|
|
33
|
+
.option("--allowed-hosts <hosts>", "Comma-separated allowed network hosts")
|
|
34
|
+
.option("--json", "Output as JSON")
|
|
35
|
+
.action(async (agentId, options) => {
|
|
36
|
+
try {
|
|
37
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
38
|
+
if (!ctx.db) {
|
|
39
|
+
logger.error("Database not available");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const db = ctx.db.getDatabase();
|
|
43
|
+
const spinner = ora("Creating sandbox...").start();
|
|
44
|
+
|
|
45
|
+
const perms = {};
|
|
46
|
+
if (options.allowRead || options.allowWrite) {
|
|
47
|
+
perms.fileSystem = {
|
|
48
|
+
read: options.allowRead
|
|
49
|
+
? options.allowRead.split(",").map((p) => p.trim())
|
|
50
|
+
: ["/tmp"],
|
|
51
|
+
write: options.allowWrite
|
|
52
|
+
? options.allowWrite.split(",").map((p) => p.trim())
|
|
53
|
+
: ["/tmp"],
|
|
54
|
+
denied: ["/etc", "/usr", "/sys"],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (options.allowedHosts) {
|
|
58
|
+
perms.network = {
|
|
59
|
+
allowed: options.allowedHosts.split(",").map((h) => h.trim()),
|
|
60
|
+
denied: [],
|
|
61
|
+
maxConnections: 10,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sandboxOpts =
|
|
66
|
+
Object.keys(perms).length > 0 ? { permissions: perms } : {};
|
|
67
|
+
const result = createSandbox(db, agentId, sandboxOpts);
|
|
68
|
+
spinner.succeed("Sandbox created");
|
|
69
|
+
|
|
70
|
+
if (options.json) {
|
|
71
|
+
console.log(JSON.stringify(result, null, 2));
|
|
72
|
+
} else {
|
|
73
|
+
logger.log(chalk.bold("Sandbox Created:"));
|
|
74
|
+
logger.log(` ID: ${chalk.cyan(result.id)}`);
|
|
75
|
+
logger.log(` Status: ${chalk.green(result.status)}`);
|
|
76
|
+
logger.log(
|
|
77
|
+
` Quota: CPU=${result.quota.cpu}, Memory=${(result.quota.memory / 1024 / 1024).toFixed(0)}MB`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await shutdown();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.error(`Failed: ${err.message}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// sandbox exec <sandbox-id> <code>
|
|
89
|
+
sandbox
|
|
90
|
+
.command("exec")
|
|
91
|
+
.description("Execute code within a sandbox")
|
|
92
|
+
.argument("<sandbox-id>", "Sandbox ID")
|
|
93
|
+
.argument("<code>", "Code to execute")
|
|
94
|
+
.option("--json", "Output as JSON")
|
|
95
|
+
.action(async (sandboxId, code, options) => {
|
|
96
|
+
try {
|
|
97
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
98
|
+
if (!ctx.db) {
|
|
99
|
+
logger.error("Database not available");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const db = ctx.db.getDatabase();
|
|
103
|
+
const spinner = ora("Executing in sandbox...").start();
|
|
104
|
+
|
|
105
|
+
const result = executeSandbox(db, sandboxId, code);
|
|
106
|
+
spinner.succeed("Execution complete");
|
|
107
|
+
|
|
108
|
+
if (options.json) {
|
|
109
|
+
console.log(JSON.stringify(result, null, 2));
|
|
110
|
+
} else {
|
|
111
|
+
logger.log(chalk.bold("Execution Result:"));
|
|
112
|
+
logger.log(` Output: ${result.output}`);
|
|
113
|
+
logger.log(
|
|
114
|
+
` Exit Code: ${result.exitCode === 0 ? chalk.green(0) : chalk.red(result.exitCode)}`,
|
|
115
|
+
);
|
|
116
|
+
logger.log(` Duration: ${result.duration}ms`);
|
|
117
|
+
logger.log(` CPU Used: ${result.resourceUsage.cpu}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await shutdown();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error(`Failed: ${err.message}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// sandbox destroy <sandbox-id>
|
|
128
|
+
sandbox
|
|
129
|
+
.command("destroy")
|
|
130
|
+
.description("Destroy a sandbox")
|
|
131
|
+
.argument("<sandbox-id>", "Sandbox ID to destroy")
|
|
132
|
+
.option("--json", "Output as JSON")
|
|
133
|
+
.action(async (sandboxId, options) => {
|
|
134
|
+
try {
|
|
135
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
136
|
+
if (!ctx.db) {
|
|
137
|
+
logger.error("Database not available");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const db = ctx.db.getDatabase();
|
|
141
|
+
|
|
142
|
+
const result = destroySandbox(db, sandboxId);
|
|
143
|
+
|
|
144
|
+
if (options.json) {
|
|
145
|
+
console.log(JSON.stringify(result, null, 2));
|
|
146
|
+
} else {
|
|
147
|
+
logger.log(chalk.yellow(`Sandbox ${sandboxId} destroyed.`));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await shutdown();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logger.error(`Failed: ${err.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// sandbox list
|
|
158
|
+
sandbox
|
|
159
|
+
.command("list")
|
|
160
|
+
.description("List active sandboxes")
|
|
161
|
+
.option("--json", "Output as JSON")
|
|
162
|
+
.action(async (options) => {
|
|
163
|
+
try {
|
|
164
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
165
|
+
if (!ctx.db) {
|
|
166
|
+
logger.error("Database not available");
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const db = ctx.db.getDatabase();
|
|
170
|
+
const sandboxes = listSandboxes(db);
|
|
171
|
+
|
|
172
|
+
if (options.json) {
|
|
173
|
+
console.log(JSON.stringify(sandboxes, null, 2));
|
|
174
|
+
} else if (sandboxes.length === 0) {
|
|
175
|
+
logger.info("No active sandboxes.");
|
|
176
|
+
} else {
|
|
177
|
+
logger.log(chalk.bold(`Active Sandboxes (${sandboxes.length}):\n`));
|
|
178
|
+
for (const s of sandboxes) {
|
|
179
|
+
logger.log(` ${chalk.cyan(s.id)}`);
|
|
180
|
+
logger.log(
|
|
181
|
+
` Agent: ${s.agentId} Status: ${chalk.green(s.status)}`,
|
|
182
|
+
);
|
|
183
|
+
logger.log(
|
|
184
|
+
` CPU: ${s.resourceUsage.cpu}/${s.quota.cpu} Memory: ${s.resourceUsage.memory}/${s.quota.memory}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await shutdown();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.error(`Failed: ${err.message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// sandbox audit [sandbox-id]
|
|
197
|
+
sandbox
|
|
198
|
+
.command("audit")
|
|
199
|
+
.description("Show audit log for sandboxes")
|
|
200
|
+
.argument("[sandbox-id]", "Optional sandbox ID to filter")
|
|
201
|
+
.option("--action <name>", "Filter by action type")
|
|
202
|
+
.option("--limit <n>", "Limit entries", parseInt)
|
|
203
|
+
.option("--json", "Output as JSON")
|
|
204
|
+
.action(async (sandboxId, options) => {
|
|
205
|
+
try {
|
|
206
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
207
|
+
if (!ctx.db) {
|
|
208
|
+
logger.error("Database not available");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const db = ctx.db.getDatabase();
|
|
212
|
+
|
|
213
|
+
const entries = getAuditLog(db, sandboxId || null, {
|
|
214
|
+
action: options.action,
|
|
215
|
+
limit: options.limit,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (options.json) {
|
|
219
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
220
|
+
} else if (entries.length === 0) {
|
|
221
|
+
logger.info("No audit entries found.");
|
|
222
|
+
} else {
|
|
223
|
+
logger.log(chalk.bold(`Audit Log (${entries.length} entries):\n`));
|
|
224
|
+
for (const e of entries) {
|
|
225
|
+
const ts = chalk.gray(e.timestamp);
|
|
226
|
+
const action = chalk.yellow(e.action);
|
|
227
|
+
logger.log(` ${ts} ${action} sandbox=${e.sandboxId}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await shutdown();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
logger.error(`Failed: ${err.message}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// sandbox quota <sandbox-id>
|
|
239
|
+
sandbox
|
|
240
|
+
.command("quota")
|
|
241
|
+
.description("Show or set sandbox quota")
|
|
242
|
+
.argument("<sandbox-id>", "Sandbox ID")
|
|
243
|
+
.option("--cpu <n>", "Set CPU quota", parseInt)
|
|
244
|
+
.option("--memory <n>", "Set memory quota in MB", parseInt)
|
|
245
|
+
.option("--json", "Output as JSON")
|
|
246
|
+
.action(async (sandboxId, options) => {
|
|
247
|
+
try {
|
|
248
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
249
|
+
if (!ctx.db) {
|
|
250
|
+
logger.error("Database not available");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const db = ctx.db.getDatabase();
|
|
254
|
+
|
|
255
|
+
if (options.cpu || options.memory) {
|
|
256
|
+
const current = getSandbox(db, sandboxId);
|
|
257
|
+
if (!current) {
|
|
258
|
+
logger.error("Sandbox not found");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const newQuota = { ...current.quota };
|
|
262
|
+
if (options.cpu) newQuota.cpu = options.cpu;
|
|
263
|
+
if (options.memory) newQuota.memory = options.memory * 1024 * 1024;
|
|
264
|
+
|
|
265
|
+
const result = setQuota(db, sandboxId, newQuota);
|
|
266
|
+
if (options.json) {
|
|
267
|
+
console.log(JSON.stringify(result, null, 2));
|
|
268
|
+
} else {
|
|
269
|
+
logger.log(chalk.green("Quota updated."));
|
|
270
|
+
logger.log(
|
|
271
|
+
` CPU: ${newQuota.cpu} Memory: ${(newQuota.memory / 1024 / 1024).toFixed(0)}MB`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
const info = getSandbox(db, sandboxId);
|
|
276
|
+
if (!info) {
|
|
277
|
+
logger.error("Sandbox not found");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
if (options.json) {
|
|
281
|
+
console.log(
|
|
282
|
+
JSON.stringify(
|
|
283
|
+
{ quota: info.quota, resourceUsage: info.resourceUsage },
|
|
284
|
+
null,
|
|
285
|
+
2,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
logger.log(chalk.bold("Quota:"));
|
|
290
|
+
logger.log(
|
|
291
|
+
` CPU: ${info.resourceUsage.cpu} / ${info.quota.cpu}`,
|
|
292
|
+
);
|
|
293
|
+
logger.log(
|
|
294
|
+
` Memory: ${info.resourceUsage.memory} / ${info.quota.memory}`,
|
|
295
|
+
);
|
|
296
|
+
logger.log(
|
|
297
|
+
` Storage: ${info.resourceUsage.storage} / ${info.quota.storage}`,
|
|
298
|
+
);
|
|
299
|
+
logger.log(
|
|
300
|
+
` Network: ${info.resourceUsage.network} / ${info.quota.network}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await shutdown();
|
|
306
|
+
} catch (err) {
|
|
307
|
+
logger.error(`Failed: ${err.message}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// sandbox monitor <sandbox-id>
|
|
313
|
+
sandbox
|
|
314
|
+
.command("monitor")
|
|
315
|
+
.description("Monitor sandbox behavior and detect suspicious patterns")
|
|
316
|
+
.argument("<sandbox-id>", "Sandbox ID to monitor")
|
|
317
|
+
.option("--json", "Output as JSON")
|
|
318
|
+
.action(async (sandboxId, options) => {
|
|
319
|
+
try {
|
|
320
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
321
|
+
if (!ctx.db) {
|
|
322
|
+
logger.error("Database not available");
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const db = ctx.db.getDatabase();
|
|
326
|
+
const spinner = ora("Analyzing behavior...").start();
|
|
327
|
+
|
|
328
|
+
const result = monitorBehavior(db, sandboxId);
|
|
329
|
+
spinner.succeed("Analysis complete");
|
|
330
|
+
|
|
331
|
+
if (options.json) {
|
|
332
|
+
console.log(JSON.stringify(result, null, 2));
|
|
333
|
+
} else {
|
|
334
|
+
logger.log(chalk.bold("Behavior Analysis:"));
|
|
335
|
+
logger.log(` Total Events: ${result.totalEvents}`);
|
|
336
|
+
const riskColor =
|
|
337
|
+
result.riskScore > 50
|
|
338
|
+
? chalk.red
|
|
339
|
+
: result.riskScore > 20
|
|
340
|
+
? chalk.yellow
|
|
341
|
+
: chalk.green;
|
|
342
|
+
logger.log(` Risk Score: ${riskColor(result.riskScore)}/100`);
|
|
343
|
+
|
|
344
|
+
if (result.patterns.length > 0) {
|
|
345
|
+
logger.log(chalk.bold("\n Detected Patterns:"));
|
|
346
|
+
for (const p of result.patterns) {
|
|
347
|
+
const sev =
|
|
348
|
+
p.severity === "high"
|
|
349
|
+
? chalk.red(p.severity)
|
|
350
|
+
: chalk.yellow(p.severity);
|
|
351
|
+
logger.log(
|
|
352
|
+
` - ${p.type} (count: ${p.count}, severity: ${sev})`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
logger.log(chalk.green("\n No suspicious patterns detected."));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await shutdown();
|
|
361
|
+
} catch (err) {
|
|
362
|
+
logger.error(`Failed: ${err.message}`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG / hybrid search commands
|
|
3
|
+
* chainlesschain search <query> [--mode vector|bm25|hybrid] [--top-k <n>]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import ora from "ora";
|
|
8
|
+
import { logger } from "../lib/logger.js";
|
|
9
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
10
|
+
import { BM25Search } from "../lib/bm25-search.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensure the notes table exists (same as note.js)
|
|
14
|
+
*/
|
|
15
|
+
function ensureNotesTable(db) {
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
title TEXT NOT NULL,
|
|
20
|
+
content TEXT DEFAULT '',
|
|
21
|
+
tags TEXT DEFAULT '[]',
|
|
22
|
+
category TEXT DEFAULT 'general',
|
|
23
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
24
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
25
|
+
deleted_at TEXT DEFAULT NULL
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load all notes from DB for indexing
|
|
32
|
+
*/
|
|
33
|
+
function loadNotes(db) {
|
|
34
|
+
ensureNotesTable(db);
|
|
35
|
+
return db
|
|
36
|
+
.prepare(
|
|
37
|
+
"SELECT id, title, content, tags, category, created_at FROM notes WHERE deleted_at IS NULL",
|
|
38
|
+
)
|
|
39
|
+
.all();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Simple vector embedding using character/word frequency (fallback when no LLM)
|
|
44
|
+
* Produces a 128-dim feature vector
|
|
45
|
+
*/
|
|
46
|
+
function simpleEmbed(text) {
|
|
47
|
+
if (!text) return new Array(128).fill(0);
|
|
48
|
+
|
|
49
|
+
const normalized = text.toLowerCase();
|
|
50
|
+
const vec = new Array(128).fill(0);
|
|
51
|
+
|
|
52
|
+
// Character frequency features (first 64 dims)
|
|
53
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
54
|
+
const code = normalized.charCodeAt(i);
|
|
55
|
+
vec[code % 64] += 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Word-level features (next 64 dims)
|
|
59
|
+
const words = normalized.split(/\s+/);
|
|
60
|
+
for (const word of words) {
|
|
61
|
+
let hash = 0;
|
|
62
|
+
for (let i = 0; i < word.length; i++) {
|
|
63
|
+
hash = (hash * 31 + word.charCodeAt(i)) & 0x7fffffff;
|
|
64
|
+
}
|
|
65
|
+
vec[64 + (hash % 64)] += 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalize
|
|
69
|
+
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0)) || 1;
|
|
70
|
+
return vec.map((v) => v / mag);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cosineSimilarity(a, b) {
|
|
74
|
+
let dot = 0;
|
|
75
|
+
let magA = 0;
|
|
76
|
+
let magB = 0;
|
|
77
|
+
for (let i = 0; i < a.length; i++) {
|
|
78
|
+
dot += a[i] * b[i];
|
|
79
|
+
magA += a[i] * a[i];
|
|
80
|
+
magB += b[i] * b[i];
|
|
81
|
+
}
|
|
82
|
+
return dot / (Math.sqrt(magA) * Math.sqrt(magB) || 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Vector search using simple embeddings
|
|
87
|
+
*/
|
|
88
|
+
function vectorSearch(query, notes, topK) {
|
|
89
|
+
const queryVec = simpleEmbed(query);
|
|
90
|
+
const scored = notes.map((note) => {
|
|
91
|
+
const text = [note.title || "", note.content || ""].join(" ");
|
|
92
|
+
const noteVec = simpleEmbed(text);
|
|
93
|
+
return {
|
|
94
|
+
id: note.id,
|
|
95
|
+
score: cosineSimilarity(queryVec, noteVec),
|
|
96
|
+
doc: note,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
scored.sort((a, b) => b.score - a.score);
|
|
100
|
+
return scored.slice(0, topK);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reciprocal Rank Fusion to combine results from multiple methods
|
|
105
|
+
*/
|
|
106
|
+
function rrfFusion(resultSets, k = 60) {
|
|
107
|
+
const scores = new Map();
|
|
108
|
+
|
|
109
|
+
for (const results of resultSets) {
|
|
110
|
+
for (let rank = 0; rank < results.length; rank++) {
|
|
111
|
+
const id = results[rank].id;
|
|
112
|
+
const rrfScore = 1 / (k + rank + 1);
|
|
113
|
+
scores.set(id, (scores.get(id) || 0) + rrfScore);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get original docs
|
|
118
|
+
const allDocs = new Map();
|
|
119
|
+
for (const results of resultSets) {
|
|
120
|
+
for (const r of results) {
|
|
121
|
+
if (!allDocs.has(r.id)) {
|
|
122
|
+
allDocs.set(r.id, r.doc);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fused = Array.from(scores.entries()).map(([id, score]) => ({
|
|
128
|
+
id,
|
|
129
|
+
score,
|
|
130
|
+
doc: allDocs.get(id),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
fused.sort((a, b) => b.score - a.score);
|
|
134
|
+
return fused;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function registerSearchCommand(program) {
|
|
138
|
+
program
|
|
139
|
+
.command("search")
|
|
140
|
+
.description("Search knowledge base (BM25 + vector hybrid)")
|
|
141
|
+
.argument("<query>", "Search query")
|
|
142
|
+
.option("--mode <mode>", "Search mode: bm25, vector, hybrid", "hybrid")
|
|
143
|
+
.option("--top-k <n>", "Number of results", "10")
|
|
144
|
+
.option("--threshold <n>", "Minimum score threshold", "0")
|
|
145
|
+
.option("--json", "Output as JSON")
|
|
146
|
+
.action(async (query, options) => {
|
|
147
|
+
const spinner = ora("Searching...").start();
|
|
148
|
+
try {
|
|
149
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
150
|
+
if (!ctx.db) {
|
|
151
|
+
spinner.fail("Database not available");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const db = ctx.db.getDatabase();
|
|
156
|
+
const notes = loadNotes(db);
|
|
157
|
+
|
|
158
|
+
if (notes.length === 0) {
|
|
159
|
+
spinner.info("No notes in knowledge base. Use 'note add' first.");
|
|
160
|
+
await shutdown();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const topK = Math.max(1, parseInt(options.topK) || 10);
|
|
165
|
+
const threshold = parseFloat(options.threshold) || 0;
|
|
166
|
+
const mode = options.mode;
|
|
167
|
+
let results = [];
|
|
168
|
+
|
|
169
|
+
if (mode === "bm25") {
|
|
170
|
+
const bm25 = new BM25Search();
|
|
171
|
+
bm25.indexDocuments(notes);
|
|
172
|
+
results = bm25.search(query, { topK, threshold });
|
|
173
|
+
} else if (mode === "vector") {
|
|
174
|
+
results = vectorSearch(query, notes, topK);
|
|
175
|
+
if (threshold > 0) {
|
|
176
|
+
results = results.filter((r) => r.score >= threshold);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// Hybrid: BM25 + vector → RRF fusion
|
|
180
|
+
const bm25 = new BM25Search();
|
|
181
|
+
bm25.indexDocuments(notes);
|
|
182
|
+
const bm25Results = bm25.search(query, { topK: topK * 2 });
|
|
183
|
+
const vecResults = vectorSearch(query, notes, topK * 2);
|
|
184
|
+
results = rrfFusion([bm25Results, vecResults]).slice(0, topK);
|
|
185
|
+
if (threshold > 0) {
|
|
186
|
+
results = results.filter((r) => r.score >= threshold);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
spinner.stop();
|
|
191
|
+
|
|
192
|
+
if (options.json) {
|
|
193
|
+
console.log(
|
|
194
|
+
JSON.stringify(
|
|
195
|
+
results.map((r) => ({
|
|
196
|
+
id: r.id,
|
|
197
|
+
score: r.score,
|
|
198
|
+
title: r.doc.title,
|
|
199
|
+
category: r.doc.category,
|
|
200
|
+
created_at: r.doc.created_at,
|
|
201
|
+
snippet: (r.doc.content || "").substring(0, 200),
|
|
202
|
+
})),
|
|
203
|
+
null,
|
|
204
|
+
2,
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
} else if (results.length === 0) {
|
|
208
|
+
logger.info(`No results for "${query}"`);
|
|
209
|
+
} else {
|
|
210
|
+
logger.log(
|
|
211
|
+
chalk.bold(
|
|
212
|
+
`Search results for "${query}" (${results.length}, mode: ${mode}):\n`,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
for (const r of results) {
|
|
216
|
+
const tags = JSON.parse(r.doc.tags || "[]");
|
|
217
|
+
const tagStr =
|
|
218
|
+
tags.length > 0 ? chalk.gray(` [${tags.join(", ")}]`) : "";
|
|
219
|
+
const snippet = (r.doc.content || "")
|
|
220
|
+
.substring(0, 120)
|
|
221
|
+
.replace(/\n/g, " ");
|
|
222
|
+
logger.log(
|
|
223
|
+
` ${chalk.yellow(r.score.toFixed(4))} ${chalk.gray(r.id.slice(0, 8))} ${chalk.white(r.doc.title)}${tagStr}`,
|
|
224
|
+
);
|
|
225
|
+
if (snippet) {
|
|
226
|
+
logger.log(` ${chalk.gray(snippet)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await shutdown();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
spinner.fail(`Search failed: ${err.message}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|