crbro-memory 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ // ─── CRBRO Keychain Broker ───────────────────────────────────────
3
+ //
4
+ // The secret filter says no. It refuses to write a credential into the brain,
5
+ // which is right — but it leaves the user holding a password and nowhere to
6
+ // put it, so it goes back into a config file in plain text and nothing was
7
+ // gained. This module is the other half of that sentence: "no, not in memory —
8
+ // I put it in your keychain and remembered the name."
9
+ //
10
+ // CRBRO stores nothing itself and invents no crypto. Every platform already
11
+ // ships a credential store that is better reviewed than anything written here
12
+ // could be, so the job is to broker access to it and stay out of the way:
13
+ //
14
+ // macOS security(1), the login keychain
15
+ // Linux secret-tool(1), the Secret Service the desktop already runs
16
+ // Windows DPAPI through PowerShell, sealed to the user account
17
+ //
18
+ // Windows is the odd one. Its Credential Manager has no supported way to read
19
+ // a secret back from the command line — cmdkey writes but will not return the
20
+ // value, and reading needs P/Invoke into CredRead. So there the secret is
21
+ // sealed with DPAPI, which is the same crypto the Credential Manager uses,
22
+ // into a file only that Windows account can open. Copied to another machine,
23
+ // lifted from a backup or pushed to a repository, it is unreadable.
24
+ //
25
+ // The store lives OUTSIDE the brain on purpose. Nothing under ~/.crbro is ever
26
+ // consulted here, so no sync, no team space and no `crbro_share` can reach a
27
+ // secret even if some future bug tried to.
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.KeychainUnavailable = void 0;
30
+ exports.detectBackend = detectBackend;
31
+ exports.setSecret = setSecret;
32
+ exports.getSecret = getSecret;
33
+ exports.listSecrets = listSecrets;
34
+ exports.removeSecret = removeSecret;
35
+ exports._resetWindowsStoreForTests = _resetWindowsStoreForTests;
36
+ const node_child_process_1 = require("node:child_process");
37
+ const node_os_1 = require("node:os");
38
+ const node_path_1 = require("node:path");
39
+ const node_fs_1 = require("node:fs");
40
+ /** Namespace under which every CRBRO secret is filed in the OS store. */
41
+ const SERVICE = 'crbro';
42
+ /**
43
+ * Windows only: the sealed file, deliberately a sibling of the brain, never
44
+ * inside it. CRBRO_KEYS_DIR redirects it, which is how the tests avoid writing
45
+ * into the real store — a test suite that can only run by touching the user's
46
+ * own credentials is a test suite nobody runs twice.
47
+ */
48
+ function winPaths() {
49
+ const dir = process.env['CRBRO_KEYS_DIR'] || (0, node_path_1.join)((0, node_os_1.homedir)(), '.crbro-keys');
50
+ return { dir, file: (0, node_path_1.join)(dir, 'keys.dpapi') };
51
+ }
52
+ class KeychainUnavailable extends Error {
53
+ reason;
54
+ constructor(reason) {
55
+ super(reason);
56
+ this.reason = reason;
57
+ this.name = 'KeychainUnavailable';
58
+ }
59
+ }
60
+ exports.KeychainUnavailable = KeychainUnavailable;
61
+ /** Runs a command without a shell, so a value with spaces or quotes cannot be reinterpreted. */
62
+ function run(cmd, args, input) {
63
+ return (0, node_child_process_1.spawnSync)(cmd, args, {
64
+ encoding: 'utf8',
65
+ input,
66
+ timeout: 30_000,
67
+ windowsHide: true,
68
+ });
69
+ }
70
+ /**
71
+ * Which store this machine can actually use — not which one it should have.
72
+ * Returns null with a reason the caller can show the user, because "no
73
+ * keychain here" is a normal answer on a headless box, not a failure.
74
+ */
75
+ function detectBackend() {
76
+ const os = (0, node_os_1.platform)();
77
+ if (os === 'darwin') {
78
+ const probe = run('security', ['-h']);
79
+ if (probe.error)
80
+ return { backend: null, reason: 'security(1) not found — unexpected on macOS.' };
81
+ return { backend: 'macos-keychain' };
82
+ }
83
+ if (os === 'win32') {
84
+ const probe = run('powershell.exe', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.Major']);
85
+ if (probe.error || probe.status !== 0) {
86
+ return { backend: null, reason: 'PowerShell not available, so DPAPI cannot be reached.' };
87
+ }
88
+ return { backend: 'windows-dpapi' };
89
+ }
90
+ // Linux and the BSDs. secret-tool needs a running Secret Service over D-Bus;
91
+ // over SSH with no desktop session there is usually none, and that is worth
92
+ // saying plainly rather than failing with a D-Bus error.
93
+ const probe = run('secret-tool', ['--help']);
94
+ if (probe.error) {
95
+ return { backend: null, reason: 'secret-tool not installed. On Debian/Ubuntu: apt install libsecret-tools.' };
96
+ }
97
+ if (!process.env['DBUS_SESSION_BUS_ADDRESS']) {
98
+ return {
99
+ backend: null,
100
+ reason: 'secret-tool is installed but no D-Bus session is running, which is normal over SSH. ' +
101
+ 'Start a session bus or use the CRBRO_SECRET_* environment variables instead.',
102
+ };
103
+ }
104
+ return { backend: 'linux-secret-service' };
105
+ }
106
+ function requireBackend() {
107
+ const { backend, reason } = detectBackend();
108
+ if (!backend)
109
+ throw new KeychainUnavailable(reason ?? 'No credential store available on this machine.');
110
+ return backend;
111
+ }
112
+ // Every DPAPI call costs a PowerShell start, about a second. A script that
113
+ // needs four credentials would pay four seconds for no reason, so a read is
114
+ // held briefly in memory.
115
+ //
116
+ // Briefly is the whole point. The MCP server is long-lived, and a cache with
117
+ // no expiry would keep every secret in the heap for as long as the editor is
118
+ // open. Thirty seconds covers the burst of reads at the start of a task and
119
+ // nothing beyond it.
120
+ const CACHE_TTL_MS = 30_000;
121
+ let cached = null;
122
+ function invalidateCache() {
123
+ cached = null;
124
+ }
125
+ function psDpapi(script, input) {
126
+ const r = run('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], input);
127
+ if (r.error || r.status !== 0) {
128
+ throw new KeychainUnavailable(`DPAPI call failed: ${(r.stderr || '').trim() || 'unknown error'}`);
129
+ }
130
+ return r.stdout;
131
+ }
132
+ function winRead() {
133
+ const { file } = winPaths();
134
+ if (cached && cached.file === file && Date.now() - cached.at < CACHE_TTL_MS) {
135
+ return cached.store;
136
+ }
137
+ if (!(0, node_fs_1.existsSync)(file))
138
+ return {};
139
+ const sealed = (0, node_fs_1.readFileSync)(file, 'utf8').trim();
140
+ if (!sealed)
141
+ return {};
142
+ // ConvertTo-SecureString without -Key is DPAPI at CurrentUser scope: only
143
+ // this Windows account, on this machine, can turn it back into text.
144
+ const json = psDpapi('$e = [Console]::In.ReadToEnd().Trim();' +
145
+ '$s = ConvertTo-SecureString $e;' +
146
+ '$b = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($s);' +
147
+ 'try { [Runtime.InteropServices.Marshal]::PtrToStringAuto($b) }' +
148
+ 'finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b) }', sealed);
149
+ try {
150
+ const store = JSON.parse(json);
151
+ cached = { store, at: Date.now(), file };
152
+ return store;
153
+ }
154
+ catch {
155
+ throw new KeychainUnavailable('The key store exists but could not be read. It was sealed by a different Windows account.');
156
+ }
157
+ }
158
+ function winWrite(store) {
159
+ const { dir, file } = winPaths();
160
+ if (!(0, node_fs_1.existsSync)(dir))
161
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
162
+ const sealed = psDpapi('$j = [Console]::In.ReadToEnd();' +
163
+ 'ConvertTo-SecureString $j -AsPlainText -Force | ConvertFrom-SecureString', JSON.stringify(store)).trim();
164
+ // Atomic: a crash mid-write leaves the previous store intact rather than a
165
+ // truncated file that would take every secret with it.
166
+ const tmp = `${file}.tmp`;
167
+ (0, node_fs_1.writeFileSync)(tmp, sealed, { encoding: 'ascii', mode: 0o600 });
168
+ (0, node_fs_1.renameSync)(tmp, file);
169
+ cached = { store, at: Date.now(), file };
170
+ }
171
+ // ─── macOS: an index of its own ──────────────────────────────────
172
+ //
173
+ // There is no clean way to list the items of one service. dump-keychain asks
174
+ // for authorisation once per item, and find-generic-password -g returns a
175
+ // single match and prints the password to stderr behind a "password: " prefix
176
+ // — reading that back just to enumerate names would mean handling secrets
177
+ // for no reason. So the names live in an item of their own: one extra write
178
+ // per change, and a listing that tells the truth.
179
+ // security(1) prints the whole value as hex pairs if a single byte of it is
180
+ // not printable, and a UTF-8 accent is two bytes above 0x7F — so "contraseña"
181
+ // comes back as "636f6e747261736ec3b161". Worse, it is ambiguous: the literal
182
+ // secret "deadbeef" is returned exactly like the hex of the bytes DE AD BE EF,
183
+ // and nothing in the output says which one it was.
184
+ //
185
+ // Base64 is printable ASCII from end to end, so the hex path is never taken
186
+ // and any byte sequence survives the round trip unchanged.
187
+ function macEncode(value) {
188
+ return Buffer.from(value, 'utf8').toString('base64');
189
+ }
190
+ function macDecode(raw) {
191
+ // -w always ends with a newline of its own (putchar), and only that one is
192
+ // removed: .trim() would eat spaces that belong to the secret.
193
+ return Buffer.from(raw.replace(/\n$/, ''), 'base64').toString('utf8');
194
+ }
195
+ const MAC_INDEX = '__crbro_index__';
196
+ function macReadIndex() {
197
+ const r = run('security', ['find-generic-password', '-s', SERVICE, '-a', MAC_INDEX, '-w']);
198
+ if (r.status !== 0)
199
+ return {};
200
+ try {
201
+ return JSON.parse(macDecode(r.stdout));
202
+ }
203
+ catch {
204
+ return {};
205
+ }
206
+ }
207
+ function macWriteIndex(index) {
208
+ run('security', ['add-generic-password', '-U', '-s', SERVICE, '-a', MAC_INDEX, '-w', macEncode(JSON.stringify(index)), '-T', '/usr/bin/security']);
209
+ }
210
+ // ─── Public surface ──────────────────────────────────────────────
211
+ function setSecret(name, value, description = '') {
212
+ const backend = requireBackend();
213
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
214
+ throw new Error(`Invalid secret name "${name}". Use SCREAMING_SNAKE_CASE, e.g. WORDPRESS_APP_PASSWORD.`);
215
+ }
216
+ if (!value)
217
+ throw new Error('Refusing to store an empty value.');
218
+ if (backend === 'windows-dpapi') {
219
+ const store = winRead();
220
+ store[name] = { value, description, updated: new Date().toISOString().slice(0, 10) };
221
+ winWrite(store);
222
+ return;
223
+ }
224
+ if (backend === 'macos-keychain') {
225
+ // -U updates in place instead of erroring when the item already exists.
226
+ // -w with the value on argv is visible to `ps` for the life of the call;
227
+ // security(1) offers no stdin form, so the exposure is unavoidable and
228
+ // measured in milliseconds on the user's own machine.
229
+ const r = run('security', ['add-generic-password', '-U', '-s', SERVICE, '-a', name, '-w', macEncode(value), '-j', description, '-T', '/usr/bin/security']);
230
+ if (r.status !== 0)
231
+ throw new KeychainUnavailable(`Keychain refused the write: ${(r.stderr || '').trim()}`);
232
+ const index = macReadIndex();
233
+ index[name] = { description, updated: new Date().toISOString().slice(0, 10) };
234
+ macWriteIndex(index);
235
+ return;
236
+ }
237
+ // secret-tool reads the secret from stdin, so it never reaches the process
238
+ // table. It stays open waiting for EOF, which spawnSync gives it.
239
+ const r = run('secret-tool', ['store', '--label', `CRBRO ${name}`, 'service', SERVICE, 'account', name, 'description', description], value);
240
+ if (r.status !== 0)
241
+ throw new KeychainUnavailable(`secret-tool refused the write: ${(r.stderr || '').trim()}`);
242
+ }
243
+ /**
244
+ * The value, or null when there is no such secret. An environment variable of
245
+ * the same name wins, which is what makes CI and one-off overrides work
246
+ * without touching the store.
247
+ */
248
+ function getSecret(name) {
249
+ const fromEnv = process.env[name];
250
+ if (fromEnv)
251
+ return fromEnv;
252
+ const backend = requireBackend();
253
+ if (backend === 'windows-dpapi') {
254
+ return winRead()[name]?.value ?? null;
255
+ }
256
+ if (backend === 'macos-keychain') {
257
+ const r = run('security', ['find-generic-password', '-s', SERVICE, '-a', name, '-w']);
258
+ // security(1) returns the OSStatus itself, truncated to eight bits: 44 is
259
+ // errSecItemNotFound, 36 is errSecInteractionNotAllowed — a locked
260
+ // keychain, which is the normal state over SSH and in CI. Reporting that
261
+ // one as "no such secret" would send someone hunting for a typo.
262
+ if (r.status === 36) {
263
+ throw new KeychainUnavailable('The macOS keychain is locked, which is usual over SSH or in CI. Unlock it in a desktop session, ' +
264
+ 'or pass the credential as an environment variable instead.');
265
+ }
266
+ if (r.status !== 0)
267
+ return null;
268
+ return macDecode(r.stdout);
269
+ }
270
+ // Unlike security(1), secret-tool adds no trailing newline when stdout
271
+ // is a pipe: write_password_stdout only appends one for a tty. Trimming
272
+ // here would silently eat a newline that was genuinely part of the secret.
273
+ // Exit 1 with nothing on stderr is how it reports "no such item"; anything
274
+ // on stderr is a real failure and must not be mistaken for absence.
275
+ const r = run('secret-tool', ['lookup', 'service', SERVICE, 'account', name]);
276
+ if (r.status === 0)
277
+ return r.stdout;
278
+ if (r.status === 1 && !(r.stderr || '').trim())
279
+ return null;
280
+ throw new KeychainUnavailable(`secret-tool failed: ${(r.stderr || '').trim() || `exit ${r.status}`}`);
281
+ }
282
+ /** Names and descriptions only. Values are never returned by design. */
283
+ function listSecrets() {
284
+ const backend = requireBackend();
285
+ if (backend === 'windows-dpapi') {
286
+ return Object.entries(winRead())
287
+ .map(([name, e]) => ({ name, description: e.description, updated: e.updated }))
288
+ .sort((a, b) => a.name.localeCompare(b.name));
289
+ }
290
+ if (backend === 'macos-keychain') {
291
+ return Object.entries(macReadIndex())
292
+ .map(([name, e]) => ({ name, description: e.description, updated: e.updated }))
293
+ .sort((a, b) => a.name.localeCompare(b.name));
294
+ }
295
+ const r = run('secret-tool', ['search', '--all', 'service', SERVICE]);
296
+ if (r.status !== 0)
297
+ return [];
298
+ const names = [...r.stdout.matchAll(/^attribute\.account = (.+)$/gm)].map(m => m[1].trim());
299
+ return [...new Set(names)].sort().map(name => ({ name, description: '', updated: '' }));
300
+ }
301
+ function removeSecret(name) {
302
+ const backend = requireBackend();
303
+ if (backend === 'windows-dpapi') {
304
+ const store = winRead();
305
+ if (!(name in store))
306
+ return false;
307
+ delete store[name];
308
+ winWrite(store);
309
+ return true;
310
+ }
311
+ if (backend === 'macos-keychain') {
312
+ const gone = run('security', ['delete-generic-password', '-s', SERVICE, '-a', name]).status === 0;
313
+ if (gone) {
314
+ const index = macReadIndex();
315
+ delete index[name];
316
+ macWriteIndex(index);
317
+ }
318
+ return gone;
319
+ }
320
+ return run('secret-tool', ['clear', 'service', SERVICE, 'account', name]).status === 0;
321
+ }
322
+ /** Windows only, and only for tests: forget the sealed file entirely. */
323
+ function _resetWindowsStoreForTests() {
324
+ invalidateCache();
325
+ const { file } = winPaths();
326
+ if ((0, node_fs_1.existsSync)(file))
327
+ (0, node_fs_1.unlinkSync)(file);
328
+ }
329
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../src/engine/keychain.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgBpE,wBAAgB,YAAY,IAAI,SAAS,CAqgCxC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoCpE,wBAAgB,YAAY,IAAI,SAAS,CAkmCxC"}
package/dist/server.js CHANGED
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  // ─── CRBRO MCP Server ────────────────────────────────────────────
3
- // Main server with all 15 tools registered
3
+ // Main server with all 22 tools registered
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.createServer = createServer;
6
6
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
7
7
  const zod_1 = require("zod");
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
8
10
  const brain_js_1 = require("./engine/brain.js");
9
11
  const cortex_js_1 = require("./engine/cortex.js");
10
12
  const synapses_js_1 = require("./engine/synapses.js");
@@ -14,10 +16,26 @@ const prefrontal_js_1 = require("./engine/prefrontal.js");
14
16
  const index_js_1 = require("./search/index.js");
15
17
  const maintenance_js_1 = require("./engine/maintenance.js");
16
18
  const space_js_1 = require("./sync/space.js");
19
+ const keychain_js_1 = require("./engine/keychain.js");
20
+ /**
21
+ * The version of CRBRO that is actually running. The manifest carries its own
22
+ * version, but that one stamps the brain FORMAT and has not moved since 1.0.0
23
+ * — reporting it as "the version" told every user the same thing regardless of
24
+ * what they had installed, which is no use to anyone deciding whether to
25
+ * update.
26
+ */
27
+ function runningVersion() {
28
+ try {
29
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf8')).version;
30
+ }
31
+ catch {
32
+ return 'unknown';
33
+ }
34
+ }
17
35
  function createServer() {
18
36
  const server = new mcp_js_1.McpServer({
19
37
  name: 'crbro-memory',
20
- version: '1.7.0',
38
+ version: '1.8.1',
21
39
  });
22
40
  // ─── Initialize engines ──────────────────────────────────────
23
41
  const brain = new brain_js_1.Brain();
@@ -37,9 +55,9 @@ function createServer() {
37
55
  // to this machine's own log. Nobody ever writes to anyone else's file, so
38
56
  // two people working at once have nothing to collide over.
39
57
  (0, space_js_1.attachSync)(brain, cortex);
40
- // v1.4.0: CRBRO is fully free — all 15 tools, no license, no network calls.
41
- // The former license engine (Firestore-backed freemium) lives in git history
42
- // before this version if it is ever needed again.
58
+ // v1.4.0: CRBRO is fully free — no license, no network calls. The former
59
+ // license engine (Firestore-backed freemium) lives in git history before
60
+ // that version if it is ever needed again.
43
61
  // ═══════════════════════════════════════════════════════════════
44
62
  // TOOL 1: crbro_boot — Boot sequence
45
63
  // ═══════════════════════════════════════════════════════════════
@@ -111,7 +129,8 @@ function createServer() {
111
129
  content: [{
112
130
  type: 'text',
113
131
  text: JSON.stringify({
114
- version: manifest.version,
132
+ crbro_version: runningVersion(),
133
+ brain_format: manifest.version,
115
134
  total_neurons: manifest.total_neurons,
116
135
  total_synapses: manifest.total_synapses,
117
136
  total_sessions: manifest.total_sessions,
@@ -175,7 +194,8 @@ function createServer() {
175
194
  redaction_note: result.redacted.length > 0
176
195
  ? `Stored, but ${result.redacted.length} credential(s) were replaced with a marker: ` +
177
196
  `${result.redacted.join(', ')}. The sentence around them was kept. ` +
178
- 'Tell the user, and do not try to store the value again.'
197
+ 'Do not try to store the value again. Offer the user crbro_secret instead: ' +
198
+ 'it puts the credential in the OS keychain, and then you record only its name here.'
179
199
  : undefined,
180
200
  total_facts: result.neuron.facts.length,
181
201
  total_decisions: result.neuron.decisions.length,
@@ -701,6 +721,90 @@ function createServer() {
701
721
  }
702
722
  });
703
723
  // ═══════════════════════════════════════════════════════════════
724
+ // TOOL 22: crbro_secret — Credentials, brokered to the OS keychain
725
+ // ═══════════════════════════════════════════════════════════════
726
+ server.tool('crbro_secret', 'Store and read credentials in the operating system own keychain — macOS Keychain, the Linux Secret Service, or DPAPI on Windows. CRBRO keeps no copy and invents no crypto: it brokers access to the store the machine already has, outside the brain, where no sync and no team space can reach it. Use it the moment the user hands you a credential: store it here, then record only the NAME with crbro_learn, never the value. Use get when a task needs one, and do not print the value back to the user unless they asked for that specific secret. Names are SCREAMING_SNAKE_CASE, e.g. WORDPRESS_APP_PASSWORD.', {
727
+ action: zod_1.z.enum(['get', 'set', 'list', 'remove', 'status'])
728
+ .describe('get = read one, set = store or update one, list = names only, remove = delete one, status = which keychain this machine offers'),
729
+ name: zod_1.z.string().optional().describe('Secret name in SCREAMING_SNAKE_CASE'),
730
+ value: zod_1.z.string().optional().describe('The credential itself. Only for set.'),
731
+ description: zod_1.z.string().optional().describe('What it is for, e.g. "WordPress example.com - REST API"'),
732
+ }, async (args) => {
733
+ try {
734
+ let payload;
735
+ if (args.action === 'status') {
736
+ const { backend, reason } = (0, keychain_js_1.detectBackend)();
737
+ payload = {
738
+ backend,
739
+ available: backend !== null,
740
+ message: backend
741
+ ? `Credentials on this machine are stored in: ${backend}.`
742
+ : `No credential store available here. ${reason ?? ''} Credentials can still be passed as environment variables.`,
743
+ };
744
+ }
745
+ else if (args.action === 'list') {
746
+ const entries = (0, keychain_js_1.listSecrets)();
747
+ payload = {
748
+ count: entries.length,
749
+ secrets: entries,
750
+ note: 'Names only. Values are never listed, by design.',
751
+ };
752
+ }
753
+ else if (!args.name) {
754
+ throw new Error(`"name" is required for action "${args.action}".`);
755
+ }
756
+ else if (args.action === 'set') {
757
+ if (!args.value)
758
+ throw new Error('"value" is required for action "set".');
759
+ (0, keychain_js_1.setSecret)(args.name, args.value, args.description ?? '');
760
+ payload = {
761
+ stored: args.name,
762
+ message: `Stored in the OS keychain as ${args.name}. Now record the NAME in the brain with crbro_learn — never the value — so a later session knows where to look.`,
763
+ };
764
+ }
765
+ else if (args.action === 'remove') {
766
+ const gone = (0, keychain_js_1.removeSecret)(args.name);
767
+ payload = {
768
+ removed: gone,
769
+ message: gone
770
+ ? `${args.name} deleted from the keychain.`
771
+ : `No secret named ${args.name} was found.`,
772
+ };
773
+ }
774
+ else {
775
+ const value = (0, keychain_js_1.getSecret)(args.name);
776
+ payload = value === null
777
+ ? {
778
+ found: false,
779
+ message: `No secret named ${args.name}. Run action "list" to see what is stored, or ask the user for it and store it with action "set".`,
780
+ }
781
+ : {
782
+ found: true,
783
+ name: args.name,
784
+ value,
785
+ note: 'Use it for the task at hand. Do not repeat it back to the user and do not write it into any file.',
786
+ };
787
+ }
788
+ return {
789
+ content: [{
790
+ type: 'text',
791
+ text: JSON.stringify(payload, null, 2),
792
+ }],
793
+ };
794
+ }
795
+ catch (err) {
796
+ return {
797
+ content: [{
798
+ type: 'text',
799
+ text: err instanceof keychain_js_1.KeychainUnavailable
800
+ ? `No credential store available: ${err.message}`
801
+ : `CRBRO secret error: ${err instanceof Error ? err.message : String(err)}`,
802
+ }],
803
+ isError: true,
804
+ };
805
+ }
806
+ });
807
+ // ═══════════════════════════════════════════════════════════════
704
808
  // TOOL 18: crbro_forget — Remove knowledge for good
705
809
  // ═══════════════════════════════════════════════════════════════
706
810
  server.tool('crbro_forget', 'Permanently remove facts from a neuron. This is for things that must not exist at all — a credential, personal data, something stored by mistake. For knowledge that merely stopped being true, use crbro_revise instead, which keeps the history. The whole neuron is copied to .quarantine/ before anything is removed, so a mistake can be undone by hand. Always tell the user what you are about to remove and get their agreement first.', {