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.
Files changed (84) hide show
  1. package/README.md +309 -19
  2. package/bin/chainlesschain.js +4 -0
  3. package/package.json +1 -1
  4. package/src/commands/a2a.js +374 -0
  5. package/src/commands/audit.js +286 -0
  6. package/src/commands/auth.js +387 -0
  7. package/src/commands/bi.js +240 -0
  8. package/src/commands/browse.js +184 -0
  9. package/src/commands/cowork.js +317 -0
  10. package/src/commands/did.js +376 -0
  11. package/src/commands/economy.js +375 -0
  12. package/src/commands/encrypt.js +233 -0
  13. package/src/commands/evolution.js +398 -0
  14. package/src/commands/export.js +125 -0
  15. package/src/commands/git.js +215 -0
  16. package/src/commands/hmemory.js +273 -0
  17. package/src/commands/hook.js +260 -0
  18. package/src/commands/import.js +259 -0
  19. package/src/commands/init.js +184 -0
  20. package/src/commands/instinct.js +202 -0
  21. package/src/commands/llm.js +155 -4
  22. package/src/commands/lowcode.js +320 -0
  23. package/src/commands/mcp.js +302 -0
  24. package/src/commands/memory.js +282 -0
  25. package/src/commands/note.js +187 -0
  26. package/src/commands/org.js +505 -0
  27. package/src/commands/p2p.js +274 -0
  28. package/src/commands/plugin.js +451 -0
  29. package/src/commands/sandbox.js +366 -0
  30. package/src/commands/search.js +237 -0
  31. package/src/commands/session.js +238 -0
  32. package/src/commands/skill.js +254 -201
  33. package/src/commands/sync.js +249 -0
  34. package/src/commands/tokens.js +214 -0
  35. package/src/commands/wallet.js +416 -0
  36. package/src/commands/workflow.js +359 -0
  37. package/src/commands/zkp.js +277 -0
  38. package/src/index.js +93 -1
  39. package/src/lib/a2a-protocol.js +371 -0
  40. package/src/lib/agent-coordinator.js +273 -0
  41. package/src/lib/agent-economy.js +369 -0
  42. package/src/lib/app-builder.js +377 -0
  43. package/src/lib/audit-logger.js +364 -0
  44. package/src/lib/bi-engine.js +299 -0
  45. package/src/lib/bm25-search.js +322 -0
  46. package/src/lib/browser-automation.js +216 -0
  47. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  48. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  49. package/src/lib/cowork/debate-review-cli.js +144 -0
  50. package/src/lib/cowork/decision-kb-cli.js +153 -0
  51. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  52. package/src/lib/cowork-adapter.js +106 -0
  53. package/src/lib/crypto-manager.js +246 -0
  54. package/src/lib/did-manager.js +270 -0
  55. package/src/lib/ensure-utf8.js +59 -0
  56. package/src/lib/evolution-system.js +508 -0
  57. package/src/lib/git-integration.js +220 -0
  58. package/src/lib/hierarchical-memory.js +471 -0
  59. package/src/lib/hook-manager.js +387 -0
  60. package/src/lib/instinct-manager.js +190 -0
  61. package/src/lib/knowledge-exporter.js +302 -0
  62. package/src/lib/knowledge-importer.js +293 -0
  63. package/src/lib/llm-providers.js +325 -0
  64. package/src/lib/mcp-client.js +413 -0
  65. package/src/lib/memory-manager.js +211 -0
  66. package/src/lib/note-versioning.js +244 -0
  67. package/src/lib/org-manager.js +424 -0
  68. package/src/lib/p2p-manager.js +317 -0
  69. package/src/lib/pdf-parser.js +96 -0
  70. package/src/lib/permission-engine.js +374 -0
  71. package/src/lib/plan-mode.js +333 -0
  72. package/src/lib/plugin-manager.js +430 -0
  73. package/src/lib/project-detector.js +53 -0
  74. package/src/lib/response-cache.js +156 -0
  75. package/src/lib/sandbox-v2.js +503 -0
  76. package/src/lib/service-container.js +183 -0
  77. package/src/lib/session-manager.js +189 -0
  78. package/src/lib/skill-loader.js +274 -0
  79. package/src/lib/sync-manager.js +347 -0
  80. package/src/lib/token-tracker.js +200 -0
  81. package/src/lib/wallet-manager.js +348 -0
  82. package/src/lib/workflow-engine.js +503 -0
  83. package/src/lib/zkp-engine.js +241 -0
  84. 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
+ }