monomind 2.3.4 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "description": "Open-source CLI extension for Claude Code. Adds an MCP server with a codebase knowledge graph, persistent memory, multi-agent coordination, and reusable slash commands. MIT licensed, runs locally, no data leaves your machine.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -368,7 +368,10 @@ module.exports = {
368
368
  if (sbResp.ok) {
369
369
  var sbData = await sbResp.json();
370
370
  if (sbData && Array.isArray(sbData.results) && sbData.results.length > 0) {
371
- sbHits = sbData.results.map(function(r) { return { key: r.key, value: r.content, score: r.score, metadata: {} }; });
371
+ sbHits = sbData.results.map(function(r) {
372
+ var src = (r.tags || []).find(function(t) { return t.indexOf('src:') === 0; });
373
+ return { key: r.key, value: r.content, score: r.score, global: !!r.global, metadata: src ? { filePath: src.slice(4) } : {} };
374
+ });
372
375
  sbMethod = sbData.method === 'semantic' ? 'semantic' : 'keyword';
373
376
  }
374
377
  }
@@ -415,7 +418,7 @@ module.exports = {
415
418
  var sbH = sbHits[sbI];
416
419
  var sbSrc = (sbH.metadata && sbH.metadata.filePath) ? String(sbH.metadata.filePath).split('/').slice(-2).join('/') : sbH.key;
417
420
  var sbText = String(sbH.value || '').replace(/\s+/g, ' ').slice(0, 240);
418
- sbLines.push(' • [' + sbSrc + '] ' + sbText);
421
+ sbLines.push(' • [' + sbSrc + (sbH.global ? ' · global' : '') + '] ' + sbText);
419
422
  }
420
423
  sbLines.push(' (deeper lookup: mcp__monomind__knowledge_search or `monomind doc search -q "..."`)');
421
424
  console.log(sbLines.join('\n'));
@@ -3,22 +3,41 @@
3
3
  */
4
4
  import * as path from 'node:path';
5
5
  import { output } from '../output.js';
