monomind 2.4.0 → 2.5.1
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/README.md +2 -0
- package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +7 -4
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +3 -1
- 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 +83 -0
- package/packages/@monomind/cli/package.json +2 -2
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.1",
|
|
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",
|
|
@@ -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.
|
|
@@ -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() {
|
|
@@ -260,7 +260,9 @@ export async function bridgeStoreEntry(options) {
|
|
|
260
260
|
? options.value.slice(0, BRIDGE_MAX_VALUE_LEN) : options.value;
|
|
261
261
|
const namespace = options.namespace ?? 'default';
|
|
262
262
|
const tags = Array.isArray(options.tags)
|
|
263
|
-
|
|
263
|
+
// src: tags carry the ingest source path for excerpt provenance — paths
|
|
264
|
+
// routinely exceed the general 64-char tag cap, so they get 512.
|
|
265
|
+
? options.tags.filter(t => typeof t === 'string' && t.length > 0 && t.length <= (t.startsWith('src:') ? 512 : MAX_TAG_LEN)).slice(0, MAX_TAGS)
|
|
264
266
|
: [];
|
|
265
267
|
const now = Date.now();
|
|
266
268
|
const id = generateId('entry');
|
|
@@ -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,89 @@ 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 — the sweep below still covers it */ }
|
|
1073
|
+
|
|
1074
|
+
// Polling sweep backstop: fs.watch/fsevents silently stops delivering on
|
|
1075
|
+
// some volumes (exFAT/SMB — exactly where many projects live). Every 60s,
|
|
1076
|
+
// a bounded mtime walk ingests anything the watcher missed. Stat-only
|
|
1077
|
+
// when nothing changed; skips while a sweep is already running.
|
|
1078
|
+
try {
|
|
1079
|
+
const _sbDocExts2 = new Set(['.md', '.txt', '.pdf', '.docx']);
|
|
1080
|
+
const _sbSkipDirs = new Set(['node_modules', '.git', 'dist', '.monomind', '.claude', '.next', '__pycache__', '.venv', 'vendor']);
|
|
1081
|
+
const _sbSweepRoot = path.resolve(projectDir || process.cwd());
|
|
1082
|
+
let _sbLastSweep = Date.now();
|
|
1083
|
+
let _sbSweeping = false;
|
|
1084
|
+
const _sbSweepTimer = setInterval(async () => {
|
|
1085
|
+
if (_sbSweeping) return;
|
|
1086
|
+
_sbSweeping = true;
|
|
1087
|
+
const since = _sbLastSweep - 5000; // small overlap so boundary writes aren't missed
|
|
1088
|
+
_sbLastSweep = Date.now();
|
|
1089
|
+
try {
|
|
1090
|
+
const changed = [];
|
|
1091
|
+
let scanned = 0;
|
|
1092
|
+
const walk = (dir, depth) => {
|
|
1093
|
+
if (depth > 4 || scanned > 3000 || changed.length >= 20) return;
|
|
1094
|
+
let names;
|
|
1095
|
+
try { names = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
1096
|
+
for (const ent of names) {
|
|
1097
|
+
if (scanned++ > 3000 || changed.length >= 20) return;
|
|
1098
|
+
if (ent.name.startsWith('.') || _sbSkipDirs.has(ent.name)) continue;
|
|
1099
|
+
const full = path.join(dir, ent.name);
|
|
1100
|
+
if (ent.isDirectory()) { walk(full, depth + 1); continue; }
|
|
1101
|
+
if (!_sbDocExts2.has(path.extname(ent.name).toLowerCase())) continue;
|
|
1102
|
+
try { if (fs.statSync(full).mtimeMs > since) changed.push(full); } catch (_) {}
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
walk(_sbSweepRoot, 0);
|
|
1106
|
+
if (changed.length) {
|
|
1107
|
+
const pipeline = await import('../knowledge/document-pipeline.js');
|
|
1108
|
+
for (const f of changed) {
|
|
1109
|
+
try {
|
|
1110
|
+
const r = await pipeline.ingestDocument(f, 'shared', _sbSweepRoot);
|
|
1111
|
+
if (r.chunksIndexed > 0 && !r.skipped) console.log(`[knowledge] sweep-ingested ${path.basename(f)} (${r.chunksIndexed} chunks)`);
|
|
1112
|
+
} catch (_) { /* per-file failures never matter here */ }
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
} catch (_) { /* sweep is best-effort */ }
|
|
1116
|
+
finally { _sbSweeping = false; }
|
|
1117
|
+
}, 60_000);
|
|
1118
|
+
if (_sbSweepTimer.unref) _sbSweepTimer.unref();
|
|
1119
|
+
activeWatchers.push({ close: () => clearInterval(_sbSweepTimer) });
|
|
1120
|
+
} catch (_) { /* non-fatal */ }
|
|
1038
1121
|
}
|
|
1039
1122
|
} catch (_) { /* non-fatal */ }
|
|
1040
1123
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
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",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
},
|
|
106
106
|
"optionalDependencies": {
|
|
107
107
|
"@huggingface/transformers": "^3.8.1",
|
|
108
|
-
"@monoes/memory": "^1.0.
|
|
108
|
+
"@monoes/memory": "^1.0.10",
|
|
109
109
|
"@monomind/hooks": "*",
|
|
110
110
|
"@monomind/mcp": "*",
|
|
111
111
|
"@monomind/routing": "*",
|