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.
Files changed (197) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -456
  3. package/SECURITY.md +135 -8
  4. package/bin/crbro.mjs +555 -299
  5. package/dist/build-id.json +10 -0
  6. package/dist/cli/common.js +211 -0
  7. package/dist/cli/export.js +522 -0
  8. package/dist/cli/import.js +683 -0
  9. package/dist/cli/markdown.js +313 -0
  10. package/dist/cli/mcpclient.js +107 -0
  11. package/dist/cli/serve.js +453 -0
  12. package/dist/cli/view.js +358 -0
  13. package/dist/cli/viewer-page.js +378 -0
  14. package/dist/daemon/daemon.js +119 -11
  15. package/dist/daemon/endpoint.js +222 -21
  16. package/dist/daemon/lines.js +102 -13
  17. package/dist/daemon/proxy.js +480 -125
  18. package/dist/daemon/surface.js +116 -0
  19. package/dist/daemon/trace.js +73 -0
  20. package/dist/engine/atrest.js +132 -0
  21. package/dist/engine/backup.js +143 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +762 -311
  25. package/dist/engine/doctor.js +676 -0
  26. package/dist/engine/evidence.js +171 -0
  27. package/dist/engine/heat.js +81 -37
  28. package/dist/engine/hippocampus.js +50 -36
  29. package/dist/engine/hookrefresh.js +296 -0
  30. package/dist/engine/hookverify.js +15 -3
  31. package/dist/engine/installer.js +1022 -0
  32. package/dist/engine/installio.js +279 -0
  33. package/dist/engine/keychain.js +735 -144
  34. package/dist/engine/maintenance.js +355 -87
  35. package/dist/engine/modinstall.js +98 -45
  36. package/dist/engine/prefrontal.js +126 -69
  37. package/dist/engine/projects.js +80 -0
  38. package/dist/engine/quarantine.js +204 -0
  39. package/dist/engine/secrets.js +41 -5
  40. package/dist/engine/shelf.js +34 -11
  41. package/dist/engine/synapses.js +183 -80
  42. package/dist/engine/triggers.js +141 -11
  43. package/dist/index.js +37 -3
  44. package/dist/mcp-surface.json +1 -0
  45. package/dist/miner/extractor.js +64 -17
  46. package/dist/miner/scheduler.js +144 -68
  47. package/dist/search/index.js +810 -176
  48. package/dist/search/quality.js +31 -0
  49. package/dist/search/semantic.js +208 -18
  50. package/dist/search/tokenize.js +44 -0
  51. package/dist/server.js +649 -343
  52. package/dist/sync/git.js +203 -72
  53. package/dist/sync/materialize.js +33 -8
  54. package/dist/sync/space.js +286 -101
  55. package/dist/sync/validate.js +204 -0
  56. package/dist/utils/budget.js +15 -2
  57. package/dist/utils/fs.js +40 -6
  58. package/dist/utils/ids.js +128 -14
  59. package/dist/utils/lock.js +219 -52
  60. package/hooks/crbro-codex.mjs +284 -0
  61. package/hooks/crbro-guard.mjs +281 -173
  62. package/hooks/crbro-hooks.shipped.json +20 -0
  63. package/hooks/crbro-lifecycle.mjs +16 -4
  64. package/hooks/crbro-subagent.mjs +180 -180
  65. package/package.json +12 -8
  66. package/dist/daemon/daemon.d.ts +0 -25
  67. package/dist/daemon/daemon.d.ts.map +0 -1
  68. package/dist/daemon/daemon.js.map +0 -1
  69. package/dist/daemon/endpoint.d.ts +0 -66
  70. package/dist/daemon/endpoint.d.ts.map +0 -1
  71. package/dist/daemon/endpoint.js.map +0 -1
  72. package/dist/daemon/lines.d.ts +0 -55
  73. package/dist/daemon/lines.d.ts.map +0 -1
  74. package/dist/daemon/lines.js.map +0 -1
  75. package/dist/daemon/proxy.d.ts +0 -55
  76. package/dist/daemon/proxy.d.ts.map +0 -1
  77. package/dist/daemon/proxy.js.map +0 -1
  78. package/dist/engine/backup.d.ts +0 -68
  79. package/dist/engine/backup.d.ts.map +0 -1
  80. package/dist/engine/backup.js.map +0 -1
  81. package/dist/engine/brain.d.ts +0 -79
  82. package/dist/engine/brain.d.ts.map +0 -1
  83. package/dist/engine/brain.js.map +0 -1
  84. package/dist/engine/cortex.d.ts +0 -670
  85. package/dist/engine/cortex.d.ts.map +0 -1
  86. package/dist/engine/cortex.js.map +0 -1
  87. package/dist/engine/dates.d.ts +0 -28
  88. package/dist/engine/dates.d.ts.map +0 -1
  89. package/dist/engine/dates.js.map +0 -1
  90. package/dist/engine/heat.d.ts +0 -28
  91. package/dist/engine/heat.d.ts.map +0 -1
  92. package/dist/engine/heat.js.map +0 -1
  93. package/dist/engine/hippocampus.d.ts +0 -60
  94. package/dist/engine/hippocampus.d.ts.map +0 -1
  95. package/dist/engine/hippocampus.js.map +0 -1
  96. package/dist/engine/hookverify.d.ts +0 -42
  97. package/dist/engine/hookverify.d.ts.map +0 -1
  98. package/dist/engine/hookverify.js.map +0 -1
  99. package/dist/engine/keychain.d.ts +0 -32
  100. package/dist/engine/keychain.d.ts.map +0 -1
  101. package/dist/engine/keychain.js.map +0 -1
  102. package/dist/engine/maintenance.d.ts +0 -218
  103. package/dist/engine/maintenance.d.ts.map +0 -1
  104. package/dist/engine/maintenance.js.map +0 -1
  105. package/dist/engine/modinstall.d.ts +0 -215
  106. package/dist/engine/modinstall.d.ts.map +0 -1
  107. package/dist/engine/modinstall.js.map +0 -1
  108. package/dist/engine/postmortem.d.ts +0 -81
  109. package/dist/engine/postmortem.d.ts.map +0 -1
  110. package/dist/engine/postmortem.js.map +0 -1
  111. package/dist/engine/prefrontal.d.ts +0 -61
  112. package/dist/engine/prefrontal.d.ts.map +0 -1
  113. package/dist/engine/prefrontal.js.map +0 -1
  114. package/dist/engine/secrets.d.ts +0 -30
  115. package/dist/engine/secrets.d.ts.map +0 -1
  116. package/dist/engine/secrets.js.map +0 -1
  117. package/dist/engine/shelf.d.ts +0 -156
  118. package/dist/engine/shelf.d.ts.map +0 -1
  119. package/dist/engine/shelf.js.map +0 -1
  120. package/dist/engine/source.d.ts +0 -6
  121. package/dist/engine/source.d.ts.map +0 -1
  122. package/dist/engine/source.js.map +0 -1
  123. package/dist/engine/synapses.d.ts +0 -82
  124. package/dist/engine/synapses.d.ts.map +0 -1
  125. package/dist/engine/synapses.js.map +0 -1
  126. package/dist/engine/triggers.d.ts +0 -53
  127. package/dist/engine/triggers.d.ts.map +0 -1
  128. package/dist/engine/triggers.js.map +0 -1
  129. package/dist/engine/usage.d.ts +0 -61
  130. package/dist/engine/usage.d.ts.map +0 -1
  131. package/dist/engine/usage.js.map +0 -1
  132. package/dist/index.d.ts +0 -2
  133. package/dist/index.d.ts.map +0 -1
  134. package/dist/index.js.map +0 -1
  135. package/dist/miner/extractor.d.ts +0 -12
  136. package/dist/miner/extractor.d.ts.map +0 -1
  137. package/dist/miner/extractor.js.map +0 -1
  138. package/dist/miner/index.d.ts +0 -61
  139. package/dist/miner/index.d.ts.map +0 -1
  140. package/dist/miner/index.js.map +0 -1
  141. package/dist/miner/scheduler.d.ts +0 -30
  142. package/dist/miner/scheduler.d.ts.map +0 -1
  143. package/dist/miner/scheduler.js.map +0 -1
  144. package/dist/miner/types.d.ts +0 -11
  145. package/dist/miner/types.d.ts.map +0 -1
  146. package/dist/miner/types.js.map +0 -1
  147. package/dist/search/index.d.ts +0 -323
  148. package/dist/search/index.d.ts.map +0 -1
  149. package/dist/search/index.js.map +0 -1
  150. package/dist/search/semantic.d.ts +0 -80
  151. package/dist/search/semantic.d.ts.map +0 -1
  152. package/dist/search/semantic.js.map +0 -1
  153. package/dist/search/synonyms.d.ts +0 -2
  154. package/dist/search/synonyms.d.ts.map +0 -1
  155. package/dist/search/synonyms.js.map +0 -1
  156. package/dist/search/tokenize.d.ts +0 -39
  157. package/dist/search/tokenize.d.ts.map +0 -1
  158. package/dist/search/tokenize.js.map +0 -1
  159. package/dist/server.d.ts +0 -41
  160. package/dist/server.d.ts.map +0 -1
  161. package/dist/server.js.map +0 -1
  162. package/dist/sync/git.d.ts +0 -36
  163. package/dist/sync/git.d.ts.map +0 -1
  164. package/dist/sync/git.js.map +0 -1
  165. package/dist/sync/materialize.d.ts +0 -35
  166. package/dist/sync/materialize.d.ts.map +0 -1
  167. package/dist/sync/materialize.js.map +0 -1
  168. package/dist/sync/ops.d.ts +0 -142
  169. package/dist/sync/ops.d.ts.map +0 -1
  170. package/dist/sync/ops.js.map +0 -1
  171. package/dist/sync/space.d.ts +0 -116
  172. package/dist/sync/space.d.ts.map +0 -1
  173. package/dist/sync/space.js.map +0 -1
  174. package/dist/types/index.d.ts +0 -346
  175. package/dist/types/index.d.ts.map +0 -1
  176. package/dist/types/index.js.map +0 -1
  177. package/dist/utils/budget.d.ts +0 -44
  178. package/dist/utils/budget.d.ts.map +0 -1
  179. package/dist/utils/budget.js.map +0 -1
  180. package/dist/utils/fs.d.ts +0 -59
  181. package/dist/utils/fs.d.ts.map +0 -1
  182. package/dist/utils/fs.js.map +0 -1
  183. package/dist/utils/hash.d.ts +0 -20
  184. package/dist/utils/hash.d.ts.map +0 -1
  185. package/dist/utils/hash.js.map +0 -1
  186. package/dist/utils/ids.d.ts +0 -47
  187. package/dist/utils/ids.d.ts.map +0 -1
  188. package/dist/utils/ids.js.map +0 -1
  189. package/dist/utils/lock.d.ts +0 -11
  190. package/dist/utils/lock.d.ts.map +0 -1
  191. package/dist/utils/lock.js.map +0 -1
  192. package/dist/utils/transcripts.d.ts +0 -63
  193. package/dist/utils/transcripts.d.ts.map +0 -1
  194. package/dist/utils/transcripts.js.map +0 -1
  195. package/dist/version.d.ts +0 -26
  196. package/dist/version.d.ts.map +0 -1
  197. package/dist/version.js.map +0 -1