6
+ import { getGlobalBrainDir } from '../memory/memory-bridge.js';
6
7
  const ingestCommand = {
7
8
  name: 'ingest',
8
9
  description: 'Ingest documents into the knowledge base',
9
10
  options: [
10
- { name: 'scope', short: 's', description: 'Knowledge scope (default: shared)', type: 'string', default: 'shared' },
11
+ // No `default` on scope: the auto-global routing below must be able to
12
+ // tell "user typed --scope shared" apart from "user typed nothing" — a
13
+ // parser-injected default makes them indistinguishable.
14
+ { name: 'scope', short: 's', description: 'Knowledge scope (default: shared; auto-routes to global for paths outside the project)', type: 'string' },
15
+ { name: 'global', short: 'g', description: 'Ingest into the personal cross-project global brain (~/.monomind/global-brain)', type: 'boolean' },
11
16
  ],
12
17
  examples: [
13
18
  { command: 'monomind doc ingest ./docs', description: 'Ingest all docs in a directory' },
19
+ { command: 'monomind doc ingest ~/notes --global', description: 'Ingest into the global brain (auto-detected for paths outside the project)' },
14
20
  { command: 'monomind doc ingest report.pdf', description: 'Ingest a single file' },
15
21
  ],
16
22
  action: async (ctx) => {
17
23
  const target = ctx.args[0] || '.';
18
- const scope = String(ctx.flags.scope || 'shared');
19
24
  const { ingestDocument, ingestDirectory } = await import('../knowledge/document-pipeline.js');
20
25
  const fs = await import('node:fs');
21
26
  const resolved = path.resolve(target);
27
+ // Zero-decision routing: an explicit --global wins; otherwise paths OUTSIDE
28
+ // the current project belong to the personal brain (a project store keyed
29
+ // to this cwd would never see them again from another project).
30
+ let scope = String(ctx.flags.scope || 'shared');
31
+ if (ctx.flags.global === true) {
32
+ scope = 'global';
33
+ }
34
+ else if (!ctx.flags.scope) {
35
+ const relToCwd = path.relative(process.cwd(), resolved);
36
+ if (relToCwd.startsWith('..') || path.isAbsolute(relToCwd)) {
37
+ scope = 'global';
38
+ output.writeln(output.dim(` ${target} is outside this project — ingesting into the global brain (use --scope shared to force project scope)`));
39
+ }
40
+ }
22
41
  const spinner = output.createSpinner({ text: 'Indexing documents...' });
23
42
  spinner.start();
24
43
  try {
@@ -68,9 +87,11 @@ const searchDocCommand = {
68
87
  { name: 'limit', short: 'l', description: 'Max results (default: 10)', type: 'number', default: 10 },
69
88
  { name: 'scope', short: 's', description: 'Knowledge scope (default: shared)', type: 'string', default: 'shared' },
70
89
  { name: 'min-score', description: 'Minimum similarity (default: 0.3)', type: 'number', default: 0.3 },
90
+ { name: 'store', description: 'Which store(s): project | global | all (default: all — project results win ties)', type: 'string', default: 'all' },
71
91
  ],
72
92
  examples: [
73
- { command: 'monomind doc search -q "authentication flow"', description: 'Search for auth docs' },
93
+ { command: 'monomind doc search -q "authentication flow"', description: 'Search project + global brain' },
94
+ { command: 'monomind doc search -q "pricing notes" --store global', description: 'Search only the personal global brain' },
74
95
  ],
75
96
  action: async (ctx) => {
76
97
  const query = String(ctx.flags.query || ctx.args[0] || '');
@@ -79,10 +100,12 @@ const searchDocCommand = {
79
100
  return { success: false, exitCode: 1 };
80
101
  }
81
102
  const { searchKnowledge } = await import('../knowledge/document-pipeline.js');
103
+ const storeFlag = String(ctx.flags.store || 'all');
82
104
  const excerpts = await searchKnowledge(query, {
83
105
  scope: String(ctx.flags.scope || 'shared'),
84
106
  limit: Number(ctx.flags.limit || 10),
85
107
  minScore: Number(ctx.flags['min-score'] || 0.3),
108
+ store: storeFlag === 'project' || storeFlag === 'global' ? storeFlag : 'all',
86
109
  });
87
110
  if (!excerpts.length) {
88
111
  output.writeln(output.dim('No results found.'));
@@ -92,7 +115,8 @@ const searchDocCommand = {
92
115
  output.writeln();
93
116
  for (let i = 0; i < excerpts.length; i++) {
94
117
  const ex = excerpts[i];
95
- output.writeln(`${output.highlight(`${i + 1}.`)} ${output.dim(`(${ex.similarity.toFixed(3)})`)} ${ex.filePath || 'unknown'}`);
118
+ const origin = ex.scope === 'global' ? ` ${output.dim('[global]')}` : '';
119
+ output.writeln(`${output.highlight(`${i + 1}.`)} ${output.dim(`(${ex.similarity.toFixed(3)})`)} ${ex.filePath || 'unknown'}${origin}`);
96
120
  const preview = ex.text.length > 200 ? ex.text.slice(0, 200) + '...' : ex.text;
97
121
  output.writeln(` ${output.dim(preview)}`);
98
122
  output.writeln();
@@ -105,11 +129,13 @@ const listDocCommand = {
105
129
  description: 'List indexed documents',
106
130
  options: [
107
131
  { name: 'scope', short: 's', description: 'Knowledge scope', type: 'string' },
132
+ { name: 'global', short: 'g', description: 'List the personal cross-project global brain', type: 'boolean' },
108
133
  ],
109
134
  action: async (ctx) => {
110
135
  const { listDocuments } = await import('../knowledge/document-pipeline.js');
111
- const scope = ctx.flags.scope ? String(ctx.flags.scope) : undefined;
112
- const docs = listDocuments(process.cwd(), scope);
136
+ const isGlobal = ctx.flags.global === true;
137
+ const scope = isGlobal ? 'global' : ctx.flags.scope ? String(ctx.flags.scope) : undefined;
138
+ const docs = listDocuments(isGlobal ? getGlobalBrainDir() : process.cwd(), scope);
113
139
  if (!docs.length) {
114
140
  output.writeln(output.dim('No documents indexed. Run: monomind doc ingest <path>'));
115
141
  return { success: true, data: [] };
@@ -133,15 +159,17 @@ const exportDocCommand = {
133
159
  options: [
134
160
  { name: 'output', short: 'o', description: 'Output directory', type: 'string', default: '.monomind/knowledge-export' },
135
161
  { name: 'scope', short: 's', description: 'Knowledge scope (default: shared)', type: 'string', default: 'shared' },
162
+ { name: 'global', short: 'g', description: 'Export the personal cross-project global brain (portable between machines)', type: 'boolean' },
136
163
  ],
137
164
  action: async (ctx) => {
138
165
  const { exportToOKF } = await import('../knowledge/document-pipeline.js');
139
166
  const outDir = path.resolve(String(ctx.flags.output || '.monomind/knowledge-export'));
140
- const scope = String(ctx.flags.scope || 'shared');
167
+ const isGlobal = ctx.flags.global === true;
168
+ const scope = isGlobal ? 'global' : String(ctx.flags.scope || 'shared');
141
169
  const spinner = output.createSpinner({ text: 'Exporting to OKF...' });
142
170
  spinner.start();
143
171
  try {
144
- const result = await exportToOKF(outDir, process.cwd(), scope);
172
+ const result = await exportToOKF(outDir, isGlobal ? getGlobalBrainDir() : process.cwd(), scope);
145
173
  spinner.succeed(`Exported ${result.exported} documents to ${result.outputDir}`);
146
174
  return { success: true, data: result };
147
175
  }
@@ -220,6 +220,13 @@ export class CLI {
220
220
  this.output.printDebug(`Completed in ${Date.now() - startTime}ms`);
221
221
  }
222
222
  if (result && !result.success) {
223
+ // Always surface the failure reason: many actions return
224
+ // { success: false, message } without logging, and exiting 1 with
225
+ // zero output left users guessing (swarm finding #12). Actions that
226
+ // already printed a rich error produce one extra terse summary line
227
+ // — acceptable; silence is not.
228
+ if (result.message)
229
+ this.output.printError(result.message);
223
230
  process.exit(result.exitCode || 1);
224
231
  }
225
232
  }
@@ -43,6 +43,8 @@ export declare function searchKnowledge(query: string, opts?: {
43
43
  limit?: number;
44
44
  minScore?: number;
45
45
  rootDir?: string;
46
+ /** which store(s): project-only, global-only, or both (default). */
47
+ store?: 'project' | 'global' | 'all';
46
48
  }): Promise<KnowledgeExcerpt[]>;
47
49
  export declare function listDocuments(rootDir?: string, scope?: string): DocumentMeta[];
48
50
  export declare function removeDocument(filePath: string, scope?: string, rootDir?: string): Promise<void>;
@@ -7,6 +7,14 @@
7
7
  * @module v1/cli/memory-bridge
8
8
  */
9
9
  export declare function safeParseEmbedding(raw: string | null | undefined): number[] | null;
10
+ /** The personal, cross-project knowledge store. Deliberately a SIBLING of
11
+ * ~/.monomind/projects (never inside it) so per-project pruning heuristics
12
+ * (`cleanup --data`) can never touch it. Env-overridable for tests and for
13
+ * users who keep their brain on a synced/external location. Resolved lazily
14
+ * so the override works regardless of import order. */
15
+ export declare function getGlobalBrainDir(): string;
16
+ /** Sentinel callers pass as dbPath to address the global brain. */
17
+ export declare const GLOBAL_BRAIN = "@global";
10
18
  /** Resolve the real on-disk LanceDB path for a given custom path (or the default). */
11
19
  export declare function bridgeGetDbPath(customPath?: string): string;
12
20
  export declare function bridgeStoreEntry(options: {
@@ -46,6 +54,7 @@ export declare function bridgeSearchEntries(options: {
46
54
  score: number;
47
55
  namespace: string;
48
56
  provenance?: string;
57
+ tags?: string[];
49
58
  }[];
50
59
  searchTime: number;
51
60
  searchMethod?: string;
@@ -70,22 +70,37 @@ function realOrResolved(p) {
70
70
  return p;
71
71
  }
72
72
  }
73
+ /** The personal, cross-project knowledge store. Deliberately a SIBLING of
74
+ * ~/.monomind/projects (never inside it) so per-project pruning heuristics
75
+ * (`cleanup --data`) can never touch it. Env-overridable for tests and for
76
+ * users who keep their brain on a synced/external location. Resolved lazily
77
+ * so the override works regardless of import order. */
78
+ export function getGlobalBrainDir() {
79
+ return process.env.MONOMIND_GLOBAL_BRAIN_DIR || path.join(os.homedir(), '.monomind', 'global-brain');
80
+ }
81
+ /** Sentinel callers pass as dbPath to address the global brain. */
82
+ export const GLOBAL_BRAIN = '@global';
73
83
  function getDbPath(customPath) {
74
84
  const defaultDir = path.join(projectDataDir(), 'lancedb');
75
85
  if (!customPath || customPath === ':memory:')
76
86
  return defaultDir;
87
+ if (customPath === GLOBAL_BRAIN)
88
+ return getGlobalBrainDir();
77
89
  // Treat legacy .db paths (and the legacy .swarm dir) as a signal to use the default
78
90
  if (customPath.endsWith('.db'))
79
91
  return defaultDir;
80
92
  const resolved = realOrResolved(path.resolve(customPath));
81
93
  // Guard against path traversal from MCP inputs: only allow paths inside the
82
- // project or inside the per-project home data dir.
94
+ // project, the per-project home data dir, or the global brain.
83
95
  const relCwd = path.relative(realOrResolved(process.cwd()), resolved);
84
96
  const relHome = path.relative(realOrResolved(projectDataDir()), resolved);
97
+ const relGlobal = path.relative(realOrResolved(getGlobalBrainDir()), resolved);
85
98
  if (!relCwd.startsWith('..') && !path.isAbsolute(relCwd))
86
99
  return resolved;
87
100
  if (!relHome.startsWith('..') && !path.isAbsolute(relHome))
88
101
  return resolved;
102
+ if (!relGlobal.startsWith('..') && !path.isAbsolute(relGlobal))
103
+ return resolved;
89
104
  return defaultDir;
90
105
  }
91
106
  /** Resolve the real on-disk LanceDB path for a given custom path (or the default). */
@@ -114,13 +129,10 @@ function getAutomemConfig() {
114
129
  function generateId(prefix) {
115
130
  return `${prefix}_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
116
131
  }
117
- // ===== Lazy singleton LanceDB backend =====
118
- let backendPromise = null;
119
- let backendInstance = null;
120
- let bridgeAvailable = null;
132
+ const backendSlots = new Map();
121
133
  let _embedder = null;
134
+ let _embedderPromise = null;
122
135
  const MAX_INIT_ATTEMPTS = 3;
123
- let initAttempts = 0;
124
136
  /** Flush after mutations: the sql.js fallback backend is in-memory WASM and
125
137
  * only reaches disk via persist(); the CLI process is short-lived, so waiting
126
138
  * for an auto-persist interval would lose writes. No-op on better-sqlite3. */
@@ -130,59 +142,73 @@ async function flushBackend(backend) {
130
142
  }
131
143
  catch { /* best effort */ }
132
144
  }
145
+ async function loadEmbedder() {
146
+ if (_embedder)
147
+ return;
148
+ if (!_embedderPromise) {
149
+ _embedderPromise = (async () => {
150
+ try {
151
+ const hf = await import('@huggingface/transformers');
152
+ // revision must be a git ref — 'main' is the HF default; 'default' 404s and
153
+ // silently killed embeddings (every search degraded to keyword matching)
154
+ // dtype pinned explicitly: transformers.js logs a "dtype not specified"
155
+ // warning to the console on every load otherwise (leaks into CLI output).
156
+ const extractor = await hf.pipeline('feature-extraction', BRIDGE_EMBEDDING_MODEL, { revision: 'main', dtype: 'fp32' });
157
+ _embedder = async (text) => {
158
+ const output = await extractor(text, { pooling: 'mean', normalize: true });
159
+ return new Float32Array(output.data);
160
+ };
161
+ }
162
+ catch (e) {
163
+ _embedderPromise = null; // allow retry (e.g. first call offline)
164
+ if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
165
+ console.error('[memory-bridge] embedding model failed to load — store and search without vectors:', e);
166
+ }
167
+ })();
168
+ }
169
+ await _embedderPromise;
170
+ }
133
171
  async function getBackend(dbPath) {
134
- if (bridgeAvailable === false)
172
+ const dir = getDbPath(dbPath);
173
+ let slot = backendSlots.get(dir);
174
+ if (!slot) {
175
+ slot = { promise: null, instance: null, available: null, attempts: 0 };
176
+ backendSlots.set(dir, slot);
177
+ }
178
+ if (slot.available === false)
135
179
  return null;
136
- if (initAttempts >= MAX_INIT_ATTEMPTS) {
137
- bridgeAvailable = false;
180
+ if (slot.attempts >= MAX_INIT_ATTEMPTS) {
181
+ slot.available = false;
138
182
  return null;
139
183
  }
140
- if (backendInstance)
141
- return backendInstance;
142
- if (!backendPromise) {
143
- backendPromise = (async () => {
184
+ if (slot.instance)
185
+ return slot.instance;
186
+ if (!slot.promise) {
187
+ slot.promise = (async () => {
144
188
  try {
145
189
  const mod = await import('@monoes/memory');
146
- // Try to create embedding generator from HuggingFace transformers
147
- let embeddingGenerator;
148
- try {
149
- const hf = await import('@huggingface/transformers');
150
- // revision must be a git ref — 'main' is the HF default; 'default' 404s and
151
- // silently killed embeddings (every search degraded to keyword matching)
152
- // dtype pinned explicitly: transformers.js logs a "dtype not specified"
153
- // warning to the console on every load otherwise (leaks into CLI output).
154
- const extractor = await hf.pipeline('feature-extraction', BRIDGE_EMBEDDING_MODEL, { revision: 'main', dtype: 'fp32' });
155
- embeddingGenerator = async (text) => {
156
- const output = await extractor(text, { pooling: 'mean', normalize: true });
157
- return new Float32Array(output.data);
158
- };
159
- _embedder = embeddingGenerator;
160
- }
161
- catch (e) {
162
- if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
163
- console.error('[memory-bridge] embedding model failed to load — store and search without vectors:', e);
164
- }
190
+ await loadEmbedder();
165
191
  // Local SQLite engine (LanceDB replaced 2026-07): better-sqlite3 when its
166
192
  // native binding loads, sql.js (pure WASM) otherwise — both persist text
167
193
  // AND embeddings, so vectors are always recomputable/derivable data.
168
- // getDbPath keeps its traversal guard and directory semantics; the SQLite
169
- // file lives inside that directory.
170
- const dir = getDbPath(dbPath);
171
194
  fs.mkdirSync(dir, { recursive: true });
172
195
  // Origin marker: records which project this data dir belongs to, so
173
196
  // `monomind cleanup --data` can verifiably prune dirs whose project
174
- // no longer exists (the dir-name hash is one-way). Best-effort.
175
- try {
176
- const originFile = path.join(projectDataDir(), 'origin.json');
177
- fs.writeFileSync(originFile, JSON.stringify({ path: path.resolve(process.cwd()), updatedAt: new Date().toISOString() }) + '\n', 'utf-8');
197
+ // no longer exists (the dir-name hash is one-way). Best-effort; never
198
+ // written for the global brain (it has no single origin project).
199
+ if (dir !== getGlobalBrainDir()) {
200
+ try {
201
+ const originFile = path.join(projectDataDir(), 'origin.json');
202
+ fs.writeFileSync(originFile, JSON.stringify({ path: path.resolve(process.cwd()), updatedAt: new Date().toISOString() }) + '\n', 'utf-8');
203
+ }
204
+ catch { /* non-fatal */ }
178
205
  }
179
- catch { /* non-fatal */ }
180
206
  const cfg = {
181
207
  databasePath: path.join(dir, 'memory.db'),
182
208
  walMode: true,
183
209
  optimize: true,
184
210
  defaultNamespace: 'default',
185
- embeddingGenerator,
211
+ embeddingGenerator: _embedder ?? undefined,
186
212
  };
187
213
  const origLog = console.log;
188
214
  console.log = (...args) => {
@@ -207,20 +233,20 @@ async function getBackend(dbPath) {
207
233
  finally {
208
234
  console.log = origLog;
209
235
  }
210
- backendInstance = backend;
211
- bridgeAvailable = true;
236
+ slot.instance = backend;
237
+ slot.available = true;
212
238
  return backend;
213
239
  }
214
240
  catch {
215
- initAttempts++;
216
- backendPromise = null;
217
- if (initAttempts >= MAX_INIT_ATTEMPTS)
218
- bridgeAvailable = false;
241
+ slot.attempts++;
242
+ slot.promise = null;
243
+ if (slot.attempts >= MAX_INIT_ATTEMPTS)
244
+ slot.available = false;
219
245
  return null;
220
246
  }
221
247
  })();
222
248
  }
223
- return backendPromise;
249
+ return slot.promise;
224
250
  }
225
251
  // ===== Core CRUD =====
226
252
  export async function bridgeStoreEntry(options) {
@@ -332,6 +358,7 @@ export async function bridgeSearchEntries(options) {
332
358
  score: r.score,
333
359
  namespace: r.entry.namespace,
334
360
  provenance: `semantic:${r.score.toFixed(3)}`,
361
+ tags: r.entry.tags ?? [],
335
362
  _createdAt: r.entry.createdAt || 0,
336
363
  }));
337
364
  searchMethod = 'semantic';
@@ -370,6 +397,7 @@ export async function bridgeSearchEntries(options) {
370
397
  score: Math.min(0.9, 0.3 + score * 0.6),
371
398
  namespace: e.namespace,
372
399
  provenance: `keyword:${score.toFixed(2)}`,
400
+ tags: e.tags ?? [],
373
401
  _createdAt: e.createdAt || 0,
374
402
  }));
375
403
  }
@@ -598,17 +626,17 @@ export async function getControllerRegistry(dbPath) {
598
626
  return getBackend(dbPath);
599
627
  }
600
628
  export async function shutdownBridge() {
601
- if (backendInstance) {
602
- try {
603
- await backendInstance.shutdown();
629
+ for (const slot of backendSlots.values()) {
630
+ if (slot.instance) {
631
+ try {
632
+ await slot.instance.shutdown();
633
+ }
634
+ catch { /* ignore */ }
604
635
  }
605
- catch { /* ignore */ }
606
636
  }
607
- backendInstance = null;
608
- backendPromise = null;
609
- bridgeAvailable = null;
637
+ backendSlots.clear();
610
638
  _embedder = null;
611
- initAttempts = 0;
639
+ _embedderPromise = null;
612
640
  }
613
641
  // ===== Pattern store =====
614
642
  export async function bridgeStorePattern(options) {
@@ -5950,11 +5950,21 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
5950
5950
  res.end('{"error":"knowledge bridge unavailable"}');
5951
5951
  return;
5952
5952
  }
5953
- const out = await bridge.bridgeSearchEntries({ query, namespace, limit });
5953
+ // scope: project | global | all (default all) project results get a
5954
+ // small tie boost; global hits are flagged so callers can show origin.
5955
+ const scope = payload.scope === 'project' || payload.scope === 'global' ? payload.scope : 'all';
5956
+ const [proj, glob] = await Promise.all([
5957
+ scope !== 'global' ? bridge.bridgeSearchEntries({ query, namespace, limit }).catch(() => null) : null,
5958
+ scope !== 'project' ? bridge.bridgeSearchEntries({ query, namespace: 'knowledge:global', limit, dbPath: '@global' }).catch(() => null) : null,
5959
+ ]);
5960
+ const merged = [
5961
+ ...(proj?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score + 0.05, global: false, tags: r.tags })),
5962
+ ...(glob?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score, global: true, tags: r.tags })),
5963
+ ].sort((a, b) => b.score - a.score).slice(0, limit);
5954
5964
  res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
5955
5965
  res.end(JSON.stringify({
5956
- method: out?.searchMethod || 'none',
5957
- results: (out?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score })),
5966
+ method: proj?.searchMethod || glob?.searchMethod || 'none',
5967
+ results: merged,
5958
5968
  }));
5959
5969
  } catch (err) {
5960
5970
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "2.3.4",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "CLI engine for Monomind — an open-source MCP server that extends Claude Code with a codebase knowledge graph (tree-sitter + SQLite), persistent memory, multi-agent task coordination, and session hooks. MIT licensed, fully local.",
6
6
  "main": "dist/src/index.js",