crbro-memory 2.10.0 → 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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -465
  3. package/SECURITY.md +114 -12
  4. package/bin/crbro.mjs +539 -300
  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 +139 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +667 -232
  25. package/dist/engine/doctor.js +312 -14
  26. package/dist/engine/heat.js +81 -37
  27. package/dist/engine/hippocampus.js +18 -7
  28. package/dist/engine/hookrefresh.js +296 -0
  29. package/dist/engine/hookverify.js +15 -3
  30. package/dist/engine/installer.js +1022 -0
  31. package/dist/engine/installio.js +279 -0
  32. package/dist/engine/keychain.js +721 -162
  33. package/dist/engine/maintenance.js +355 -87
  34. package/dist/engine/modinstall.js +98 -45
  35. package/dist/engine/prefrontal.js +29 -6
  36. package/dist/engine/quarantine.js +204 -0
  37. package/dist/engine/secrets.js +41 -5
  38. package/dist/engine/shelf.js +25 -3
  39. package/dist/engine/synapses.js +183 -80
  40. package/dist/engine/triggers.js +141 -11
  41. package/dist/index.js +37 -3
  42. package/dist/mcp-surface.json +1 -0
  43. package/dist/miner/extractor.js +64 -17
  44. package/dist/miner/scheduler.js +144 -68
  45. package/dist/search/index.js +797 -176
  46. package/dist/search/semantic.js +208 -18
  47. package/dist/search/tokenize.js +44 -0
  48. package/dist/server.js +583 -373
  49. package/dist/sync/git.js +203 -72
  50. package/dist/sync/space.js +100 -25
  51. package/dist/sync/validate.js +204 -0
  52. package/dist/utils/budget.js +15 -2
  53. package/dist/utils/fs.js +40 -6
  54. package/dist/utils/ids.js +128 -14
  55. package/dist/utils/lock.js +94 -7
  56. package/hooks/crbro-codex.mjs +284 -0
  57. package/hooks/crbro-guard.mjs +281 -173
  58. package/hooks/crbro-hooks.shipped.json +20 -0
  59. package/hooks/crbro-lifecycle.mjs +16 -4
  60. package/hooks/crbro-subagent.mjs +180 -180
  61. package/package.json +9 -5
  62. package/dist/daemon/daemon.d.ts +0 -25
  63. package/dist/daemon/daemon.d.ts.map +0 -1
  64. package/dist/daemon/daemon.js.map +0 -1
  65. package/dist/daemon/endpoint.d.ts +0 -66
  66. package/dist/daemon/endpoint.d.ts.map +0 -1
  67. package/dist/daemon/endpoint.js.map +0 -1
  68. package/dist/daemon/lines.d.ts +0 -55
  69. package/dist/daemon/lines.d.ts.map +0 -1
  70. package/dist/daemon/lines.js.map +0 -1
  71. package/dist/daemon/proxy.d.ts +0 -55
  72. package/dist/daemon/proxy.d.ts.map +0 -1
  73. package/dist/daemon/proxy.js.map +0 -1
  74. package/dist/engine/backup.d.ts +0 -68
  75. package/dist/engine/backup.d.ts.map +0 -1
  76. package/dist/engine/backup.js.map +0 -1
  77. package/dist/engine/brain.d.ts +0 -79
  78. package/dist/engine/brain.d.ts.map +0 -1
  79. package/dist/engine/brain.js.map +0 -1
  80. package/dist/engine/cortex.d.ts +0 -682
  81. package/dist/engine/cortex.d.ts.map +0 -1
  82. package/dist/engine/cortex.js.map +0 -1
  83. package/dist/engine/dates.d.ts +0 -28
  84. package/dist/engine/dates.d.ts.map +0 -1
  85. package/dist/engine/dates.js.map +0 -1
  86. package/dist/engine/doctor.d.ts +0 -93
  87. package/dist/engine/doctor.d.ts.map +0 -1
  88. package/dist/engine/doctor.js.map +0 -1
  89. package/dist/engine/evidence.d.ts +0 -26
  90. package/dist/engine/evidence.d.ts.map +0 -1
  91. package/dist/engine/evidence.js.map +0 -1
  92. package/dist/engine/heat.d.ts +0 -28
  93. package/dist/engine/heat.d.ts.map +0 -1
  94. package/dist/engine/heat.js.map +0 -1
  95. package/dist/engine/hippocampus.d.ts +0 -60
  96. package/dist/engine/hippocampus.d.ts.map +0 -1
  97. package/dist/engine/hippocampus.js.map +0 -1
  98. package/dist/engine/hookverify.d.ts +0 -42
  99. package/dist/engine/hookverify.d.ts.map +0 -1
  100. package/dist/engine/hookverify.js.map +0 -1
  101. package/dist/engine/keychain.d.ts +0 -32
  102. package/dist/engine/keychain.d.ts.map +0 -1
  103. package/dist/engine/keychain.js.map +0 -1
  104. package/dist/engine/maintenance.d.ts +0 -218
  105. package/dist/engine/maintenance.d.ts.map +0 -1
  106. package/dist/engine/maintenance.js.map +0 -1
  107. package/dist/engine/modinstall.d.ts +0 -215
  108. package/dist/engine/modinstall.d.ts.map +0 -1
  109. package/dist/engine/modinstall.js.map +0 -1
  110. package/dist/engine/postmortem.d.ts +0 -81
  111. package/dist/engine/postmortem.d.ts.map +0 -1
  112. package/dist/engine/postmortem.js.map +0 -1
  113. package/dist/engine/prefrontal.d.ts +0 -64
  114. package/dist/engine/prefrontal.d.ts.map +0 -1
  115. package/dist/engine/prefrontal.js.map +0 -1
  116. package/dist/engine/projects.d.ts +0 -20
  117. package/dist/engine/projects.d.ts.map +0 -1
  118. package/dist/engine/projects.js.map +0 -1
  119. package/dist/engine/secrets.d.ts +0 -30
  120. package/dist/engine/secrets.d.ts.map +0 -1
  121. package/dist/engine/secrets.js.map +0 -1
  122. package/dist/engine/shelf.d.ts +0 -155
  123. package/dist/engine/shelf.d.ts.map +0 -1
  124. package/dist/engine/shelf.js.map +0 -1
  125. package/dist/engine/source.d.ts +0 -6
  126. package/dist/engine/source.d.ts.map +0 -1
  127. package/dist/engine/source.js.map +0 -1
  128. package/dist/engine/synapses.d.ts +0 -82
  129. package/dist/engine/synapses.d.ts.map +0 -1
  130. package/dist/engine/synapses.js.map +0 -1
  131. package/dist/engine/triggers.d.ts +0 -53
  132. package/dist/engine/triggers.d.ts.map +0 -1
  133. package/dist/engine/triggers.js.map +0 -1
  134. package/dist/engine/usage.d.ts +0 -61
  135. package/dist/engine/usage.d.ts.map +0 -1
  136. package/dist/engine/usage.js.map +0 -1
  137. package/dist/index.d.ts +0 -2
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js.map +0 -1
  140. package/dist/miner/extractor.d.ts +0 -12
  141. package/dist/miner/extractor.d.ts.map +0 -1
  142. package/dist/miner/extractor.js.map +0 -1
  143. package/dist/miner/index.d.ts +0 -61
  144. package/dist/miner/index.d.ts.map +0 -1
  145. package/dist/miner/index.js.map +0 -1
  146. package/dist/miner/scheduler.d.ts +0 -30
  147. package/dist/miner/scheduler.d.ts.map +0 -1
  148. package/dist/miner/scheduler.js.map +0 -1
  149. package/dist/miner/types.d.ts +0 -11
  150. package/dist/miner/types.d.ts.map +0 -1
  151. package/dist/miner/types.js.map +0 -1
  152. package/dist/search/index.d.ts +0 -328
  153. package/dist/search/index.d.ts.map +0 -1
  154. package/dist/search/index.js.map +0 -1
  155. package/dist/search/quality.d.ts +0 -18
  156. package/dist/search/quality.d.ts.map +0 -1
  157. package/dist/search/quality.js.map +0 -1
  158. package/dist/search/semantic.d.ts +0 -80
  159. package/dist/search/semantic.d.ts.map +0 -1
  160. package/dist/search/semantic.js.map +0 -1
  161. package/dist/search/synonyms.d.ts +0 -2
  162. package/dist/search/synonyms.d.ts.map +0 -1
  163. package/dist/search/synonyms.js.map +0 -1
  164. package/dist/search/tokenize.d.ts +0 -39
  165. package/dist/search/tokenize.d.ts.map +0 -1
  166. package/dist/search/tokenize.js.map +0 -1
  167. package/dist/server.d.ts +0 -41
  168. package/dist/server.d.ts.map +0 -1
  169. package/dist/server.js.map +0 -1
  170. package/dist/sync/git.d.ts +0 -36
  171. package/dist/sync/git.d.ts.map +0 -1
  172. package/dist/sync/git.js.map +0 -1
  173. package/dist/sync/materialize.d.ts +0 -36
  174. package/dist/sync/materialize.d.ts.map +0 -1
  175. package/dist/sync/materialize.js.map +0 -1
  176. package/dist/sync/ops.d.ts +0 -147
  177. package/dist/sync/ops.d.ts.map +0 -1
  178. package/dist/sync/ops.js.map +0 -1
  179. package/dist/sync/space.d.ts +0 -118
  180. package/dist/sync/space.d.ts.map +0 -1
  181. package/dist/sync/space.js.map +0 -1
  182. package/dist/types/index.d.ts +0 -369
  183. package/dist/types/index.d.ts.map +0 -1
  184. package/dist/types/index.js.map +0 -1
  185. package/dist/utils/budget.d.ts +0 -44
  186. package/dist/utils/budget.d.ts.map +0 -1
  187. package/dist/utils/budget.js.map +0 -1
  188. package/dist/utils/fs.d.ts +0 -59
  189. package/dist/utils/fs.d.ts.map +0 -1
  190. package/dist/utils/fs.js.map +0 -1
  191. package/dist/utils/hash.d.ts +0 -20
  192. package/dist/utils/hash.d.ts.map +0 -1
  193. package/dist/utils/hash.js.map +0 -1
  194. package/dist/utils/ids.d.ts +0 -47
  195. package/dist/utils/ids.d.ts.map +0 -1
  196. package/dist/utils/ids.js.map +0 -1
  197. package/dist/utils/lock.d.ts +0 -13
  198. package/dist/utils/lock.d.ts.map +0 -1
  199. package/dist/utils/lock.js.map +0 -1
  200. package/dist/utils/transcripts.d.ts +0 -63
  201. package/dist/utils/transcripts.d.ts.map +0 -1
  202. package/dist/utils/transcripts.js.map +0 -1
  203. package/dist/version.d.ts +0 -26
  204. package/dist/version.d.ts.map +0 -1
  205. package/dist/version.js.map +0 -1
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+ // ─── crbro-memory view ───────────────────────────────────────────
3
+ //
4
+ // A local page to see the memory, close pending items and review what may be
5
+ // stale — the answer to "I trust my CLAUDE.md because I can see it".
6
+ //
7
+ // Security model, in order of what a request meets:
8
+ // · bound to 127.0.0.1 only, never another interface;
9
+ // · the Host header must be 127.0.0.1:<port> or localhost:<port>, which
10
+ // defeats DNS rebinding (a hostile page whose name resolves to 127.0.0.1
11
+ // still sends its own name as Host);
12
+ // · every /api request carries a fresh 32-byte token in a header; the page
13
+ // gets it once through the URL fragment, which no request or Referer
14
+ // carries, and keeps it in sessionStorage;
15
+ // · no CORS headers at all, a request with a foreign Origin is refused, and
16
+ // writes need Content-Type application/json, so a cross-site form cannot
17
+ // even reach the token check;
18
+ // · the API is a fixed list of operations, not a tool proxy: no credential
19
+ // tool, no whole-neuron delete, no free-form arguments;
20
+ // · a strict CSP with a per-response nonce; brain text only ever reaches
21
+ // the DOM as text.
22
+ // Reads and writes are MCP tool calls to a server spawned over stdio, exactly
23
+ // like import: the daemon, when it is on, stays the single writer.
24
+ var __importDefault = (this && this.__importDefault) || function (mod) {
25
+ return (mod && mod.__esModule) ? mod : { "default": mod };
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.VIEW_HELP = void 0;
29
+ exports.handleApi = handleApi;
30
+ exports.startViewer = startViewer;
31
+ exports.main = main;
32
+ const node_http_1 = __importDefault(require("node:http"));
33
+ const node_crypto_1 = require("node:crypto");
34
+ const common_js_1 = require("./common.js");
35
+ const mcpclient_js_1 = require("./mcpclient.js");
36
+ const viewer_page_js_1 = require("./viewer-page.js");
37
+ exports.VIEW_HELP = `
38
+ crbro-memory view [--port <n>] [--no-open]
39
+
40
+ Open a local page to see and tidy the memory: neurons with filters, every
41
+ entry with its date, status, verification and freshness, a search box
42
+ (crbro_recall), open items to resolve or discard, possibly-stale entries to
43
+ verify or retire, and removing one entry (asks first).
44
+
45
+ --port <n> Port on 127.0.0.1 (default: a free one picked by the system).
46
+ --no-open Print the link instead of opening the browser.
47
+
48
+ Only this machine can reach it (127.0.0.1), and only with the one-time link
49
+ printed below: it carries a random token that dies with the process.
50
+ Ctrl+C stops the page and the CRBRO server it started.
51
+ `;
52
+ const MAX_BODY = 64 * 1024;
53
+ const REF_RE = /^[\s\S]{1,300}$/;
54
+ const PENDING_ID_RE = /^p_[A-Za-z0-9]{4,32}$/;
55
+ const ENTRY_KINDS = new Set(['fact', 'decision', 'pattern', 'preference', 'error', 'debt']);
56
+ class HttpError extends Error {
57
+ status;
58
+ constructor(status, message) {
59
+ super(message);
60
+ this.status = status;
61
+ }
62
+ }
63
+ function send(res, status, body, headers) {
64
+ res.writeHead(status, { ...common_js_1.BASE_SECURITY_HEADERS, 'Content-Length': String(Buffer.byteLength(body)), ...headers });
65
+ res.end(body);
66
+ }
67
+ function sendJson(res, status, payload) {
68
+ send(res, status, JSON.stringify(payload), {
69
+ 'Content-Type': 'application/json; charset=utf-8',
70
+ 'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none'",
71
+ });
72
+ }
73
+ async function readBody(req) {
74
+ const type = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
75
+ if (type !== 'application/json')
76
+ throw new HttpError(415, 'Content-Type must be application/json');
77
+ const declared = Number(req.headers['content-length'] || 0);
78
+ if (declared > MAX_BODY)
79
+ throw new HttpError(413, 'body too large');
80
+ const chunks = [];
81
+ let size = 0;
82
+ for await (const chunk of req) {
83
+ size += chunk.length;
84
+ if (size > MAX_BODY)
85
+ throw new HttpError(413, 'body too large');
86
+ chunks.push(chunk);
87
+ }
88
+ try {
89
+ return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
90
+ }
91
+ catch {
92
+ throw new HttpError(400, 'invalid JSON');
93
+ }
94
+ }
95
+ const str = (v, what, re = REF_RE) => {
96
+ if (typeof v !== 'string' || !re.test(v))
97
+ throw new HttpError(400, `invalid ${what}`);
98
+ return v;
99
+ };
100
+ /** One tool call; an isError answer becomes a 4xx with the tool's own message. */
101
+ async function tool(caller, name, args) {
102
+ const r = await caller.call(name, args);
103
+ if (!r.ok)
104
+ throw new HttpError(400, r.text.slice(0, 500) || `${name} failed`);
105
+ if (r.data === null)
106
+ throw new HttpError(400, r.text.slice(0, 500) || `${name} answered without data`);
107
+ return r.data;
108
+ }
109
+ /** The fixed API. Each route is one or two tool calls with validated arguments. */
110
+ async function handleApi(caller, method, pathname, query, body) {
111
+ const get = method === 'GET', post = method === 'POST';
112
+ switch (pathname) {
113
+ case '/api/status': {
114
+ if (!get)
115
+ break;
116
+ const d = await tool(caller, 'crbro_inspect', { view: 'status' });
117
+ return { status: d.status ?? d };
118
+ }
119
+ case '/api/neurons': {
120
+ if (!get)
121
+ break;
122
+ const all = [];
123
+ let offset = 0, total = 0;
124
+ for (let guard = 0; guard < 1000; guard++) {
125
+ const d = await tool(caller, 'crbro_inspect', { view: 'neurons', limit: 100, offset });
126
+ const page = d.neurons ?? d;
127
+ const rows = Array.isArray(page.neurons) ? page.neurons : [];
128
+ total = page.total ?? rows.length;
129
+ all.push(...rows);
130
+ offset += rows.length;
131
+ if (!rows.length || offset >= total)
132
+ break;
133
+ }
134
+ return { total, neurons: all };
135
+ }
136
+ case '/api/neuron': {
137
+ if (!get)
138
+ break;
139
+ const id = str(query.get('id'), 'id');
140
+ const offset = Math.max(0, Math.min(1_000_000, Number(query.get('offset') || 0) || 0));
141
+ const d = await tool(caller, 'crbro_inspect', {
142
+ view: 'neuron', neuron: id, limit: 50, offset, include_superseded: query.get('retired') === '1',
143
+ });
144
+ const n = d.neuron ?? d;
145
+ // crbro_inspect leaves fields at their default out of each row (2.11):
146
+ // the page shows them, so they are filled back in here.
147
+ if (Array.isArray(n.entries)) {
148
+ n.entries = n.entries.map((e) => ({
149
+ ...e,
150
+ verification: e.verification ?? { method: 'none' },
151
+ freshness: e.freshness ?? { state: 'unverified' },
152
+ }));
153
+ }
154
+ return { neuron: n };
155
+ }
156
+ case '/api/entry': {
157
+ if (!get)
158
+ break;
159
+ const neuron = str(query.get('neuron'), 'neuron');
160
+ const id = str(query.get('id'), 'id');
161
+ const d = await tool(caller, 'crbro_inspect', { view: 'neuron', neuron, entries: [id] });
162
+ const n = d.neuron ?? d;
163
+ return { entry: Array.isArray(n.entries) ? n.entries[0] ?? null : null };
164
+ }
165
+ case '/api/search': {
166
+ if (!get)
167
+ break;
168
+ const q = str(query.get('q'), 'query', /^[\s\S]{1,500}$/);
169
+ const d = await tool(caller, 'crbro_recall', { query: q, limit: 10, scope: 'all' });
170
+ return { results: d.results ?? [], possibly_stale: d.possibly_stale ?? [], no_answer: d.no_answer ?? undefined };
171
+ }
172
+ case '/api/pending': {
173
+ if (get) {
174
+ const d = await tool(caller, 'crbro_context', { scope: 'all' });
175
+ const open = (d.pending_tasks || []).map((t) => (typeof t === 'string' ? { id: '', text: t } : t));
176
+ return { open, closed: d.recently_closed || [] };
177
+ }
178
+ if (!post)
179
+ break;
180
+ const b = await body();
181
+ const id = str(b?.id, 'pending id', PENDING_ID_RE);
182
+ if (b?.action !== 'resolve' && b?.action !== 'discard')
183
+ throw new HttpError(400, 'action must be resolve or discard');
184
+ const d = await tool(caller, 'crbro_context', { scope: 'all', [b.action === 'resolve' ? 'resolve_pending' : 'discard_pending']: id });
185
+ const hit = b.action === 'resolve' ? d.resolved : d.discarded;
186
+ if (Array.isArray(hit) && hit.length === 0)
187
+ throw new HttpError(404, 'no open item with that id');
188
+ return { ok: true };
189
+ }
190
+ case '/api/stale': {
191
+ if (!get)
192
+ break;
193
+ const d = await tool(caller, 'crbro_maintenance', { dry_run: true });
194
+ return {
195
+ stale: d.stale_sample ?? [], stale_count: d.stale_entries ?? 0,
196
+ expired: d.expired_sample ?? [], expired_count: d.expired_entries ?? 0,
197
+ };
198
+ }
199
+ case '/api/verify':
200
+ case '/api/retire': {
201
+ if (!post)
202
+ break;
203
+ const b = await body();
204
+ const neuron = str(b?.neuron, 'neuron');
205
+ const id = str(b?.id, 'entry id');
206
+ const kind = str(b?.kind, 'kind');
207
+ if (!ENTRY_KINDS.has(kind))
208
+ throw new HttpError(400, 'invalid kind');
209
+ const verify = pathname === '/api/verify';
210
+ if (verify && !['fact', 'decision', 'pattern'].includes(kind))
211
+ throw new HttpError(400, 'only facts, decisions and patterns are verified');
212
+ let target = id;
213
+ if (kind !== 'fact' && !verify) {
214
+ // Retiring a decision, pattern, error or debt goes by its exact text.
215
+ const d = await tool(caller, 'crbro_inspect', { view: 'neuron', neuron, entries: [id] });
216
+ const entry = (d.neuron ?? d).entries?.[0];
217
+ if (!entry?.text)
218
+ throw new HttpError(404, 'entry not found');
219
+ target = entry.text;
220
+ }
221
+ const args = { neuron, [kind === 'fact' ? 'facts' : 'entries']: [target] };
222
+ if (verify) {
223
+ args.status = 'verified';
224
+ args.evidence = { method: 'user' };
225
+ }
226
+ else {
227
+ args.status = 'superseded';
228
+ args.note = 'Retired from crbro-memory view.';
229
+ }
230
+ const d = await tool(caller, 'crbro_revise', args);
231
+ if (Array.isArray(d.unmatched) && d.unmatched.length)
232
+ throw new HttpError(409, 'nothing matched: the entry may have changed or be retired already');
233
+ return { ok: true };
234
+ }
235
+ case '/api/remove': {
236
+ if (!post)
237
+ break;
238
+ const b = await body();
239
+ const neuron = str(b?.neuron, 'neuron');
240
+ const id = str(b?.id, 'entry id');
241
+ const d = await tool(caller, 'crbro_forget', { neuron, facts: [id] });
242
+ if (!d.removed)
243
+ throw new HttpError(409, 'nothing matched: the entry may already be gone');
244
+ return { ok: true, backup: d.backup ?? undefined };
245
+ }
246
+ default:
247
+ throw new HttpError(404, 'not found');
248
+ }
249
+ throw new HttpError(405, 'method not allowed');
250
+ }
251
+ async function startViewer(opts) {
252
+ const token = opts.token ?? (0, common_js_1.randomToken)();
253
+ const log = opts.log ?? (() => undefined);
254
+ let port = 0;
255
+ const server = node_http_1.default.createServer(async (req, res) => {
256
+ try {
257
+ if (!(0, common_js_1.hostAllowed)(req.headers.host, (0, common_js_1.loopbackHosts)(port))) {
258
+ return sendJson(res, 421, { error: 'Host not allowed: open the link printed by crbro-memory view.' });
259
+ }
260
+ const url = new URL(req.url || '/', `http://127.0.0.1:${port}`);
261
+ const method = req.method || 'GET';
262
+ if (url.pathname === '/' || url.pathname === '/index.html') {
263
+ if (method !== 'GET' && method !== 'HEAD')
264
+ return sendJson(res, 405, { error: 'method not allowed' });
265
+ const nonce = (0, node_crypto_1.randomBytes)(16).toString('base64');
266
+ return send(res, 200, method === 'HEAD' ? '' : (0, viewer_page_js_1.viewerPage)(nonce), {
267
+ 'Content-Type': 'text/html; charset=utf-8',
268
+ 'Content-Security-Policy': (0, viewer_page_js_1.viewerCsp)(nonce),
269
+ });
270
+ }
271
+ if (!url.pathname.startsWith('/api/'))
272
+ return sendJson(res, 404, { error: 'not found' });
273
+ // A browser always says where a request comes from. Ours is same-origin.
274
+ const origin = req.headers.origin;
275
+ if (origin !== undefined && !(0, common_js_1.hostAllowed)(origin.replace(/^http:\/\//i, ''), (0, common_js_1.loopbackHosts)(port))) {
276
+ return sendJson(res, 403, { error: 'cross-origin request refused' });
277
+ }
278
+ if (req.headers['sec-fetch-site'] && !['same-origin', 'none'].includes(String(req.headers['sec-fetch-site']))) {
279
+ return sendJson(res, 403, { error: 'cross-site request refused' });
280
+ }
281
+ const presented = req.headers['x-crbro-token'];
282
+ if (!(0, common_js_1.tokenMatches)(Array.isArray(presented) ? presented[0] : presented, token)) {
283
+ return sendJson(res, 401, { error: 'missing or wrong token: open the link printed by crbro-memory view' });
284
+ }
285
+ const payload = await handleApi(opts.caller, method, url.pathname, url.searchParams, () => readBody(req));
286
+ sendJson(res, 200, payload);
287
+ }
288
+ catch (err) {
289
+ if (err instanceof HttpError)
290
+ return sendJson(res, err.status, { error: err.message });
291
+ log(`request failed: ${err instanceof Error ? err.message : String(err)}`);
292
+ if (!res.headersSent)
293
+ sendJson(res, 500, { error: 'internal error' });
294
+ else
295
+ res.destroy();
296
+ }
297
+ });
298
+ server.headersTimeout = 10_000;
299
+ server.requestTimeout = 120_000;
300
+ server.maxHeadersCount = 50;
301
+ await new Promise((resolve, reject) => {
302
+ server.once('error', reject);
303
+ server.listen(opts.port ?? 0, '127.0.0.1', () => { server.off('error', reject); resolve(); });
304
+ });
305
+ port = server.address().port;
306
+ const url = `http://127.0.0.1:${port}/#t=${token}`;
307
+ return {
308
+ url, port, token,
309
+ close: () => new Promise(resolve => {
310
+ server.close(() => resolve());
311
+ server.closeAllConnections?.();
312
+ }),
313
+ };
314
+ }
315
+ /** CLI entry: resolves with the exit code once the page is stopped. */
316
+ async function main(argv, deps = {}) {
317
+ const a = (0, common_js_1.parseArgs)(argv, { port: 'number', 'no-open': 'boolean' });
318
+ if (a.flags.help) {
319
+ console.log(exports.VIEW_HELP);
320
+ return 0;
321
+ }
322
+ const port = a.flags.port;
323
+ if (port !== undefined && port > 65535)
324
+ a.errors.push('--port must be 0-65535 (0 = any free port)');
325
+ if (a.positionals.length)
326
+ a.errors.push(`unexpected argument: ${a.positionals[0]}`);
327
+ if (a.errors.length) {
328
+ console.error(` ❌ ${a.errors.join('; ')}`);
329
+ console.error(' Usage: crbro-memory view [--port <n>] [--no-open] (--help for more)');
330
+ return 2;
331
+ }
332
+ let caller = null;
333
+ let viewer = null;
334
+ try {
335
+ caller = await (deps.connect ? deps.connect() : (0, mcpclient_js_1.spawnServer)());
336
+ viewer = await startViewer({ caller, port, log: line => console.error(` [view] ${line}`) });
337
+ }
338
+ catch (err) {
339
+ await caller?.close();
340
+ const msg = err instanceof Error ? err.message : String(err);
341
+ console.error(` ❌ view: ${/EADDRINUSE/.test(msg) ? `port ${port} is in use; pick another with --port, or leave it out` : msg}`);
342
+ return 1;
343
+ }
344
+ console.log('');
345
+ console.log(' 🧠 CRBRO memory viewer — this machine only (127.0.0.1)');
346
+ console.log(` ${viewer.url}`);
347
+ console.log(' The link carries a one-time token: do not share it. Ctrl+C to stop.');
348
+ console.log('');
349
+ if (!a.flags['no-open'])
350
+ (0, common_js_1.openBrowser)(viewer.url);
351
+ deps.onReady?.(viewer);
352
+ await (0, common_js_1.waitForStop)(deps.signal);
353
+ await viewer.close();
354
+ await caller.close();
355
+ console.log(' Viewer stopped.');
356
+ return 0;
357
+ }
358
+ //# sourceMappingURL=view.js.map