@@ -0,0 +1,683 @@
1
+ "use strict";
2
+ // ─── crbro-memory import ─────────────────────────────────────────
3
+ //
4
+ // Bring knowledge in from what already sits on the owner's machine: a
5
+ // CLAUDE.md / AGENTS.md, a Claude Code memory folder, or a CRBRO export (JSON
6
+ // or the Markdown vault, for round trips). Dry run by default.
7
+ //
8
+ // Writes go ONLY through MCP tool calls to a CRBRO server this command spawns
9
+ // over stdio — never into brain files — so a running daemon stays the single
10
+ // writer and learn's validation, redaction and de-duplication all apply.
11
+ // Idempotent: before anything is learned, the target neuron's index is read
12
+ // and every entry whose content id is already there (live or retired) is left
13
+ // alone, so a second run adds nothing and bumps no counter.
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ImportError = exports.IMPORT_HELP = void 0;
19
+ exports.memoryKind = memoryKind;
20
+ exports.memoryProjectName = memoryProjectName;
21
+ exports.documentRootName = documentRootName;
22
+ exports.buildPlan = buildPlan;
23
+ exports.storedId = storedId;
24
+ exports.learnTopicOf = learnTopicOf;
25
+ exports.protocolLost = protocolLost;
26
+ exports.executePlan = executePlan;
27
+ exports.formatReport = formatReport;
28
+ exports.main = main;
29
+ const node_path_1 = __importDefault(require("node:path"));
30
+ const node_fs_1 = require("node:fs");
31
+ const secrets_js_1 = require("../engine/secrets.js");
32
+ const ops_js_1 = require("../sync/ops.js");
33
+ const hash_js_1 = require("../utils/hash.js");
34
+ const ids_js_1 = require("../utils/ids.js");
35
+ const common_js_1 = require("./common.js");
36
+ const markdown_js_1 = require("./markdown.js");
37
+ const export_js_1 = require("./export.js");
38
+ const mcpclient_js_1 = require("./mcpclient.js");
39
+ exports.IMPORT_HELP = `
40
+ crbro-memory import <path> [--apply] [--topic <name>] [--type fact|preference|pattern]
41
+
42
+ Learn what a file or folder already says. DRY RUN by default: it shows what
43
+ would be learned and into which neuron, and writes nothing. --apply writes.
44
+
45
+ <path> can be:
46
+ CLAUDE.md, AGENTS.md or any instructions Markdown
47
+ The single H1 names the document; each H2 becomes a topic
48
+ "<title> — <H2>" (deeper headings stay in their section and lead
49
+ their entries); every bullet, paragraph, table or code block becomes
50
+ one entry, stored as --type (default fact).
51
+ A Claude Code memory folder (Markdown files with name / description /
52
+ metadata.type frontmatter; MEMORY.md, the index, is skipped)
53
+ Every file becomes one entry "<name>: <body>" in one topic,
54
+ "<project> (Claude Code memory)", by type:
55
+ user → preference · feedback → pattern · project → fact · reference → fact
56
+ A body over 1,500 characters is split into its paragraphs.
57
+ A CRBRO export: crbro-export.json, or a folder exported with --format md
58
+ Neurons, entries of every kind (map, summary, tags, synapses) come
59
+ back by name; retired entries are never re-learned.
60
+
61
+ --apply Write. Without it nothing changes.
62
+ --topic <name> Markdown sources: put everything into this one topic.
63
+ --type <kind> Markdown sources: fact (default), preference or pattern,
64
+ overriding the memory-type mapping.
65
+
66
+ Writes go through a CRBRO server this command starts (the daemon, when it
67
+ is on), with the same CRBRO_PATH. Importing the same thing twice adds nothing.
68
+ `;
69
+ const LEARN_KINDS = new Set(['fact', 'decision', 'pattern', 'preference', 'error', 'debt']);
70
+ class ImportError extends Error {
71
+ }
72
+ exports.ImportError = ImportError;
73
+ const MAX_MD_BYTES = 5 * 1024 * 1024;
74
+ const MAX_JSON_BYTES = 512 * 1024 * 1024;
75
+ const SYNAPSE_TYPES = new Set(['dependency', 'causal', 'temporal', 'conceptual', 'hierarchy', 'alternative']);
76
+ const SHELF = new Set(['volatile', 'normal', 'durable', 'permanent']);
77
+ async function readText(file, max) {
78
+ const st = await node_fs_1.promises.stat(file);
79
+ if (st.size > max)
80
+ throw new ImportError(`${file} is ${Math.round(st.size / 1048576)} MB, over the ${Math.round(max / 1048576)} MB this command reads.`);
81
+ return (await node_fs_1.promises.readFile(file, 'utf8')).replace(/^/, '');
82
+ }
83
+ function cleanReference(r) {
84
+ if (!r || typeof r !== 'object')
85
+ return undefined;
86
+ if (!['file', 'url', 'person'].includes(r.kind) || typeof r.value !== 'string' || !r.value || r.value.length > 2048)
87
+ return undefined;
88
+ return {
89
+ kind: r.kind, value: r.value,
90
+ ...(typeof r.field === 'string' && r.field.length <= 256 ? { field: r.field } : {}),
91
+ ...(typeof r.project === 'string' && r.project.length <= 256 ? { project: r.project } : {}),
92
+ };
93
+ }
94
+ /** A neuron of an export, as topics and entries. Retired entries are counted, not planned. */
95
+ function topicFromNeuron(n, source) {
96
+ const all = (0, export_js_1.entriesOf)(n, true);
97
+ const live = all.filter(e => e.status === 'active');
98
+ return {
99
+ retired: all.length - live.length,
100
+ topic: {
101
+ topic: n.name, exportId: n.id, type: n.type, domain: n.domain || undefined,
102
+ summary: n.summary || undefined, tags: Array.isArray(n.tags) && n.tags.length ? n.tags : undefined,
103
+ source,
104
+ entries: live.map(e => ({
105
+ kind: e.kind === 'map' ? 'map' : e.kind,
106
+ text: e.text,
107
+ ...(e.keywords?.length ? { keywords: e.keywords } : {}),
108
+ ...(e.rationale ? { rationale: e.rationale } : {}),
109
+ ...(e.kind === 'fact' && typeof e.confidence === 'number' ? { confidence: e.confidence } : {}),
110
+ ...(e.kind === 'fact' && e.shelf_life && SHELF.has(e.shelf_life) ? { shelf_life: e.shelf_life } : {}),
111
+ ...(e.kind === 'fact' && (e.temporal === 'current' || e.temporal === 'historical') ? { temporal: e.temporal } : {}),
112
+ ...(e.kind === 'fact' && cleanReference(e.reference) ? { reference: cleanReference(e.reference) } : {}),
113
+ })),
114
+ },
115
+ };
116
+ }
117
+ function synapsesFrom(list) {
118
+ const seen = new Set();
119
+ const out = [];
120
+ for (const s of list) {
121
+ if (!Array.isArray(s.nodes) || s.nodes.length !== 2)
122
+ continue;
123
+ const [a, b] = s.nodes.map(String);
124
+ if (!a || !b || a === b)
125
+ continue;
126
+ const key = [a, b].sort().join('|');
127
+ if (seen.has(key))
128
+ continue;
129
+ seen.add(key);
130
+ out.push({
131
+ from: a, to: b,
132
+ type: SYNAPSE_TYPES.has(String(s.type)) ? String(s.type) : 'conceptual',
133
+ strength: typeof s.strength === 'number' && s.strength >= 0 && s.strength <= 1 ? s.strength : 0.5,
134
+ context: typeof s.context === 'string' ? s.context : '',
135
+ });
136
+ }
137
+ return out;
138
+ }
139
+ async function planFromJson(file) {
140
+ let data;
141
+ try {
142
+ data = JSON.parse(await readText(file, MAX_JSON_BYTES));
143
+ }
144
+ catch (err) {
145
+ if (err instanceof ImportError)
146
+ throw err;
147
+ throw new ImportError(`${file} is not valid JSON.`);
148
+ }
149
+ if (data?.format !== export_js_1.EXPORT_FORMAT || !Array.isArray(data?.neurons)) {
150
+ throw new ImportError(`${file} is not a CRBRO export (expected "format": "${export_js_1.EXPORT_FORMAT}").`);
151
+ }
152
+ const plan = { sourceKind: 'crbro-json', path: file, topics: [], synapses: [], skippedRetired: 0, warnings: [] };
153
+ for (const n of data.neurons) {
154
+ if (!n || typeof n.id !== 'string' || typeof n.name !== 'string') {
155
+ plan.warnings.push('a neuron without id or name was skipped');
156
+ continue;
157
+ }
158
+ const { topic, retired } = topicFromNeuron(n, file);
159
+ plan.skippedRetired += retired;
160
+ if (topic.entries.length || topic.summary)
161
+ plan.topics.push(topic);
162
+ }
163
+ plan.synapses = synapsesFrom(Array.isArray(data.synapses) ? data.synapses : []);
164
+ return plan;
165
+ }
166
+ const SYNAPSE_LINE = /^- \[\[([^|\]]+)(?:\|[^\]]*)?\]\] — ([a-z]+) · strength ([\d.]+)(?: · (.*))?$/;
167
+ async function planFromVault(dir) {
168
+ const plan = { sourceKind: 'crbro-md', path: dir, topics: [], synapses: [], skippedRetired: 0, warnings: [] };
169
+ const neuronsDir = node_path_1.default.join(dir, 'neurons');
170
+ let names = [];
171
+ try {
172
+ names = (await node_fs_1.promises.readdir(neuronsDir)).filter(n => n.endsWith('.md')).sort();
173
+ }
174
+ catch {
175
+ throw new ImportError(`${dir} has no neurons/ folder: not a CRBRO Markdown export.`);
176
+ }
177
+ const noteToId = new Map();
178
+ const rawLinks = [];
179
+ for (const name of names) {
180
+ const file = node_path_1.default.join(neuronsDir, name);
181
+ const fm = (0, markdown_js_1.parseFrontmatter)(await readText(file, MAX_MD_BYTES));
182
+ if (!fm.present || fm.data.crbro_export === undefined || typeof fm.data.id !== 'string') {
183
+ plan.warnings.push(`neurons/${name} is not an exported neuron note; skipped`);
184
+ continue;
185
+ }
186
+ const id = fm.data.id;
187
+ noteToId.set(node_path_1.default.basename(name, '.md'), id);
188
+ const entries = (0, markdown_js_1.parseVaultEntries)(fm.body);
189
+ const live = entries.filter(e => e.status === 'active');
190
+ plan.skippedRetired += entries.length - live.length;
191
+ // The summary is the quote block right under the title.
192
+ const summaryLines = [];
193
+ let afterTitle = false;
194
+ for (const line of fm.body.replace(/\r\n?/g, '\n').split('\n')) {
195
+ if (!afterTitle) {
196
+ if (/^# /.test(line))
197
+ afterTitle = true;
198
+ continue;
199
+ }
200
+ if (!line.trim() && !summaryLines.length)
201
+ continue;
202
+ if (line.startsWith('> ') || line === '>') {
203
+ summaryLines.push(line.replace(/^> ?/, ''));
204
+ continue;
205
+ }
206
+ break;
207
+ }
208
+ let inSynapses = false;
209
+ for (const line of fm.body.replace(/\r\n?/g, '\n').split('\n')) {
210
+ if (/^## /.test(line)) {
211
+ inSynapses = line.trim() === '## Synapses';
212
+ continue;
213
+ }
214
+ if (!inSynapses)
215
+ continue;
216
+ const m = SYNAPSE_LINE.exec(line.trim());
217
+ if (m)
218
+ rawLinks.push({ fromId: id, note: m[1], type: m[2], strength: Number(m[3]), context: m[4] || '' });
219
+ }
220
+ const topic = {
221
+ topic: typeof fm.data.name === 'string' && fm.data.name ? fm.data.name : id,
222
+ exportId: id,
223
+ type: typeof fm.data.type === 'string' ? fm.data.type : undefined,
224
+ domain: typeof fm.data.domain === 'string' && fm.data.domain ? fm.data.domain : undefined,
225
+ summary: summaryLines.join('\n').trim() || undefined,
226
+ tags: Array.isArray(fm.data.tags) && fm.data.tags.length ? fm.data.tags.map(String) : undefined,
227
+ source: file,
228
+ entries: live.map(e => ({
229
+ kind: (e.kind === 'map' ? 'map' : LEARN_KINDS.has(e.kind) ? e.kind : 'fact'),
230
+ text: e.text,
231
+ ...(e.keywords?.length ? { keywords: e.keywords } : {}),
232
+ ...(e.rationale ? { rationale: e.rationale } : {}),
233
+ })),
234
+ };
235
+ if (topic.entries.length || topic.summary)
236
+ plan.topics.push(topic);
237
+ }
238
+ plan.synapses = synapsesFrom(rawLinks
239
+ .filter(l => noteToId.has(l.note))
240
+ .map(l => ({ nodes: [l.fromId, noteToId.get(l.note)], type: l.type, strength: l.strength, context: l.context, id: '', co_access_count: 0, last_co_access: '' })));
241
+ return plan;
242
+ }
243
+ /** user → preference · feedback → pattern · project, reference and anything else → fact. */
244
+ function memoryKind(type) {
245
+ switch (String(type ?? '').toLowerCase()) {
246
+ case 'user': return 'preference';
247
+ case 'feedback': return 'pattern';
248
+ default: return 'fact';
249
+ }
250
+ }
251
+ /** The project a Claude Code memory folder belongs to: …/projects/<encoded cwd>/memory → its last path segment. */
252
+ function memoryProjectName(dir) {
253
+ const abs = node_path_1.default.resolve(dir);
254
+ const parent = node_path_1.default.basename(node_path_1.default.dirname(abs));
255
+ if (node_path_1.default.basename(abs).toLowerCase() === 'memory' && node_path_1.default.basename(node_path_1.default.dirname(node_path_1.default.dirname(abs))).toLowerCase() === 'projects') {
256
+ const parts = parent.split('-').filter(Boolean);
257
+ return parts[parts.length - 1] || parent;
258
+ }
259
+ return node_path_1.default.basename(abs);
260
+ }
261
+ const MEMORY_SPLIT_CHARS = 1_500;
262
+ async function memoryFiles(dir) {
263
+ const out = [];
264
+ let names = [];
265
+ try {
266
+ names = (await node_fs_1.promises.readdir(dir)).filter(n => n.toLowerCase().endsWith('.md')).sort();
267
+ }
268
+ catch {
269
+ return out;
270
+ }
271
+ for (const n of names) {
272
+ if (n.toLowerCase() === 'memory.md')
273
+ continue; // the index of the folder, not a memory
274
+ const file = node_path_1.default.join(dir, n);
275
+ try {
276
+ if (!(await node_fs_1.promises.lstat(file)).isFile())
277
+ continue;
278
+ const fm = (0, markdown_js_1.parseFrontmatter)(await readText(file, MAX_MD_BYTES));
279
+ if (fm.present && (typeof fm.data.name === 'string' || typeof fm.data.description === 'string'))
280
+ out.push({ file, fm });
281
+ }
282
+ catch { /* unreadable: not a memory file */ }
283
+ }
284
+ return out;
285
+ }
286
+ async function planFromMemoryFolder(dir, files, opts) {
287
+ const plan = { sourceKind: 'claude-memory', path: dir, topics: [], synapses: [], skippedRetired: 0, warnings: [] };
288
+ const topic = { topic: opts.topic || `${memoryProjectName(dir)} (Claude Code memory)`, entries: [], source: dir };
289
+ for (const { file, fm } of files) {
290
+ const name = String(fm.data.name || node_path_1.default.basename(file, '.md')).trim();
291
+ const description = typeof fm.data.description === 'string' ? fm.data.description.trim() : '';
292
+ const metaType = (fm.data.metadata && typeof fm.data.metadata === 'object' ? fm.data.metadata.type : undefined) ?? fm.data.type;
293
+ const kind = opts.type ?? memoryKind(metaType);
294
+ const body = fm.body.trim();
295
+ if (!body && !description)
296
+ continue;
297
+ if (body.length <= MEMORY_SPLIT_CHARS) {
298
+ topic.entries.push({ kind, text: `${name}: ${body || description}` });
299
+ }
300
+ else {
301
+ for (const section of (0, markdown_js_1.splitInstructions)(body, name)) {
302
+ for (const e of section.entries)
303
+ topic.entries.push({ kind, text: `${name}: ${e}` });
304
+ }
305
+ }
306
+ }
307
+ if (topic.entries.length)
308
+ plan.topics.push(topic);
309
+ else
310
+ plan.warnings.push('no memory file with content was found');
311
+ return plan;
312
+ }
313
+ /** The name a document's untitled part goes under: the folder of a CLAUDE.md / AGENTS.md, else the file name. */
314
+ function documentRootName(file) {
315
+ const stem = node_path_1.default.basename(file, node_path_1.default.extname(file));
316
+ if (/^(claude|agents|gemini|copilot-instructions)$/i.test(stem)) {
317
+ const folder = node_path_1.default.basename(node_path_1.default.dirname(node_path_1.default.resolve(file)));
318
+ return folder && folder !== '.' ? folder : stem;
319
+ }
320
+ return stem;
321
+ }
322
+ async function planFromMarkdownFile(file, opts) {
323
+ const text = await readText(file, MAX_MD_BYTES);
324
+ const fm = (0, markdown_js_1.parseFrontmatter)(text);
325
+ const plan = { sourceKind: 'markdown', path: file, topics: [], synapses: [], skippedRetired: 0, warnings: [] };
326
+ const kind = opts.type ?? 'fact';
327
+ const sections = (0, markdown_js_1.splitInstructions)(fm.body, documentRootName(file));
328
+ if (opts.topic) {
329
+ const all = sections.flatMap(s => s.entries);
330
+ if (all.length)
331
+ plan.topics.push({ topic: opts.topic, entries: all.map(t => ({ kind, text: t })), source: file });
332
+ }
333
+ else {
334
+ for (const s of sections)
335
+ plan.topics.push({ topic: s.topic, entries: s.entries.map(t => ({ kind, text: t })), source: file });
336
+ }
337
+ if (!plan.topics.length)
338
+ plan.warnings.push('the file holds no paragraph, bullet or block to learn');
339
+ return plan;
340
+ }
341
+ /** Look at `p` and build the plan. Reads only the source; the brain is not involved yet. */
342
+ async function buildPlan(p, opts = {}) {
343
+ let st;
344
+ try {
345
+ st = await node_fs_1.promises.stat(p);
346
+ }
347
+ catch {
348
+ throw new ImportError(`${p} does not exist.`);
349
+ }
350
+ const crbroOnly = () => {
351
+ if (opts.topic || opts.type)
352
+ throw new ImportError('--topic and --type apply to Markdown sources, not to a CRBRO export: its neurons and kinds are kept.');
353
+ };
354
+ if (st.isFile()) {
355
+ const ext = node_path_1.default.extname(p).toLowerCase();
356
+ if (ext === '.json') {
357
+ crbroOnly();
358
+ return planFromJson(p);
359
+ }
360
+ if (ext === '.md' || ext === '.markdown' || ext === '.txt') {
361
+ const fm = (0, markdown_js_1.parseFrontmatter)(await readText(p, MAX_MD_BYTES));
362
+ if (fm.present && fm.data.crbro_export !== undefined) {
363
+ throw new ImportError(`${p} is one note of an exported vault: import the vault folder instead.`);
364
+ }
365
+ if (fm.present && typeof fm.data.name === 'string' && (fm.data.metadata?.type || fm.data.type)) {
366
+ return planFromMemoryFolder(node_path_1.default.dirname(p), [{ file: p, fm }], opts);
367
+ }
368
+ return planFromMarkdownFile(p, opts);
369
+ }
370
+ throw new ImportError(`${p}: expected a .md, .json or a folder.`);
371
+ }
372
+ if (!st.isDirectory())
373
+ throw new ImportError(`${p} is neither a file nor a folder.`);
374
+ const has = async (rel) => node_fs_1.promises.stat(node_path_1.default.join(p, rel)).then(() => true, () => false);
375
+ if (await has('crbro-export.json')) {
376
+ crbroOnly();
377
+ return planFromJson(node_path_1.default.join(p, 'crbro-export.json'));
378
+ }
379
+ if (await has(export_js_1.VAULT_MARKER) || await has(node_path_1.default.join('neurons'))) {
380
+ crbroOnly();
381
+ return planFromVault(p);
382
+ }
383
+ const files = await memoryFiles(p);
384
+ if (files.length)
385
+ return planFromMemoryFolder(p, files, opts);
386
+ throw new ImportError(`${p} is not a CRBRO export or a Claude Code memory folder. For a CLAUDE.md, pass the file itself.`);
387
+ }
388
+ /** The id an entry will have once stored: learn trims nothing, so we trim first and redact like it does. */
389
+ function storedId(e) {
390
+ if (e.kind === 'map')
391
+ return 'map';
392
+ const stored = (0, secrets_js_1.redact)(e.text.trim()).text;
393
+ return e.kind === 'fact' ? (0, hash_js_1.factId)(stored) : (0, ops_js_1.entryId)(stored);
394
+ }
395
+ /**
396
+ * The topic name learn should create a neuron under. Only protocols matter:
397
+ * boot injects neurons whose id starts with protocol_, and learn infers the
398
+ * type from the name, so a protocol whose name does not say so would come
399
+ * back as a project. Adding the word steers the inference.
400
+ */
401
+ function learnTopicOf(t) {
402
+ if (t.type === 'protocol' && (0, ids_js_1.inferNeuronType)(t.topic) !== 'protocol' && (0, ids_js_1.inferNeuronType)(`${t.topic} protocol`) === 'protocol') {
403
+ return `${t.topic} protocol`;
404
+ }
405
+ return t.topic;
406
+ }
407
+ /** A protocol whose name steers the inference elsewhere (a tech or process word in it) cannot come back as one. */
408
+ function protocolLost(t) {
409
+ return t.type === 'protocol' && (0, ids_js_1.inferNeuronType)(learnTopicOf(t)) !== 'protocol';
410
+ }
411
+ async function readTarget(caller, ref) {
412
+ const state = { id: '', name: '', summary: '', tags: [], ids: new Set(), hasMap: false, connections: new Set() };
413
+ let offset = 0;
414
+ for (let guard = 0; guard < 10_000; guard++) {
415
+ const r = await caller.call('crbro_inspect', { view: 'neuron', neuron: ref, include_superseded: true, limit: 40, offset });
416
+ const n = r.data?.neuron ?? r.data;
417
+ if (!r.ok || !n || typeof n.id !== 'string') {
418
+ if (offset === 0)
419
+ return null;
420
+ throw new Error(`reading ${ref} failed half-way: ${r.text.slice(0, 200)}`);
421
+ }
422
+ if (offset === 0) {
423
+ state.id = n.id;
424
+ state.name = n.name;
425
+ state.summary = n.summary || '';
426
+ state.tags = Array.isArray(n.tags) ? n.tags : [];
427
+ for (const c of n.connections || [])
428
+ if (c?.target_id)
429
+ state.connections.add(c.target_id);
430
+ }
431
+ const rows = Array.isArray(n.entries) ? n.entries : [];
432
+ for (const row of rows) {
433
+ if (row.kind === 'map')
434
+ state.hasMap = true;
435
+ if (typeof row.id === 'string')
436
+ state.ids.add(row.id);
437
+ }
438
+ if (!n.entries_pagination?.has_more || rows.length === 0)
439
+ break;
440
+ offset += rows.length;
441
+ }
442
+ return state;
443
+ }
444
+ /** Find the neuron a topic lands in: its export id, then the name learn will use, then its own name. */
445
+ async function resolveTarget(caller, t) {
446
+ const refs = [t.exportId, learnTopicOf(t), t.topic].filter((x, i, a) => !!x && a.indexOf(x) === i);
447
+ for (const ref of refs) {
448
+ const s = await readTarget(caller, ref);
449
+ if (s)
450
+ return s;
451
+ }
452
+ return null;
453
+ }
454
+ /** Compare the plan with the brain and, with apply, write the difference. */
455
+ async function executePlan(caller, plan, apply) {
456
+ const report = { plan, applied: apply, topics: [], learned: 0, maps: 0, connected: 0, skippedRetiredByServer: 0, failures: [] };
457
+ // A brain that was never initialized has no manifest, and learn would leave
458
+ // half-made neurons behind. Initializing is `init`'s job (or a client's
459
+ // first boot), not an import side effect: say so before writing anything.
460
+ const status = await caller.call('crbro_inspect', { view: 'status' });
461
+ if (!status.ok) {
462
+ const why = /not initialized/i.test(status.text)
463
+ ? 'this brain has not been initialized yet. Run `crbro-memory init` (or start any CRBRO client once) first.'
464
+ : `the CRBRO server could not read the brain: ${status.text.slice(0, 200)}`;
465
+ if (apply)
466
+ throw new ImportError(`nothing was written: ${why}`);
467
+ plan.warnings.push(why);
468
+ }
469
+ const idMap = new Map(); // export id → id in this brain
470
+ const connectionsOf = new Map();
471
+ for (const t of plan.topics) {
472
+ const target = await resolveTarget(caller, t);
473
+ const seen = new Set(target?.ids ?? []);
474
+ const fresh = [];
475
+ let present = 0;
476
+ for (const e of t.entries) {
477
+ if (!e.text.trim())
478
+ continue;
479
+ const id = storedId(e);
480
+ const already = e.kind === 'map' ? (target?.hasMap || seen.has('map')) : seen.has(id);
481
+ if (already) {
482
+ present++;
483
+ continue;
484
+ }
485
+ seen.add(id);
486
+ fresh.push(e);
487
+ }
488
+ const tr = {
489
+ topic: t.topic, target: target?.id ?? null, fresh, present,
490
+ summary: !!t.summary && !target?.summary,
491
+ tags: !!t.tags?.length && !(target?.tags?.length),
492
+ };
493
+ report.topics.push(tr);
494
+ if (!target && fresh.length && protocolLost(t)) {
495
+ plan.warnings.push(`"${t.topic}" was a protocol in the export, but crbro_learn infers the type from the name and this one ` +
496
+ `reads as a ${(0, ids_js_1.inferNeuronType)(learnTopicOf(t))}: it comes back as an ordinary neuron, and boot will not inject it. ` +
497
+ 'Recreate it under a name with the word "protocol" and no technology or process word.');
498
+ }
499
+ if (target && t.exportId)
500
+ idMap.set(t.exportId, target.id);
501
+ if (target)
502
+ connectionsOf.set(target.id, target.connections);
503
+ if (!apply)
504
+ continue;
505
+ let neuronId = target?.id;
506
+ const learnTopic = learnTopicOf(t);
507
+ for (const e of fresh) {
508
+ if (e.kind === 'map')
509
+ continue; // after the entries, so the neuron exists with its proper type
510
+ const args = {
511
+ topic: learnTopic, type: e.kind, content: e.text.trim(),
512
+ ...(neuronId ? { neuron_id: neuronId } : {}),
513
+ ...(t.domain ? { domain: t.domain } : {}),
514
+ ...(e.kind === 'decision' && e.rationale ? { rationale: e.rationale } : {}),
515
+ ...(e.kind === 'fact' && e.keywords?.length ? { keywords: e.keywords } : {}),
516
+ ...(e.kind === 'fact' && typeof e.confidence === 'number' ? { confidence: e.confidence } : {}),
517
+ ...(e.kind === 'fact' && e.shelf_life ? { shelf_life: e.shelf_life } : {}),
518
+ ...(e.kind === 'fact' && e.temporal ? { temporal: e.temporal } : {}),
519
+ ...(e.kind === 'fact' && e.reference ? { reference: e.reference } : {}),
520
+ };
521
+ const r = await caller.call('crbro_learn', args);
522
+ if (!r.ok || !r.data?.neuron_id) {
523
+ report.failures.push(`${t.topic}: ${e.kind} "${preview((0, secrets_js_1.redact)(e.text).text, 60)}" — ${r.text.slice(0, 200)}`);
524
+ continue;
525
+ }
526
+ neuronId = r.data.neuron_id;
527
+ if (r.data.action === 'skipped_retired')
528
+ report.skippedRetiredByServer++;
529
+ else
530
+ report.learned++;
531
+ }
532
+ const map = fresh.find(e => e.kind === 'map');
533
+ if (map) {
534
+ const r = await caller.call('crbro_map', { neuron: neuronId ?? learnTopic, content: map.text.trim(), ...(t.domain ? { domain: t.domain } : {}) });
535
+ if (r.ok) {
536
+ report.maps++;
537
+ if (!neuronId && typeof r.data?.neuron_id === 'string')
538
+ neuronId = r.data.neuron_id;
539
+ }
540
+ else
541
+ report.failures.push(`${t.topic}: map — ${r.text.slice(0, 200)}`);
542
+ }
543
+ if (!neuronId && (tr.summary || tr.tags)) {
544
+ // Nothing new to learn but metadata to carry: the neuron must exist first.
545
+ const s = await readTarget(caller, learnTopic);
546
+ neuronId = s?.id;
547
+ }
548
+ if (neuronId) {
549
+ const revise = {};
550
+ if (tr.summary)
551
+ revise.summary = t.summary;
552
+ if (tr.tags)
553
+ revise.tags = t.tags;
554
+ // A protocol created under "<name> protocol" gets its own name back; the id keeps the prefix boot needs.
555
+ if (!target && learnTopic !== t.topic)
556
+ revise.name = t.topic;
557
+ if (Object.keys(revise).length) {
558
+ const r = await caller.call('crbro_revise', { neuron: neuronId, ...revise });
559
+ if (!r.ok)
560
+ report.failures.push(`${t.topic}: summary/tags — ${r.text.slice(0, 200)}`);
561
+ }
562
+ if (t.exportId)
563
+ idMap.set(t.exportId, neuronId);
564
+ if (!connectionsOf.has(neuronId))
565
+ connectionsOf.set(neuronId, new Set());
566
+ }
567
+ }
568
+ // Synapses, between neurons that both made it.
569
+ if (apply) {
570
+ for (const s of plan.synapses) {
571
+ const a = idMap.get(s.from), b = idMap.get(s.to);
572
+ if (!a || !b || a === b)
573
+ continue;
574
+ if (connectionsOf.get(a)?.has(b) || connectionsOf.get(b)?.has(a))
575
+ continue;
576
+ const r = await caller.call('crbro_connect', { from: a, to: b, type: s.type, strength: s.strength, ...(s.context ? { context: s.context } : {}) });
577
+ if (r.ok) {
578
+ report.connected++;
579
+ connectionsOf.get(a)?.add(b);
580
+ connectionsOf.get(b)?.add(a);
581
+ }
582
+ else
583
+ report.failures.push(`synapse ${a} ↔ ${b} — ${r.text.slice(0, 200)}`);
584
+ }
585
+ }
586
+ return report;
587
+ }
588
+ const preview = (s, n) => {
589
+ const one = s.replace(/\s+/g, ' ').trim();
590
+ return one.length > n ? `${one.slice(0, n - 1)}…` : one;
591
+ };
592
+ const SOURCE_LABEL = {
593
+ 'crbro-json': 'CRBRO export (JSON)',
594
+ 'crbro-md': 'CRBRO export (Markdown vault)',
595
+ 'claude-memory': 'Claude Code memory folder',
596
+ markdown: 'instructions Markdown',
597
+ };
598
+ function formatReport(r, maxLines = 200) {
599
+ const out = [''];
600
+ out.push(` ${r.applied ? 'Import' : 'Dry run'}: ${r.plan.path} (${SOURCE_LABEL[r.plan.sourceKind]})`);
601
+ if (!r.applied)
602
+ out.push(' Nothing is written. Add --apply to write what is listed below.');
603
+ out.push('');
604
+ let lines = 0, hidden = 0;
605
+ for (const t of r.topics) {
606
+ const where = t.target ? `existing neuron ${t.target}` : 'new neuron';
607
+ const extras = [t.summary ? 'summary' : '', t.tags ? 'tags' : ''].filter(Boolean).join(', ');
608
+ out.push(` → "${t.topic}" (${where}): ${t.fresh.length} new, ${t.present} already there${extras ? ` · sets ${extras}` : ''}`);
609
+ for (const e of t.fresh) {
610
+ if (lines >= maxLines) {
611
+ hidden++;
612
+ continue;
613
+ }
614
+ out.push(` + ${e.kind}: ${preview((0, secrets_js_1.redact)(e.text).text, 100)}`);
615
+ lines++;
616
+ }
617
+ }
618
+ if (hidden)
619
+ out.push(` … and ${hidden} more`);
620
+ const fresh = r.topics.reduce((s, t) => s + t.fresh.length, 0);
621
+ const present = r.topics.reduce((s, t) => s + t.present, 0);
622
+ const newNeurons = r.topics.filter(t => !t.target && (t.fresh.length || t.summary)).length;
623
+ out.push('');
624
+ out.push(` ${r.applied ? 'Learned' : 'Would learn'} ${r.applied ? r.learned + r.maps : fresh} entr${(r.applied ? r.learned + r.maps : fresh) === 1 ? 'y' : 'ies'} in ${r.topics.filter(t => t.fresh.length).length} topic(s) (${newNeurons} new neuron(s)); ${present} already in the brain.`);
625
+ if (r.plan.skippedRetired)
626
+ out.push(` ${r.plan.skippedRetired} retired entr${r.plan.skippedRetired === 1 ? 'y' : 'ies'} in the source left out: retired knowledge is never re-learned.`);
627
+ if (r.plan.synapses.length)
628
+ out.push(` Synapses in the source: ${r.plan.synapses.length}${r.applied ? `; ${r.connected} connected now` : ' (connected on --apply when both ends exist)'}.`);
629
+ if (r.skippedRetiredByServer)
630
+ out.push(` ${r.skippedRetiredByServer} matched an entry this brain had retired and were not stored.`);
631
+ for (const w of r.plan.warnings)
632
+ out.push(` ⚠️ ${w}`);
633
+ for (const f of r.failures.slice(0, 20))
634
+ out.push(` ❌ ${f}`);
635
+ if (r.failures.length > 20)
636
+ out.push(` ❌ … and ${r.failures.length - 20} more failures`);
637
+ out.push('');
638
+ return out.join('\n');
639
+ }
640
+ /** CLI entry: returns the exit code. */
641
+ async function main(argv, deps = {}) {
642
+ const a = (0, common_js_1.parseArgs)(argv, { apply: 'boolean', topic: 'string', type: 'string' });
643
+ if (a.flags.help) {
644
+ console.log(exports.IMPORT_HELP);
645
+ return 0;
646
+ }
647
+ const type = a.flags.type;
648
+ if (type !== undefined && !['fact', 'preference', 'pattern'].includes(type))
649
+ a.errors.push('--type must be fact, preference or pattern');
650
+ const topic = typeof a.flags.topic === 'string' ? a.flags.topic.trim() : undefined;
651
+ if (a.flags.topic !== undefined && !topic)
652
+ a.errors.push('--topic needs a name');
653
+ if (a.positionals.length !== 1)
654
+ a.errors.push(a.positionals.length ? 'one path at a time' : 'missing <path>');
655
+ if (a.errors.length) {
656
+ console.error(` ❌ ${a.errors.join('; ')}`);
657
+ console.error(' Usage: crbro-memory import <path> [--apply] [--topic <name>] [--type fact|preference|pattern] (--help for more)');
658
+ return 2;
659
+ }
660
+ let plan;
661
+ try {
662
+ plan = await buildPlan(node_path_1.default.resolve(a.positionals[0]), { topic, type: type });
663
+ }
664
+ catch (err) {
665
+ console.error(` ❌ import: ${err instanceof Error ? err.message : String(err)}`);
666
+ return 1;
667
+ }
668
+ let caller = null;
669
+ try {
670
+ caller = await (deps.connect ? deps.connect() : (0, mcpclient_js_1.spawnServer)());
671
+ const report = await executePlan(caller, plan, !!a.flags.apply);
672
+ console.log(formatReport(report));
673
+ return report.failures.length ? 1 : 0;
674
+ }
675
+ catch (err) {
676
+ console.error(` ❌ import: ${err instanceof Error ? err.message : String(err)}`);
677
+ return 1;
678
+ }
679
+ finally {
680
+ await caller?.close();
681
+ }
682
+ }
683
+ //# sourceMappingURL=import.js.map