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
package/dist/cli/view.js
ADDED
|
@@ -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
|