crbro-memory 2.9.1 → 2.11.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/LICENSE +21 -21
- package/README.md +256 -456
- package/SECURITY.md +135 -8
- package/bin/crbro.mjs +555 -299
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +143 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +762 -311
- package/dist/engine/doctor.js +676 -0
- package/dist/engine/evidence.js +171 -0
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +50 -36
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +735 -144
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +126 -69
- package/dist/engine/projects.js +80 -0
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +34 -11
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +810 -176
- package/dist/search/quality.js +31 -0
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +649 -343
- package/dist/sync/git.js +203 -72
- package/dist/sync/materialize.js +33 -8
- package/dist/sync/space.js +286 -101
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +219 -52
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +12 -8
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -670
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -61
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -156
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -323
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -35
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -142
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -116
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -346
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -11
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── crbro-memory export ─────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// The brain as something the owner can open without CRBRO: an Obsidian-ready
|
|
5
|
+
// Markdown vault, or one JSON file that `import` reads back. Read-only from
|
|
6
|
+
// the brain folder (safe while a daemon or a client runs: nothing is locked,
|
|
7
|
+
// nothing is written there), every text passes through the same redaction
|
|
8
|
+
// learn applies, and the output can never land inside the brain.
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ENTRY_KINDS = exports.EXPORT_HELP = exports.VAULT_MARKER = exports.EXPORT_FORMAT_VERSION = exports.EXPORT_FORMAT = void 0;
|
|
14
|
+
exports.readBrain = readBrain;
|
|
15
|
+
exports.deepRedact = deepRedact;
|
|
16
|
+
exports.redactNeuron = redactNeuron;
|
|
17
|
+
exports.entriesOf = entriesOf;
|
|
18
|
+
exports.withoutRetired = withoutRetired;
|
|
19
|
+
exports.buildExport = buildExport;
|
|
20
|
+
exports.renderVault = renderVault;
|
|
21
|
+
exports.checkOutDir = checkOutDir;
|
|
22
|
+
exports.runExport = runExport;
|
|
23
|
+
exports.main = main;
|
|
24
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
25
|
+
const node_fs_1 = require("node:fs");
|
|
26
|
+
const brain_js_1 = require("../engine/brain.js");
|
|
27
|
+
const secrets_js_1 = require("../engine/secrets.js");
|
|
28
|
+
const ops_js_1 = require("../sync/ops.js");
|
|
29
|
+
const hash_js_1 = require("../utils/hash.js");
|
|
30
|
+
const version_js_1 = require("../version.js");
|
|
31
|
+
const common_js_1 = require("./common.js");
|
|
32
|
+
const markdown_js_1 = require("./markdown.js");
|
|
33
|
+
exports.EXPORT_FORMAT = 'crbro-export';
|
|
34
|
+
exports.EXPORT_FORMAT_VERSION = 1;
|
|
35
|
+
exports.VAULT_MARKER = '.crbro-export.json';
|
|
36
|
+
exports.EXPORT_HELP = `
|
|
37
|
+
crbro-memory export [--out <dir>] [--format md|json] [--include-retired]
|
|
38
|
+
|
|
39
|
+
Write the brain to a folder you can read without CRBRO. Read-only: the brain
|
|
40
|
+
is never written, so it is safe while a client or the daemon is running.
|
|
41
|
+
|
|
42
|
+
--out <dir> Where to write. Must be new or empty, and outside the
|
|
43
|
+
brain folder. Default: ./crbro-export-<date>-<time>
|
|
44
|
+
--format md|json md (default): an Obsidian-compatible vault, one note per
|
|
45
|
+
neuron with YAML frontmatter, entries grouped by kind,
|
|
46
|
+
synapses as [[wiki links]], one index note per domain,
|
|
47
|
+
the day logs and the open items.
|
|
48
|
+
json: crbro-export.json, a faithful dump that
|
|
49
|
+
\`crbro-memory import\` reads back.
|
|
50
|
+
--include-retired Also export superseded and retracted entries (marked).
|
|
51
|
+
|
|
52
|
+
Credentials are redacted exactly as crbro_learn does before anything is
|
|
53
|
+
written. The brain is the one crbro_* tools use: CRBRO_PATH, else ~/.crbro.
|
|
54
|
+
`;
|
|
55
|
+
async function readJsonFile(file) {
|
|
56
|
+
const raw = await node_fs_1.promises.readFile(file, 'utf8');
|
|
57
|
+
return JSON.parse(raw.replace(/^/, ''));
|
|
58
|
+
}
|
|
59
|
+
async function readDir(dir, root, unreadable) {
|
|
60
|
+
let names = [];
|
|
61
|
+
try {
|
|
62
|
+
names = (await node_fs_1.promises.readdir(dir)).filter(n => n.endsWith('.json')).sort();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const n of names) {
|
|
69
|
+
const file = node_path_1.default.join(dir, n);
|
|
70
|
+
try {
|
|
71
|
+
const st = await node_fs_1.promises.lstat(file);
|
|
72
|
+
if (!st.isFile())
|
|
73
|
+
continue;
|
|
74
|
+
const v = await readJsonFile(file);
|
|
75
|
+
if (v && typeof v === 'object')
|
|
76
|
+
out.push(v);
|
|
77
|
+
else
|
|
78
|
+
unreadable.push(node_path_1.default.relative(root, file));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
unreadable.push(node_path_1.default.relative(root, file));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
const looksLikeNeuron = (n) => n && typeof n.id === 'string' && typeof n.name === 'string';
|
|
87
|
+
/** Read the brain folder. Nothing is created, locked or written. */
|
|
88
|
+
async function readBrain(root) {
|
|
89
|
+
const paths = new brain_js_1.BrainPaths(root);
|
|
90
|
+
const unreadable = [];
|
|
91
|
+
const live = (await readDir(paths.cortex, root, unreadable)).filter(looksLikeNeuron);
|
|
92
|
+
const archived = (await readDir(paths.archives, root, unreadable)).filter(looksLikeNeuron)
|
|
93
|
+
.map(n => ({ ...n, archived: true }));
|
|
94
|
+
const synapses = (await readDir(paths.synapses, root, unreadable))
|
|
95
|
+
.filter(s => Array.isArray(s?.nodes) && s.nodes.length === 2);
|
|
96
|
+
const sessions = (await readDir(paths.hippocampus, root, unreadable))
|
|
97
|
+
.filter(s => typeof s?.session_id === 'string')
|
|
98
|
+
.sort((a, b) => String(b.date || b.session_id).localeCompare(String(a.date || a.session_id)));
|
|
99
|
+
let context = null;
|
|
100
|
+
try {
|
|
101
|
+
context = await readJsonFile(paths.activeContext());
|
|
102
|
+
}
|
|
103
|
+
catch { /* none yet */ }
|
|
104
|
+
return { neurons: [...live, ...archived], synapses, sessions, context, unreadable };
|
|
105
|
+
}
|
|
106
|
+
// ─── Redaction ───────────────────────────────────────────────────
|
|
107
|
+
/** Redact every string value, recursively. Keys are left alone. Counts the markers placed. */
|
|
108
|
+
function deepRedact(value, counter) {
|
|
109
|
+
if (typeof value === 'string') {
|
|
110
|
+
const r = (0, secrets_js_1.redact)(value);
|
|
111
|
+
counter.n += r.found.length;
|
|
112
|
+
return r.text;
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(value))
|
|
115
|
+
return value.map(v => deepRedact(v, counter));
|
|
116
|
+
if (value && typeof value === 'object') {
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const [k, v] of Object.entries(value))
|
|
119
|
+
out[k] = deepRedact(v, counter);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
const SIDECARS = ['entry_dates', 'entry_status', 'entry_source', 'entry_verified', 'entry_verification'];
|
|
125
|
+
/**
|
|
126
|
+
* Redact one neuron and keep it coherent: an entry whose text changed gets a
|
|
127
|
+
* new content id, so its sidecar keys (dates, retirement, verification…) move
|
|
128
|
+
* with it, and a fact's own id is recomputed.
|
|
129
|
+
*/
|
|
130
|
+
function redactNeuron(n, counter) {
|
|
131
|
+
const out = deepRedact(n, counter);
|
|
132
|
+
const rekey = (before, after) => {
|
|
133
|
+
(before || []).forEach((t, i) => {
|
|
134
|
+
const t2 = after?.[i];
|
|
135
|
+
if (t2 === undefined || t2 === t)
|
|
136
|
+
return;
|
|
137
|
+
const from = (0, ops_js_1.entryId)(t), to = (0, ops_js_1.entryId)(t2);
|
|
138
|
+
for (const s of SIDECARS) {
|
|
139
|
+
const map = out[s];
|
|
140
|
+
if (map && from in map) {
|
|
141
|
+
map[to] = map[from];
|
|
142
|
+
delete map[from];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
rekey(n.patterns, out.patterns);
|
|
148
|
+
rekey(n.preferences, out.preferences);
|
|
149
|
+
rekey(n.errors, out.errors);
|
|
150
|
+
rekey(n.debts, out.debts);
|
|
151
|
+
rekey((n.decisions || []).map(d => d.text), (out.decisions || []).map(d => d.text));
|
|
152
|
+
(n.facts || []).forEach((f, i) => {
|
|
153
|
+
const g = out.facts?.[i];
|
|
154
|
+
if (g && g.text !== f.text)
|
|
155
|
+
g.id = (0, hash_js_1.factId)(g.text);
|
|
156
|
+
});
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
exports.ENTRY_KINDS = ['fact', 'decision', 'pattern', 'preference', 'error', 'debt', 'map'];
|
|
160
|
+
/** Every entry of a neuron, active first by kind, with the metadata that sits in its sidecars. */
|
|
161
|
+
function entriesOf(n, includeRetired) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const f of n.facts || []) {
|
|
164
|
+
const status = (f.status || 'active');
|
|
165
|
+
if (status !== 'active' && !includeRetired)
|
|
166
|
+
continue;
|
|
167
|
+
out.push({
|
|
168
|
+
kind: 'fact', id: f.id || (0, hash_js_1.factId)(f.text), text: f.text, date: f.added || '', status,
|
|
169
|
+
...(f.verified ? { verified: f.verified } : {}),
|
|
170
|
+
...(f.verification ? { verification: f.verification } : {}),
|
|
171
|
+
...(f.keys?.length ? { keywords: f.keys } : {}),
|
|
172
|
+
...(typeof f.confidence === 'number' ? { confidence: f.confidence } : {}),
|
|
173
|
+
...(f.shelf_life ? { shelf_life: f.shelf_life } : {}),
|
|
174
|
+
...(f.temporal ? { temporal: f.temporal } : {}),
|
|
175
|
+
...(f.reference ? { reference: f.reference } : {}),
|
|
176
|
+
...(f.source && f.source !== 'session' && f.source !== 'manual' && !/^session_/.test(f.source) ? { origin: f.source } : {}),
|
|
177
|
+
...(f.revision_note ? { revision_note: f.revision_note } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const side = (kind, text, date, extra = {}) => {
|
|
181
|
+
const id = (0, ops_js_1.entryId)(text);
|
|
182
|
+
const ret = n.entry_status?.[id];
|
|
183
|
+
if (ret && !includeRetired)
|
|
184
|
+
return;
|
|
185
|
+
out.push({
|
|
186
|
+
kind, id, text, date: date || n.entry_dates?.[id] || '',
|
|
187
|
+
status: ret ? ret.status : 'active',
|
|
188
|
+
...(n.entry_verified?.[id] ? { verified: n.entry_verified[id] } : {}),
|
|
189
|
+
...(n.entry_verification?.[id] ? { verification: n.entry_verification[id] } : {}),
|
|
190
|
+
...(n.entry_source?.[id] ? { origin: n.entry_source[id] } : {}),
|
|
191
|
+
...(ret?.note ? { revision_note: ret.note } : {}),
|
|
192
|
+
...extra,
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
for (const d of n.decisions || [])
|
|
196
|
+
side('decision', d.text, d.date || '', d.rationale ? { rationale: d.rationale } : {});
|
|
197
|
+
for (const t of n.patterns || [])
|
|
198
|
+
side('pattern', t, '');
|
|
199
|
+
for (const t of n.preferences || [])
|
|
200
|
+
side('preference', t, '');
|
|
201
|
+
for (const t of n.errors || [])
|
|
202
|
+
side('error', t, '');
|
|
203
|
+
for (const t of n.debts || [])
|
|
204
|
+
side('debt', t, '');
|
|
205
|
+
if (n.map?.text)
|
|
206
|
+
out.push({ kind: 'map', id: 'map', text: n.map.text, date: n.map.updated || '', status: 'active' });
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
/** The neuron with its retired entries (and their sidecar keys) taken out. */
|
|
210
|
+
function withoutRetired(n) {
|
|
211
|
+
const retired = new Set(Object.keys(n.entry_status || {}));
|
|
212
|
+
const keep = (t) => !retired.has((0, ops_js_1.entryId)(t));
|
|
213
|
+
const out = {
|
|
214
|
+
...n,
|
|
215
|
+
facts: (n.facts || []).filter(f => !f.status || f.status === 'active'),
|
|
216
|
+
decisions: (n.decisions || []).filter(d => keep(d.text)),
|
|
217
|
+
patterns: (n.patterns || []).filter(keep),
|
|
218
|
+
preferences: (n.preferences || []).filter(keep),
|
|
219
|
+
...(n.errors ? { errors: n.errors.filter(keep) } : {}),
|
|
220
|
+
...(n.debts ? { debts: n.debts.filter(keep) } : {}),
|
|
221
|
+
};
|
|
222
|
+
for (const s of SIDECARS) {
|
|
223
|
+
const map = n[s];
|
|
224
|
+
if (!map)
|
|
225
|
+
continue;
|
|
226
|
+
const kept = Object.fromEntries(Object.entries(map).filter(([k]) => !retired.has(k)));
|
|
227
|
+
out[s] = kept;
|
|
228
|
+
}
|
|
229
|
+
delete out.entry_status;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function buildExport(snap, includeRetired, nowIso = new Date().toISOString()) {
|
|
233
|
+
const counter = { n: 0 };
|
|
234
|
+
const neurons = snap.neurons
|
|
235
|
+
.map(n => redactNeuron(includeRetired ? n : withoutRetired(n), counter))
|
|
236
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
237
|
+
const synapses = deepRedact(snap.synapses, counter);
|
|
238
|
+
const sessions = deepRedact(snap.sessions, counter);
|
|
239
|
+
const context = deepRedact(snap.context, counter);
|
|
240
|
+
const entries = neurons.reduce((s, n) => s + entriesOf(n, includeRetired).length, 0);
|
|
241
|
+
const openItems = (context?.pending_tasks || []).length;
|
|
242
|
+
return {
|
|
243
|
+
format: exports.EXPORT_FORMAT,
|
|
244
|
+
format_version: exports.EXPORT_FORMAT_VERSION,
|
|
245
|
+
exported_at: nowIso,
|
|
246
|
+
crbro_version: version_js_1.RUNNING_VERSION,
|
|
247
|
+
include_retired: includeRetired,
|
|
248
|
+
redactions: counter.n,
|
|
249
|
+
counts: {
|
|
250
|
+
neurons: neurons.length, archived: neurons.filter(n => n.archived).length, entries,
|
|
251
|
+
synapses: synapses.length, sessions: sessions.length, open_items: openItems,
|
|
252
|
+
},
|
|
253
|
+
neurons, synapses, sessions, context,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// ─── Markdown vault ──────────────────────────────────────────────
|
|
257
|
+
const KIND_TITLES = {
|
|
258
|
+
map: 'Map', error: 'Errors', debt: 'Debts', preference: 'Preferences',
|
|
259
|
+
pattern: 'Patterns', decision: 'Decisions', fact: 'Facts',
|
|
260
|
+
};
|
|
261
|
+
const SECTION_ORDER = ['map', 'error', 'debt', 'preference', 'pattern', 'decision', 'fact'];
|
|
262
|
+
const day = (iso) => (iso && iso.length >= 10 ? iso.slice(0, 10) : iso || '');
|
|
263
|
+
/** Text for a wiki link alias: no brackets or pipes, one line. */
|
|
264
|
+
const alias = (s) => String(s).replace(/[\[\]|#^]/g, ' ').replace(/\s+/g, ' ').trim() || 'untitled';
|
|
265
|
+
function verificationLabel(e) {
|
|
266
|
+
if (e.verification) {
|
|
267
|
+
const ref = e.verification.reference ? ` (${e.verification.reference.kind}: ${e.verification.reference.value})` : '';
|
|
268
|
+
return `verified: ${e.verification.method} ${day(e.verification.at)}${ref}`;
|
|
269
|
+
}
|
|
270
|
+
if (e.verified)
|
|
271
|
+
return `verified: ${day(e.verified)}`;
|
|
272
|
+
return 'unverified';
|
|
273
|
+
}
|
|
274
|
+
function metaLine(e) {
|
|
275
|
+
const parts = [`\`${e.kind}\``];
|
|
276
|
+
if (e.kind !== 'map')
|
|
277
|
+
parts.push(`id \`${e.id}\``);
|
|
278
|
+
parts.push(e.date ? day(e.date) : 'undated');
|
|
279
|
+
parts.push(e.status === 'active' ? 'active' : `**${e.status}**${e.revision_note ? ` (${e.revision_note.replace(/\s+/g, ' ')})` : ''}`);
|
|
280
|
+
if (e.kind !== 'map')
|
|
281
|
+
parts.push(verificationLabel(e));
|
|
282
|
+
if (e.keywords?.length)
|
|
283
|
+
parts.push(`keywords: ${e.keywords.join(', ')}`);
|
|
284
|
+
if (e.shelf_life)
|
|
285
|
+
parts.push(`shelf life: ${e.shelf_life}`);
|
|
286
|
+
if (e.temporal)
|
|
287
|
+
parts.push(e.temporal);
|
|
288
|
+
if (e.origin)
|
|
289
|
+
parts.push(`origin: ${e.origin}`);
|
|
290
|
+
return parts.join(' · ');
|
|
291
|
+
}
|
|
292
|
+
/** Every file of the vault, relative to its root. Pure: nothing is written here. */
|
|
293
|
+
function renderVault(data) {
|
|
294
|
+
const files = [];
|
|
295
|
+
const used = new Set();
|
|
296
|
+
const unique = (base) => {
|
|
297
|
+
let name = base, i = 2;
|
|
298
|
+
while (used.has(name.toLowerCase()))
|
|
299
|
+
name = `${base}_${i++}`;
|
|
300
|
+
used.add(name.toLowerCase());
|
|
301
|
+
return name;
|
|
302
|
+
};
|
|
303
|
+
const noteOf = new Map();
|
|
304
|
+
for (const n of data.neurons)
|
|
305
|
+
noteOf.set(n.id, unique((0, common_js_1.safeFileName)(n.id, 'neuron')));
|
|
306
|
+
const domainNote = new Map();
|
|
307
|
+
for (const d of [...new Set(data.neurons.map(n => n.domain || 'general'))].sort()) {
|
|
308
|
+
domainNote.set(d, unique(`index_${(0, common_js_1.safeFileName)(d, 'general')}`));
|
|
309
|
+
}
|
|
310
|
+
const nameOf = new Map(data.neurons.map(n => [n.id, n.name]));
|
|
311
|
+
const link = (id) => (noteOf.has(id) ? `[[${noteOf.get(id)}|${alias(nameOf.get(id) || id)}]]` : `\`${id}\``);
|
|
312
|
+
// Synapses by neuron.
|
|
313
|
+
const links = new Map();
|
|
314
|
+
for (const s of data.synapses) {
|
|
315
|
+
const [a, b] = s.nodes;
|
|
316
|
+
(links.get(a) || links.set(a, []).get(a)).push({ other: b, s });
|
|
317
|
+
(links.get(b) || links.set(b, []).get(b)).push({ other: a, s });
|
|
318
|
+
}
|
|
319
|
+
for (const n of data.neurons) {
|
|
320
|
+
const entries = entriesOf(n, data.include_retired);
|
|
321
|
+
const dates = entries.map(e => e.date).filter(Boolean).sort();
|
|
322
|
+
const updated = [n.last_accessed, dates[dates.length - 1]].filter(Boolean).sort().pop();
|
|
323
|
+
const fm = (0, markdown_js_1.frontmatterBlock)({
|
|
324
|
+
crbro_export: exports.EXPORT_FORMAT_VERSION,
|
|
325
|
+
id: n.id, name: n.name, type: n.type, domain: n.domain || 'general',
|
|
326
|
+
tags: n.tags || [], heat: typeof n.heat === 'number' ? Math.round(n.heat * 1000) / 1000 : 0,
|
|
327
|
+
created: n.created || '', updated: updated || '',
|
|
328
|
+
...(n.archived ? { archived: true } : {}),
|
|
329
|
+
entries: entries.length,
|
|
330
|
+
});
|
|
331
|
+
const lines = [fm, '', `# ${n.name}`, ''];
|
|
332
|
+
if (n.summary?.trim()) {
|
|
333
|
+
for (const l of n.summary.trim().split(/\r?\n/))
|
|
334
|
+
lines.push(`> ${l}`);
|
|
335
|
+
lines.push('');
|
|
336
|
+
}
|
|
337
|
+
const dn = domainNote.get(n.domain || 'general');
|
|
338
|
+
lines.push(`Domain: [[${dn}|${alias(n.domain || 'general')}]] · type \`${n.type}\` · heat ${(n.heat ?? 0).toFixed(2)}${n.archived ? ' · **archived**' : ''}`, '');
|
|
339
|
+
for (const kind of SECTION_ORDER) {
|
|
340
|
+
const group = entries.filter(e => e.kind === kind)
|
|
341
|
+
.sort((a, b) => (a.status === 'active' ? 0 : 1) - (b.status === 'active' ? 0 : 1) || String(b.date).localeCompare(String(a.date)));
|
|
342
|
+
if (!group.length)
|
|
343
|
+
continue;
|
|
344
|
+
lines.push(`## ${KIND_TITLES[kind]}`, '');
|
|
345
|
+
for (const e of group) {
|
|
346
|
+
lines.push((0, markdown_js_1.entryMarker)({ kind: e.kind, id: e.id, status: e.status, ...(e.keywords?.length ? { keywords: e.keywords } : {}) }));
|
|
347
|
+
lines.push(metaLine(e), '');
|
|
348
|
+
lines.push((0, markdown_js_1.defuseMarkers)(e.text.trim()));
|
|
349
|
+
if (e.rationale?.trim())
|
|
350
|
+
lines.push('', markdown_js_1.ENTRY_WHY, `**Why:** ${(0, markdown_js_1.defuseMarkers)(e.rationale.trim())}`);
|
|
351
|
+
lines.push(markdown_js_1.ENTRY_END, '');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const mine = (links.get(n.id) || []).sort((a, b) => b.s.strength - a.s.strength);
|
|
355
|
+
if (mine.length) {
|
|
356
|
+
lines.push('## Synapses', '');
|
|
357
|
+
for (const { other, s } of mine) {
|
|
358
|
+
lines.push(`- ${link(other)} — ${s.type} · strength ${Number(s.strength ?? 0).toFixed(2)}${s.context ? ` · ${s.context.replace(/\s+/g, ' ')}` : ''}`);
|
|
359
|
+
}
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
362
|
+
files.push({ rel: `neurons/${noteOf.get(n.id)}.md`, content: lines.join('\n') });
|
|
363
|
+
}
|
|
364
|
+
// One index note per domain.
|
|
365
|
+
for (const [d, note] of domainNote) {
|
|
366
|
+
const members = data.neurons.filter(n => (n.domain || 'general') === d).sort((a, b) => (b.heat ?? 0) - (a.heat ?? 0));
|
|
367
|
+
const lines = [(0, markdown_js_1.frontmatterBlock)({ crbro_export: exports.EXPORT_FORMAT_VERSION, domain_index: d, neurons: members.length }), '',
|
|
368
|
+
`# Domain: ${d}`, '', `${members.length} neuron(s), hottest first.`, ''];
|
|
369
|
+
for (const n of members) {
|
|
370
|
+
lines.push(`- ${link(n.id)} — \`${n.type}\` · heat ${(n.heat ?? 0).toFixed(2)} · ${entriesOf(n, data.include_retired).length} entries${n.archived ? ' · archived' : ''}`);
|
|
371
|
+
}
|
|
372
|
+
files.push({ rel: `domains/${note}.md`, content: lines.join('\n') + '\n' });
|
|
373
|
+
}
|
|
374
|
+
// Day logs.
|
|
375
|
+
for (const s of data.sessions) {
|
|
376
|
+
const name = unique((0, common_js_1.safeFileName)(s.session_id, 'session'));
|
|
377
|
+
const lines = [(0, markdown_js_1.frontmatterBlock)({
|
|
378
|
+
crbro_export: exports.EXPORT_FORMAT_VERSION, session_id: s.session_id, date: s.date || '',
|
|
379
|
+
topics: s.topics_touched || [],
|
|
380
|
+
}), '', `# ${s.session_id}`, ''];
|
|
381
|
+
if (s.summary?.trim())
|
|
382
|
+
lines.push(s.summary.trim(), '');
|
|
383
|
+
if (s.topics_touched?.length)
|
|
384
|
+
lines.push(`Topics: ${s.topics_touched.map(link).join(', ')}`, '');
|
|
385
|
+
files.push({ rel: `sessions/${name}.md`, content: lines.join('\n') });
|
|
386
|
+
}
|
|
387
|
+
// Open items.
|
|
388
|
+
const open = (data.context?.pending_tasks || []).map(t => (typeof t === 'string' ? { id: '', text: t, added: '' } : t));
|
|
389
|
+
const closed = data.context?.recently_closed || [];
|
|
390
|
+
const ol = [(0, markdown_js_1.frontmatterBlock)({ crbro_export: exports.EXPORT_FORMAT_VERSION, open_items: open.length }), '', '# Open items', ''];
|
|
391
|
+
if (!open.length)
|
|
392
|
+
ol.push('_None._', '');
|
|
393
|
+
for (const t of open) {
|
|
394
|
+
ol.push(`- [ ] ${t.text.replace(/\s*\n\s*/g, ' ')}${t.id ? ` · \`${t.id}\`` : ''}${t.added ? ` · added ${day(t.added)}` : ''}${t.due ? ` · due ${t.due}` : ''}${t.project ? ` · project ${t.project}` : ''}`);
|
|
395
|
+
}
|
|
396
|
+
if (closed.length) {
|
|
397
|
+
ol.push('', '## Recently closed', '');
|
|
398
|
+
for (const t of closed)
|
|
399
|
+
ol.push(`- [x] ${t.text.replace(/\s*\n\s*/g, ' ')}${t.closed ? ` · closed ${day(t.closed)}` : ''}`);
|
|
400
|
+
}
|
|
401
|
+
files.push({ rel: 'open-items.md', content: ol.join('\n') + '\n' });
|
|
402
|
+
// The front page.
|
|
403
|
+
const c = data.counts;
|
|
404
|
+
const home = [
|
|
405
|
+
(0, markdown_js_1.frontmatterBlock)({ crbro_export: exports.EXPORT_FORMAT_VERSION, exported_at: data.exported_at, crbro_version: data.crbro_version }), '',
|
|
406
|
+
'# CRBRO memory', '',
|
|
407
|
+
`Exported ${day(data.exported_at)} by crbro-memory ${data.crbro_version}: ${c.neurons} neurons (${c.archived} archived), ` +
|
|
408
|
+
`${c.entries} entries, ${c.synapses} synapses, ${c.sessions} day logs, ${c.open_items} open items.` +
|
|
409
|
+
(data.include_retired ? ' Retired entries included and marked.' : ' Retired entries left out.'),
|
|
410
|
+
'',
|
|
411
|
+
data.redactions ? `${data.redactions} credential(s) were replaced with a [REDACTED: …] marker on the way out.` : 'No credential was found to redact.',
|
|
412
|
+
'',
|
|
413
|
+
'This folder is a copy. Editing it does not change the brain: to bring changes back, use `crbro-memory import <this folder>`.',
|
|
414
|
+
'', '## Domains', '',
|
|
415
|
+
...[...domainNote].map(([d, note]) => `- [[${note}|${alias(d)}]]`),
|
|
416
|
+
'', '## Elsewhere', '', '- [[open-items|Open items]]',
|
|
417
|
+
...(data.sessions.length ? ['', '## Latest day logs', '', ...files.filter(f => f.rel.startsWith('sessions/')).slice(0, 10)
|
|
418
|
+
.map(f => `- [[${node_path_1.default.posix.basename(f.rel, '.md')}]]`)] : []),
|
|
419
|
+
'',
|
|
420
|
+
];
|
|
421
|
+
files.push({ rel: 'README.md', content: home.join('\n') });
|
|
422
|
+
return files;
|
|
423
|
+
}
|
|
424
|
+
// ─── Writing ─────────────────────────────────────────────────────
|
|
425
|
+
/** Refuse an output folder inside the brain, or one that already holds anything. */
|
|
426
|
+
async function checkOutDir(out, brainRoot) {
|
|
427
|
+
const realOut = await (0, common_js_1.realpathNearest)(out);
|
|
428
|
+
const realBrain = await (0, common_js_1.realpathNearest)(brainRoot);
|
|
429
|
+
if ((0, common_js_1.isInside)(realOut, realBrain)) {
|
|
430
|
+
return `refusing to write inside the brain folder (${realBrain}). Pick a folder outside it with --out.`;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const st = await node_fs_1.promises.stat(out);
|
|
434
|
+
if (!st.isDirectory())
|
|
435
|
+
return `${out} exists and is not a folder.`;
|
|
436
|
+
const inside = await node_fs_1.promises.readdir(out);
|
|
437
|
+
if (inside.length)
|
|
438
|
+
return `${out} is not empty. Export into a new or empty folder.`;
|
|
439
|
+
}
|
|
440
|
+
catch { /* does not exist: created */ }
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
async function writeNew(root, rel, content) {
|
|
444
|
+
const file = node_path_1.default.join(root, ...rel.split('/'));
|
|
445
|
+
// Belt and braces: every relative path is built from safeFileName, but the
|
|
446
|
+
// final check is on the resolved path.
|
|
447
|
+
if (!(0, common_js_1.isInside)(file, root) || node_path_1.default.resolve(file) === node_path_1.default.resolve(root))
|
|
448
|
+
throw new Error(`unsafe output path: ${rel}`);
|
|
449
|
+
await node_fs_1.promises.mkdir(node_path_1.default.dirname(file), { recursive: true });
|
|
450
|
+
await node_fs_1.promises.writeFile(file, content, { encoding: 'utf8', flag: 'wx' });
|
|
451
|
+
}
|
|
452
|
+
async function runExport(opts) {
|
|
453
|
+
try {
|
|
454
|
+
const st = await node_fs_1.promises.stat(opts.brainRoot);
|
|
455
|
+
if (!st.isDirectory())
|
|
456
|
+
throw new Error();
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
throw new Error(`no brain at ${opts.brainRoot} (set CRBRO_PATH, or run crbro-memory init).`);
|
|
460
|
+
}
|
|
461
|
+
const problem = await checkOutDir(opts.out, opts.brainRoot);
|
|
462
|
+
if (problem)
|
|
463
|
+
throw new Error(problem);
|
|
464
|
+
const snap = await readBrain(opts.brainRoot);
|
|
465
|
+
const data = buildExport(snap, opts.includeRetired);
|
|
466
|
+
await node_fs_1.promises.mkdir(opts.out, { recursive: true });
|
|
467
|
+
if (opts.format === 'json') {
|
|
468
|
+
await writeNew(opts.out, 'crbro-export.json', JSON.stringify(data, null, 2) + '\n');
|
|
469
|
+
return { out: opts.out, files: 1, data, unreadable: snap.unreadable };
|
|
470
|
+
}
|
|
471
|
+
const files = renderVault(data);
|
|
472
|
+
for (const f of files)
|
|
473
|
+
await writeNew(opts.out, f.rel, f.content);
|
|
474
|
+
await writeNew(opts.out, exports.VAULT_MARKER, JSON.stringify({
|
|
475
|
+
format: `${exports.EXPORT_FORMAT}-md`, format_version: exports.EXPORT_FORMAT_VERSION, exported_at: data.exported_at,
|
|
476
|
+
crbro_version: data.crbro_version, include_retired: data.include_retired, counts: data.counts,
|
|
477
|
+
}, null, 2) + '\n');
|
|
478
|
+
return { out: opts.out, files: files.length + 1, data, unreadable: snap.unreadable };
|
|
479
|
+
}
|
|
480
|
+
function stamp(d = new Date()) {
|
|
481
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
482
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
483
|
+
}
|
|
484
|
+
/** CLI entry: returns the exit code. */
|
|
485
|
+
async function main(argv) {
|
|
486
|
+
const a = (0, common_js_1.parseArgs)(argv, { out: 'string', format: 'string', 'include-retired': 'boolean' });
|
|
487
|
+
if (a.flags.help) {
|
|
488
|
+
console.log(exports.EXPORT_HELP);
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
const format = a.flags.format ?? 'md';
|
|
492
|
+
if (format !== 'md' && format !== 'json')
|
|
493
|
+
a.errors.push('--format must be md or json');
|
|
494
|
+
if (a.positionals.length)
|
|
495
|
+
a.errors.push(`unexpected argument: ${a.positionals[0]}`);
|
|
496
|
+
if (a.errors.length) {
|
|
497
|
+
console.error(` ❌ ${a.errors.join('; ')}`);
|
|
498
|
+
console.error(' Usage: crbro-memory export [--out <dir>] [--format md|json] [--include-retired] (--help for more)');
|
|
499
|
+
return 2;
|
|
500
|
+
}
|
|
501
|
+
const brainRoot = (0, brain_js_1.resolveBrainDir)();
|
|
502
|
+
const out = node_path_1.default.resolve(a.flags.out ?? `crbro-export-${stamp()}`);
|
|
503
|
+
try {
|
|
504
|
+
const r = await runExport({ brainRoot, out, format: format, includeRetired: !!a.flags['include-retired'] });
|
|
505
|
+
const c = r.data.counts;
|
|
506
|
+
console.log('');
|
|
507
|
+
console.log(` ✅ Exported ${c.neurons} neurons, ${c.entries} entries, ${c.synapses} synapses, ${c.sessions} day logs, ${c.open_items} open items`);
|
|
508
|
+
console.log(` ${format === 'json' ? node_path_1.default.join(r.out, 'crbro-export.json') : `${r.out} (${r.files} files — open the folder as an Obsidian vault)`}`);
|
|
509
|
+
if (r.data.redactions)
|
|
510
|
+
console.log(` ${r.data.redactions} credential(s) redacted on the way out.`);
|
|
511
|
+
if (r.unreadable.length)
|
|
512
|
+
console.log(` ⚠️ ${r.unreadable.length} file(s) could not be read and were left out: ${r.unreadable.slice(0, 5).join(', ')}${r.unreadable.length > 5 ? ', …' : ''}`);
|
|
513
|
+
console.log(' The brain was only read. Nothing in it changed.');
|
|
514
|
+
console.log('');
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
console.error(` ❌ export: ${err instanceof Error ? err.message : String(err)}`);
|
|
519
|
+
return 1;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
//# sourceMappingURL=export.js.map
|