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 +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +5 -2
- package/packages/@monomind/cli/dist/src/commands/doc.js +36 -8
- package/packages/@monomind/cli/dist/src/index.js +7 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +2 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +0 -0
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +84 -56
- package/packages/@monomind/cli/dist/src/ui/server.mjs +13 -3
- package/packages/@monomind/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.
|
|
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) {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
112
|
-
const
|
|
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
|
|
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>;
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
137
|
-
|
|
180
|
+
if (slot.attempts >= MAX_INIT_ATTEMPTS) {
|
|
181
|
+
slot.available = false;
|
|
138
182
|
return null;
|
|
139
183
|
}
|
|
140
|
-
if (
|
|
141
|
-
return
|
|
142
|
-
if (!
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
211
|
-
|
|
236
|
+
slot.instance = backend;
|
|
237
|
+
slot.available = true;
|
|
212
238
|
return backend;
|
|
213
239
|
}
|
|
214
240
|
catch {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
608
|
-
backendPromise = null;
|
|
609
|
-
bridgeAvailable = null;
|
|
637
|
+
backendSlots.clear();
|
|
610
638
|
_embedder = null;
|
|
611
|
-
|
|
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
|
-
|
|
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:
|
|
5957
|
-
results:
|
|
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
|
+
"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",
|