monomind 2.3.4 → 2.5.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/README.md +2 -0
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +5 -2
- package/packages/@monomind/cli/README.md +2 -0
- 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/init/claudemd-generator.js +7 -4
- 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/orgrt/daemon.d.ts +8 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +22 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +2 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.js +5 -0
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +11 -2
- package/packages/@monomind/cli/dist/src/ui/server.mjs +48 -3
- package/packages/@monomind/cli/package.json +1 -1
package/README.md
CHANGED
|
@@ -223,6 +223,8 @@ monomind doc list # what's indexed
|
|
|
223
223
|
monomind doc export # portable OKF bundle — move your brain between machines
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
+
**And it follows you across projects.** Ingest a path from *outside* the current project (`monomind doc ingest ~/notes`, or add `--global`) and it lands in your personal global brain at `~/.monomind/global-brain` — searchable from every project on the machine. All retrieval (CLI search, per-prompt injection, the dashboard) merges both stores automatically, with project knowledge winning ties and global hits labeled `[global]`. `doc export --global` moves your whole brain between machines as an OKF bundle — still no cloud, ever.
|
|
227
|
+
|
|
226
228
|
Retrieval quality is a tested invariant, not a hope: a golden-set eval (paraphrase queries against notes written in different vocabulary) runs in CI with an 80% recall bar.
|
|
227
229
|
|
|
228
230
|
> **Privacy note:** the embedding model (~90MB) is fetched once from HuggingFace's CDN when your first document is indexed, then cached locally forever. That download is the only outbound request the Second Brain ever makes — your documents and queries never leave your machine. Offline at first index? Search degrades gracefully to keyword matching and `monomind doctor` tells you how to warm up later.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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'));
|
|
@@ -223,6 +223,8 @@ monomind doc list # what's indexed
|
|
|
223
223
|
monomind doc export # portable OKF bundle — move your brain between machines
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
+
**And it follows you across projects.** Ingest a path from *outside* the current project (`monomind doc ingest ~/notes`, or add `--global`) and it lands in your personal global brain at `~/.monomind/global-brain` — searchable from every project on the machine. All retrieval (CLI search, per-prompt injection, the dashboard) merges both stores automatically, with project knowledge winning ties and global hits labeled `[global]`. `doc export --global` moves your whole brain between machines as an OKF bundle — still no cloud, ever.
|
|
227
|
+
|
|
226
228
|
Retrieval quality is a tested invariant, not a hope: a golden-set eval (paraphrase queries against notes written in different vocabulary) runs in CI with an 80% recall bar.
|
|
227
229
|
|
|
228
230
|
> **Privacy note:** the embedding model (~90MB) is fetched once from HuggingFace's CDN when your first document is indexed, then cached locally forever. That download is the only outbound request the Second Brain ever makes — your documents and queries never leave your machine. Offline at first index? Search degrades gracefully to keyword matching and `monomind doctor` tells you how to warm up later.
|
|
@@ -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
|
}
|
|
@@ -399,12 +399,15 @@ If the \`documents\` capability is active (check \`.monomind/capabilities.json\`
|
|
|
399
399
|
|
|
400
400
|
**CLI access:**
|
|
401
401
|
\`\`\`bash
|
|
402
|
-
monomind doc search -q "your query" # Semantic search
|
|
403
|
-
monomind doc
|
|
404
|
-
monomind doc
|
|
405
|
-
monomind doc
|
|
402
|
+
monomind doc search -q "your query" # Semantic search (project + global brain merged)
|
|
403
|
+
monomind doc search -q "..." --store global # Personal global brain only
|
|
404
|
+
monomind doc list # List indexed docs (--global for the global brain)
|
|
405
|
+
monomind doc ingest ./path # Ingest new documents (paths outside the project auto-route to the global brain)
|
|
406
|
+
monomind doc export # Export as OKF bundle (--global to move your brain between machines)
|
|
406
407
|
\`\`\`
|
|
407
408
|
|
|
409
|
+
**Global brain:** the user has a personal cross-project knowledge store at \`~/.monomind/global-brain\`. All searches (knowledge_search, doc search, per-prompt injection) automatically merge it with project knowledge — project results win ties, global hits are labeled \`[global]\`. Cite the label so the user knows which brain answered.
|
|
410
|
+
|
|
408
411
|
**Re-indexing** happens automatically on session start (unchanged files are skipped via content hash).`;
|
|
409
412
|
}
|
|
410
413
|
function graphifySection() {
|
|
@@ -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) {
|
|
@@ -113,6 +113,14 @@ export declare class OrgDaemon {
|
|
|
113
113
|
* memory bridge (semantic when the local model is available, tokenized
|
|
114
114
|
* keyword otherwise). Failures return a message, never throw into the tool. */
|
|
115
115
|
private recallOrgMemory;
|
|
116
|
+
/** knowledge_search implementation for org agents: the user's Second Brain
|
|
117
|
+
* (this project's documents + the personal global brain), merged with the
|
|
118
|
+
* same project-first ranking every other surface uses. Failures return a
|
|
119
|
+
* message, never throw into the tool call. */
|
|
120
|
+
searchProjectKnowledge(query: string): Promise<{
|
|
121
|
+
text: string;
|
|
122
|
+
hits: number;
|
|
123
|
+
}>;
|
|
116
124
|
/** Persist the run's outcome into cross-run org memory so org_recall (and
|
|
117
125
|
* future runs) can find it by meaning, not just recency. Best-effort. */
|
|
118
126
|
private storeRunMemory;
|
|
@@ -88,6 +88,11 @@ export class OrgDaemon {
|
|
|
88
88
|
bus.emit({ type: 'status', from: r, reason: 'org-recall', msg: `recall: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
|
|
89
89
|
return answer.text;
|
|
90
90
|
},
|
|
91
|
+
searchKnowledge: async (r, q) => {
|
|
92
|
+
const answer = await this.searchProjectKnowledge(q);
|
|
93
|
+
bus.emit({ type: 'status', from: r, reason: 'knowledge-search', msg: `knowledge: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
|
|
94
|
+
return answer.text;
|
|
95
|
+
},
|
|
91
96
|
queryFn: this.opts.queryFn,
|
|
92
97
|
};
|
|
93
98
|
// Supervised session: transient crashes (provider blips, network) restart
|
|
@@ -502,6 +507,23 @@ export class OrgDaemon {
|
|
|
502
507
|
return { text: `org memory unavailable (${err instanceof Error ? err.message : 'error'})`, hits: 0 };
|
|
503
508
|
}
|
|
504
509
|
}
|
|
510
|
+
/** knowledge_search implementation for org agents: the user's Second Brain
|
|
511
|
+
* (this project's documents + the personal global brain), merged with the
|
|
512
|
+
* same project-first ranking every other surface uses. Failures return a
|
|
513
|
+
* message, never throw into the tool call. */
|
|
514
|
+
async searchProjectKnowledge(query) {
|
|
515
|
+
try {
|
|
516
|
+
const { searchKnowledge } = await import('../knowledge/document-pipeline.js');
|
|
517
|
+
const excerpts = await searchKnowledge(query, { rootDir: this.root, limit: 5, store: 'all' });
|
|
518
|
+
if (!excerpts.length)
|
|
519
|
+
return { text: 'No matching documents in the Second Brain for that query.', hits: 0 };
|
|
520
|
+
const text = excerpts.map((e, i) => `${i + 1}. [${e.filePath || 'unknown'}${e.scope === 'global' ? ' · global' : ''}] (${e.similarity.toFixed(2)})\n${e.text.slice(0, 800)}`).join('\n\n');
|
|
521
|
+
return { text, hits: excerpts.length };
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
return { text: `knowledge search unavailable (${err instanceof Error ? err.message : 'error'})`, hits: 0 };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
505
527
|
/** Persist the run's outcome into cross-run org memory so org_recall (and
|
|
506
528
|
* future runs) can find it by meaning, not just recency. Best-effort. */
|
|
507
529
|
async storeRunMemory(name, def, run, summary) {
|
|
@@ -17,6 +17,8 @@ export interface SessionOpts {
|
|
|
17
17
|
onComplete?: (role: string, outcome: 'achieved' | 'partial' | 'failed', summary: string) => void;
|
|
18
18
|
/** Search the org's accumulated cross-run memory (memory_namespace). */
|
|
19
19
|
recall?: (role: string, query: string) => Promise<string>;
|
|
20
|
+
/** Search the user's Second Brain (project documents + personal global brain). */
|
|
21
|
+
searchKnowledge?: (role: string, query: string) => Promise<string>;
|
|
20
22
|
def?: OrgDef;
|
|
21
23
|
maxTurns?: number;
|
|
22
24
|
queryFn?: typeof query;
|
|
@@ -15,6 +15,7 @@ export function buildRolePrompt(role, def, roster) {
|
|
|
15
15
|
`Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
|
|
16
16
|
`If you need a human decision, call ask_human with your question, then end your turn — you'll receive the human's answer as a new message when it arrives. Do not call ask_human for anything you can resolve yourself.`,
|
|
17
17
|
`Before starting substantial work, call org_recall to check what previous runs already learned or delivered — do not redo finished work.`,
|
|
18
|
+
`The user's documents (notes, handbooks, specs) are searchable with knowledge_search — ground your work in them instead of guessing; results labeled [global] come from the user's personal cross-project brain.`,
|
|
18
19
|
`When you receive a message, act on it, then org_send your result to the requester.`,
|
|
19
20
|
isCoordinator
|
|
20
21
|
? `When the org's goal for this run is achieved (or clearly can't be), call org_complete exactly once with the outcome and a concise summary of what was done — it is recorded in the org's run history and briefed to the next run. Then end your turn.`
|
|
@@ -56,6 +57,10 @@ async function runOneSession(opts) {
|
|
|
56
57
|
name: 'org',
|
|
57
58
|
version: '1.0.0',
|
|
58
59
|
tools: [
|
|
60
|
+
...(opts.searchKnowledge ? [tool('knowledge_search', 'Semantic search over the user\'s Second Brain: this project\'s indexed documents plus their personal cross-project global brain. Use to ground work in the user\'s actual notes, handbooks, and documents.', { query: z.string() }, async (args) => {
|
|
61
|
+
const text = await opts.searchKnowledge(role.id, args.query);
|
|
62
|
+
return { content: [{ type: 'text', text }] };
|
|
63
|
+
})] : []),
|
|
59
64
|
...(opts.recall ? [tool('org_recall', 'Search this org\'s accumulated memory from previous runs (outcomes, decisions, learnings). Use before starting work that may already have been done.', { query: z.string() }, async (args) => {
|
|
60
65
|
const text = await opts.recall(role.id, args.query);
|
|
61
66
|
return { content: [{ type: 'text', text }] };
|
|
@@ -1917,6 +1917,11 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
1917
1917
|
<div class="filter-bar" style="margin-bottom:6px">
|
|
1918
1918
|
<input class="filter-input" id="sb-search-input" type="text" placeholder="Ask your Second Brain — semantic search by meaning…"
|
|
1919
1919
|
onkeydown="if(event.key==='Enter')secondBrainSearch()">
|
|
1920
|
+
<select id="sb-search-scope" class="btn" title="Which brain to search" style="padding:3px 6px">
|
|
1921
|
+
<option value="all" selected>project + global</option>
|
|
1922
|
+
<option value="project">project only</option>
|
|
1923
|
+
<option value="global">global only</option>
|
|
1924
|
+
</select>
|
|
1920
1925
|
<button class="btn" onclick="secondBrainSearch()" title="Semantic search (warm local model)">⌕ Search</button>
|
|
1921
1926
|
</div>
|
|
1922
1927
|
<div id="sb-search-results" style="display:none;margin-bottom:12px"></div>
|
|
@@ -13609,10 +13614,11 @@ async function secondBrainSearch() {
|
|
|
13609
13614
|
box.style.display = 'block';
|
|
13610
13615
|
box.innerHTML = '<div class="loading-txt">Searching…</div>';
|
|
13611
13616
|
try {
|
|
13617
|
+
const scopeSel = document.getElementById('sb-search-scope');
|
|
13612
13618
|
const r = await fetch('/api/knowledge/search', {
|
|
13613
13619
|
method: 'POST',
|
|
13614
13620
|
headers: { 'Content-Type': 'application/json' },
|
|
13615
|
-
body: JSON.stringify({ query: q, limit: 5 }),
|
|
13621
|
+
body: JSON.stringify({ query: q, limit: 5, scope: scopeSel ? scopeSel.value : 'all' }),
|
|
13616
13622
|
});
|
|
13617
13623
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
13618
13624
|
const data = await r.json();
|
|
@@ -13625,8 +13631,11 @@ async function secondBrainSearch() {
|
|
|
13625
13631
|
'<div style="font-size:10px;letter-spacing:0.07em;text-transform:uppercase;color:var(--text-xs);margin-bottom:6px">' +
|
|
13626
13632
|
results.length + ' result(s) · ' + esc(data.method || '') + '</div>' +
|
|
13627
13633
|
results.map(function (res) {
|
|
13634
|
+
var srcTag = (res.tags || []).find(function (t) { return t.indexOf('src:') === 0; });
|
|
13635
|
+
var label = srcTag ? srcTag.slice(4).split('/').slice(-2).join('/') : (res.key || '');
|
|
13628
13636
|
return '<div style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;margin-bottom:6px">' +
|
|
13629
|
-
'<div style="font-size:10px;color:var(--text-lo);margin-bottom:3px">' + esc(
|
|
13637
|
+
'<div style="font-size:10px;color:var(--text-lo);margin-bottom:3px">' + esc(label) +
|
|
13638
|
+
(res.global ? ' <span style="color:var(--accent-dim,#888);border:1px solid var(--border);border-radius:4px;padding:0 4px">global</span>' : '') +
|
|
13630
13639
|
' <span style="color:var(--accent)">' + (typeof res.score === 'number' ? res.score.toFixed(2) : '') + '</span></div>' +
|
|
13631
13640
|
'<div style="font-size:12px;white-space:pre-wrap;max-height:120px;overflow:hidden">' + esc(String(res.content || '').slice(0, 600)) + '</div>' +
|
|
13632
13641
|
'</div>';
|
|
@@ -1035,6 +1035,41 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
1035
1035
|
.catch(() => { /* warm-up is best-effort */ });
|
|
1036
1036
|
}, 3000);
|
|
1037
1037
|
if (_warmTimer.unref) _warmTimer.unref();
|
|
1038
|
+
|
|
1039
|
+
// ── Second Brain live ingestion ──────────────────────────────────
|
|
1040
|
+
// This server is the one long-lived local process AND holds the warm
|
|
1041
|
+
// embedding model — so it watches for document changes and ingests
|
|
1042
|
+
// in-process within seconds, instead of waiting for the next session
|
|
1043
|
+
// start. Best-effort: recursive fs.watch is unsupported on some
|
|
1044
|
+
// platforms/volumes; the session-start reindex remains the backstop.
|
|
1045
|
+
try {
|
|
1046
|
+
const _sbDocExts = new Set(['.md', '.txt', '.pdf', '.docx']);
|
|
1047
|
+
const _sbSkip = /(^|\/)(node_modules|\.git|dist|\.monomind|\.claude|\.next|__pycache__|\.venv|vendor)(\/|$)/;
|
|
1048
|
+
const _sbPending = new Map(); // file -> debounce timer
|
|
1049
|
+
const _sbRoot = path.resolve(projectDir || process.cwd());
|
|
1050
|
+
const _sbWatcher = fs.watch(_sbRoot, { recursive: true }, (_evt, rel) => {
|
|
1051
|
+
try {
|
|
1052
|
+
if (!rel) return;
|
|
1053
|
+
const relStr = String(rel);
|
|
1054
|
+
if (_sbSkip.test(relStr) || relStr.startsWith('.')) return;
|
|
1055
|
+
if (!_sbDocExts.has(path.extname(relStr).toLowerCase())) return;
|
|
1056
|
+
const full = path.join(_sbRoot, relStr);
|
|
1057
|
+
clearTimeout(_sbPending.get(full));
|
|
1058
|
+
_sbPending.set(full, setTimeout(async () => {
|
|
1059
|
+
_sbPending.delete(full);
|
|
1060
|
+
try {
|
|
1061
|
+
if (!fs.existsSync(full)) return; // deleted — session-start reindex handles removal
|
|
1062
|
+
const pipeline = await import('../knowledge/document-pipeline.js');
|
|
1063
|
+
const r = await pipeline.ingestDocument(full, 'shared', _sbRoot);
|
|
1064
|
+
if (r.chunksIndexed > 0 && !r.skipped) {
|
|
1065
|
+
console.log(`[knowledge] live-ingested ${path.basename(full)} (${r.chunksIndexed} chunks)`);
|
|
1066
|
+
}
|
|
1067
|
+
} catch (_) { /* single-file ingest failure never matters here */ }
|
|
1068
|
+
}, 5000));
|
|
1069
|
+
} catch (_) { /* watcher callback must never throw */ }
|
|
1070
|
+
});
|
|
1071
|
+
activeWatchers.push(_sbWatcher);
|
|
1072
|
+
} catch (_) { /* recursive watch unavailable — session-start reindex covers it */ }
|
|
1038
1073
|
}
|
|
1039
1074
|
} catch (_) { /* non-fatal */ }
|
|
1040
1075
|
|
|
@@ -5950,11 +5985,21 @@ new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{col
|
|
|
5950
5985
|
res.end('{"error":"knowledge bridge unavailable"}');
|
|
5951
5986
|
return;
|
|
5952
5987
|
}
|
|
5953
|
-
|
|
5988
|
+
// scope: project | global | all (default all) — project results get a
|
|
5989
|
+
// small tie boost; global hits are flagged so callers can show origin.
|
|
5990
|
+
const scope = payload.scope === 'project' || payload.scope === 'global' ? payload.scope : 'all';
|
|
5991
|
+
const [proj, glob] = await Promise.all([
|
|
5992
|
+
scope !== 'global' ? bridge.bridgeSearchEntries({ query, namespace, limit }).catch(() => null) : null,
|
|
5993
|
+
scope !== 'project' ? bridge.bridgeSearchEntries({ query, namespace: 'knowledge:global', limit, dbPath: '@global' }).catch(() => null) : null,
|
|
5994
|
+
]);
|
|
5995
|
+
const merged = [
|
|
5996
|
+
...(proj?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score + 0.05, global: false, tags: r.tags })),
|
|
5997
|
+
...(glob?.results || []).map(r => ({ key: r.key, content: String(r.content || '').slice(0, 2000), score: r.score, global: true, tags: r.tags })),
|
|
5998
|
+
].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5954
5999
|
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
5955
6000
|
res.end(JSON.stringify({
|
|
5956
|
-
method:
|
|
5957
|
-
results:
|
|
6001
|
+
method: proj?.searchMethod || glob?.searchMethod || 'none',
|
|
6002
|
+
results: merged,
|
|
5958
6003
|
}));
|
|
5959
6004
|
} catch (err) {
|
|
5960
6005
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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",
|