@chatpanel/gateway 0.6.35 → 0.6.37

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/README.md CHANGED
@@ -172,6 +172,85 @@ Streaming (SSE) is supported on all three: placeholders are restored on the fly,
172
172
  holding back a tail so a token split across chunks (`[[PER` … `SON_1]]`) still
173
173
  restores cleanly.
174
174
 
175
+ ## Use it as an MCP server (Codex, Claude Code, any MCP client)
176
+
177
+ Point one MCP server at the gateway and any CLI agent can reach your **local history**
178
+ (past chats, meeting transcripts, notes — redacted on the way out) **and every skill
179
+ installed on your machine** — across Claude Code, Codex, Copilot, Gemini, Hermes,
180
+ `~/.agents/skills` and any folder you configure. History is served by the gateway; skills
181
+ are proxied from the [bridge](https://github.com/chatpanel/chatpanel-bridge) (optional — if
182
+ it is not running, the history tools still work and the skill tools say so).
183
+
184
+ It is a stdio MCP server: `chatpanel-gateway mcp`.
185
+
186
+ **Codex** — add to `~/.codex/config.toml`:
187
+
188
+ ```toml
189
+ [mcp_servers.chatpanel]
190
+ command = "chatpanel-gateway"
191
+ args = ["mcp"]
192
+ ```
193
+
194
+ > **Corporate Codex with an approval guardian?** If a tool call is rejected with
195
+ > *"Automatic approval review failed"* (an `approvals_reviewer = "auto_review"` policy
196
+ > can't assess a tool it doesn't know), pre-approve ChatPanel's read-only tools the same
197
+ > way you would any other server — one block per tool:
198
+ >
199
+ > ```toml
200
+ > [mcp_servers.chatpanel.tools.search_history]
201
+ > approval_mode = "approve"
202
+ > [mcp_servers.chatpanel.tools.get_record]
203
+ > approval_mode = "approve"
204
+ > [mcp_servers.chatpanel.tools.list_history]
205
+ > approval_mode = "approve"
206
+ > [mcp_servers.chatpanel.tools.list_skills]
207
+ > approval_mode = "approve"
208
+ > [mcp_servers.chatpanel.tools.open_skill]
209
+ > approval_mode = "approve"
210
+ > [mcp_servers.chatpanel.tools.read_skill_file]
211
+ > approval_mode = "approve"
212
+ > ```
213
+ >
214
+ > All six only read local data (your redacted history and your installed skills), so
215
+ > approving them carries no write or network risk.
216
+
217
+ **Claude Code** — one command (`--scope user` makes it available in every project):
218
+
219
+ ```bash
220
+ claude mcp add --scope user chatpanel chatpanel-gateway mcp
221
+ ```
222
+
223
+ **Any other MCP client** — run the stdio server `chatpanel-gateway mcp`, or point at it the
224
+ way your client configures a `command` + `args` stdio server.
225
+
226
+ > If your client launches with a stripped `PATH` and cannot find `chatpanel-gateway`, use
227
+ > the absolute path (find it with `which chatpanel-gateway`).
228
+
229
+ ### Tools it exposes
230
+
231
+ | Tool | What it does |
232
+ |------|--------------|
233
+ | `search_history` | Full-text search your chats, meetings and notes by relevance |
234
+ | `get_record` | Fetch one record's full text by id (`chat:…`, `meeting:…`, `note:…`) |
235
+ | `list_history` | Browse/page the corpus (newest first, no bodies) |
236
+ | `list_skills` | List every installed skill (name + one-line description, and where it came from) |
237
+ | `open_skill` | Load one skill's full instructions by name |
238
+ | `read_skill_file` | Read a reference file a skill's instructions point at |
239
+
240
+ Everything a history tool returns is **redacted** with the same engine
241
+ ([`@chatpanel/pii`](https://github.com/chatpanel/chatpanel-pii)) the gateway uses for model
242
+ traffic — the real values never leave your device. Skill scripts are never served as text.
243
+
244
+ ### One local view
245
+
246
+ ```bash
247
+ chatpanel-gateway local
248
+ ```
249
+
250
+ prints what is running — the gateway (this) and the bridge (your agents + skills) — so you
251
+ can see the whole local runtime at a glance. The bridge is an optional companion; a missing
252
+ one is reported plainly, never as an error.
253
+
175
254
  ## How it fits with ChatPanel
176
255
 
177
256
  The [extension](https://github.com/chatpanel/chatpanel-extension) redacts inside
@@ -192,6 +271,22 @@ agent's own multi-turn loop is blinded, not just the first prompt.
192
271
  edits. The default tier touches only structured secrets and (in `full`) detected
193
272
  entities — keep your dictionary prose-focused.
194
273
 
274
+ **Using it as an MCP server:**
275
+
276
+ - The **gateway must be running** for any of its tools to work — it is a background
277
+ service (`chatpanel-gateway --install` registers it to start at login). If a tool
278
+ returns *"the ChatPanel gateway is not running"*, start it and retry; the message tells
279
+ you the command.
280
+ - **Skills need the bridge** (an optional companion). Without it, the history tools still
281
+ work and the skill tools return a one-line *"the bridge is not running — install it
282
+ with …"* — no silent empty result.
283
+ - History tools search the gateway's **warm store**, which is seeded from your ChatPanel
284
+ backups. If `list_history` says it is empty, the gateway has not been seeded yet — open
285
+ ChatPanel so a backup lands, or check the [ingest docs](#endpoints).
286
+ - Everything returned is **redacted** at the configured tier. A tool result may contain a
287
+ placeholder like `[[EMAIL_1]]` where a value was blinded — that is the privacy guarantee
288
+ working, not a bug.
289
+
195
290
  ## License
196
291
 
197
292
  Source-available under the same license as the ChatPanel extension and bridge —
@@ -4,6 +4,7 @@
4
4
  // chatpanel-gateway start the gateway (foreground)
5
5
  // chatpanel-gateway mcp stdio MCP server exposing warm history as tools
6
6
  // chatpanel-gateway local show the local runtime — bridge + gateway, one view
7
+ // chatpanel-gateway connect point your CLI agents (Codex, Claude Code, …) at this server
7
8
  // chatpanel-gateway --install register login auto-start + start now
8
9
  // chatpanel-gateway --uninstall remove login auto-start
9
10
  // chatpanel-gateway --status is auto-start registered?
@@ -24,6 +25,10 @@ try {
24
25
  // Read-only unified view of both services. No server.js import — just HTTP probes.
25
26
  const { localStatus, formatLocalStatus } = await import('../src/local-status.js');
26
27
  process.stdout.write(formatLocalStatus(await localStatus()));
28
+ } else if (arg === 'connect') {
29
+ const { connectAgents, formatConnect } = await import('../src/connect-agents.js');
30
+ const dryRun = process.argv.includes('--dry-run') || process.argv.includes('-n');
31
+ process.stdout.write(formatConnect(connectAgents({ dryRun }), { dryRun }));
27
32
  } else {
28
33
  const { start, VERSION } = await import('../src/server.js');
29
34
  const { installService, uninstallService, serviceStatus } = await import('../src/service.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.35",
3
+ "version": "0.6.37",
4
4
  "description": "Local privacy gateway \u2014 redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "scripts": {
14
14
  "start": "node bin/chatpanel-gateway.js",
15
- "test": "node --test",
15
+ "test": "node scripts/run-tests.mjs",
16
16
  "typecheck": "tsc -p tsconfig.json",
17
17
  "build:bin": "bash scripts/build-binaries.sh"
18
18
  },
@@ -0,0 +1,173 @@
1
+ // connect-agents.js — wire the local CLI agents to the ChatPanel MCP server in one step.
2
+ //
3
+ // `chatpanel-gateway connect` detects the agent CLIs installed on this machine and points
4
+ // each at `chatpanel-gateway mcp`, so a person gets their history + skills in every agent
5
+ // without hand-editing four different config files in four different formats.
6
+ //
7
+ // Two rules keep this safe to run unattended:
8
+ // • BACK UP then EDIT, and be IDEMPOTENT — running it twice changes nothing the second
9
+ // time. A user's existing MCP servers are never touched.
10
+ // • Only auto-write a format we can write CORRECTLY. Codex (TOML append), Claude Code
11
+ // (its own CLI) and Gemini (plain JSON) are safe. For agents whose config is JSONC with
12
+ // comments, or whose format we cannot verify, we PRINT the exact snippet instead of
13
+ // risking a corrupted config. Guessing a format and clobbering a config is worse than
14
+ // asking the user to paste four lines.
15
+
16
+ import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from 'node:fs';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { join } from 'node:path';
19
+ import os from 'node:os';
20
+
21
+ // The six read-only tools the ChatPanel MCP server exposes (history + skills). Named here so
22
+ // the Codex approval blocks stay in step with what the server actually advertises.
23
+ const TOOLS = ['search_history', 'get_record', 'list_history', 'list_skills', 'open_skill', 'read_skill_file'];
24
+
25
+ // Resolve the command a config should launch. A bare name works when the client inherits a
26
+ // normal PATH; an absolute path is the safe fallback when it does not.
27
+ export function resolveServerCommand(which = whichSync) {
28
+ const abs = which('chatpanel-gateway');
29
+ return abs || 'chatpanel-gateway';
30
+ }
31
+
32
+ function whichSync(bin) {
33
+ try {
34
+ return execFileSync('which', [bin], { encoding: 'utf8' }).trim() || null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function backup(path) {
41
+ const b = `${path}.bak.chatpanel`;
42
+ try { copyFileSync(path, b); return b; } catch { return null; }
43
+ }
44
+
45
+ // ── Codex ──────────────────────────────────────────────────────────────────────────────
46
+ // ~/.codex/config.toml. TOML, and Node has no TOML writer — but the change is a pure
47
+ // APPEND of a server block plus per-tool approval blocks, so a text append (guarded by a
48
+ // presence check) is correct and non-destructive. The approval blocks matter: a corporate
49
+ // Codex with `approvals_reviewer = "auto_review"` rejects a tool it cannot assess, and this
50
+ // is how every other server on such a setup is pre-approved.
51
+ function connectCodex({ cmd, dryRun, home }) {
52
+ const dir = join(home, '.codex');
53
+ const path = join(dir, 'config.toml');
54
+ if (!existsSync(dir)) return { id: 'codex', status: 'absent' };
55
+ const cur = existsSync(path) ? readFileSync(path, 'utf8') : '';
56
+ if (/\[mcp_servers\.chatpanel\]/.test(cur)) return { id: 'codex', status: 'already', path };
57
+
58
+ let block = '\n# ChatPanel — local history (redacted) + your installed skills, via one MCP server.\n';
59
+ block += `[mcp_servers.chatpanel]\ncommand = ${JSON.stringify(cmd)}\nargs = ["mcp"]\n\n`;
60
+ block += '# Read-only tools — pre-approved so a corporate auto_review guardian does not reject them.\n';
61
+ for (const t of TOOLS) block += `[mcp_servers.chatpanel.tools.${t}]\napproval_mode = "approve"\n\n`;
62
+
63
+ if (dryRun) return { id: 'codex', status: 'would', path, preview: block.trim() };
64
+ const bak = backup(path);
65
+ writeFileSync(path, `${cur.replace(/\s*$/, '')}\n${block}`);
66
+ return { id: 'codex', status: 'configured', path, backup: bak };
67
+ }
68
+
69
+ // ── Claude Code ────────────────────────────────────────────────────────────────────────
70
+ // Its own CLI owns ~/.claude.json, so use `claude mcp add` rather than editing it. --scope
71
+ // user makes it available in every project.
72
+ function connectClaude({ cmd, dryRun, which }) {
73
+ const claude = which('claude');
74
+ if (!claude) return { id: 'claude-code', status: 'absent' };
75
+ // Already added? `claude mcp get` exits non-zero when it is not there.
76
+ try { execFileSync(claude, ['mcp', 'get', 'chatpanel'], { stdio: 'ignore' }); return { id: 'claude-code', status: 'already' }; } catch { /* not present */ }
77
+ if (dryRun) return { id: 'claude-code', status: 'would', preview: `claude mcp add --scope user chatpanel ${cmd} mcp` };
78
+ try {
79
+ execFileSync(claude, ['mcp', 'add', '--scope', 'user', 'chatpanel', cmd, 'mcp'], { stdio: 'ignore' });
80
+ return { id: 'claude-code', status: 'configured' };
81
+ } catch (e) {
82
+ return { id: 'claude-code', status: 'error', message: e.message };
83
+ }
84
+ }
85
+
86
+ // ── Gemini CLI / Antigravity ─────────────────────────────────────────────────────────────
87
+ // ~/.gemini/settings.json — plain JSON, safe to parse and merge.
88
+ function connectGemini({ cmd, dryRun, home }) {
89
+ const dir = join(home, '.gemini');
90
+ const path = join(dir, 'settings.json');
91
+ if (!existsSync(dir)) return { id: 'gemini', status: 'absent' };
92
+ let cfg = {};
93
+ if (existsSync(path)) {
94
+ try { cfg = JSON.parse(readFileSync(path, 'utf8')); } catch { return { id: 'gemini', status: 'error', message: 'settings.json is not valid JSON — left untouched', path }; }
95
+ }
96
+ cfg.mcpServers = cfg.mcpServers || {};
97
+ if (cfg.mcpServers.chatpanel) return { id: 'gemini', status: 'already', path };
98
+ const entry = { command: cmd, args: ['mcp'] };
99
+ if (dryRun) return { id: 'gemini', status: 'would', path, preview: JSON.stringify({ mcpServers: { chatpanel: entry } }, null, 2) };
100
+ const bak = existsSync(path) ? backup(path) : null;
101
+ cfg.mcpServers.chatpanel = entry;
102
+ mkdirSync(dir, { recursive: true });
103
+ writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`);
104
+ return { id: 'gemini', status: 'configured', path, backup: bak };
105
+ }
106
+
107
+ // ── Agents we do NOT auto-write (JSONC with comments, or a format we can't verify) ───────
108
+ // Detected and given the exact snippet, rather than risking a clobbered config.
109
+ function manualAgent(id, present, snippet) {
110
+ return present ? { id, status: 'manual', snippet } : { id, status: 'absent' };
111
+ }
112
+
113
+ function otherAgents({ cmd, home, which }) {
114
+ const out = [];
115
+ // OpenCode — opencode.json / .jsonc. JSONC keeps comments; we won't rewrite and drop them.
116
+ out.push(manualAgent('opencode', existsSync(join(home, '.opencode')) || existsSync(join(home, '.config', 'opencode')),
117
+ `Add to your opencode config (opencode.json):\n "mcp": { "chatpanel": { "type": "local", "command": [${JSON.stringify(cmd)}, "mcp"], "enabled": true } }`));
118
+ // GitHub Copilot CLI.
119
+ out.push(manualAgent('copilot', existsSync(join(home, '.copilot')),
120
+ `Copilot CLI: add an MCP server named "chatpanel" running: ${cmd} mcp`));
121
+ // Hermes.
122
+ out.push(manualAgent('hermes', existsSync(join(home, '.hermes')) || !!which('hermes'),
123
+ `Hermes: register an MCP server "chatpanel" with command ${cmd} mcp (see Hermes' MCP docs).`));
124
+ // Pi.
125
+ out.push(manualAgent('pi', existsSync(join(home, '.pi')) || !!which('pi'),
126
+ `Pi: add an MCP server "chatpanel" running: ${cmd} mcp`));
127
+ return out;
128
+ }
129
+
130
+ /**
131
+ * Detect and (unless dryRun) configure every agent found. Returns one result per agent so a
132
+ * caller can render a summary. Pure-ish: the only side effects are the config writes, and
133
+ * dryRun suppresses them.
134
+ */
135
+ export function connectAgents({ dryRun = false, cmd, home = os.homedir(), which = whichSync } = {}) {
136
+ const serverCmd = cmd || resolveServerCommand(which);
137
+ const results = [
138
+ connectCodex({ cmd: serverCmd, dryRun, home }),
139
+ connectClaude({ cmd: serverCmd, dryRun, which }),
140
+ connectGemini({ cmd: serverCmd, dryRun, home }),
141
+ ...otherAgents({ cmd: serverCmd, home, which }),
142
+ ];
143
+ return { cmd: serverCmd, results };
144
+ }
145
+
146
+ const LABEL = {
147
+ codex: 'Codex', 'claude-code': 'Claude Code', gemini: 'Gemini / Antigravity',
148
+ opencode: 'OpenCode', copilot: 'GitHub Copilot', hermes: 'Hermes', pi: 'Pi',
149
+ };
150
+
151
+ /** Human summary for the CLI. */
152
+ export function formatConnect({ cmd, results }, { dryRun = false } = {}) {
153
+ const lines = [`ChatPanel MCP — connecting your agents to: ${cmd} mcp`, ''];
154
+ const icon = { configured: '✓', already: '•', would: '→', manual: '✎', absent: '·', error: '✕' };
155
+ for (const r of results) {
156
+ const name = LABEL[r.id] || r.id;
157
+ if (r.status === 'absent') continue; // don't list agents that aren't installed
158
+ const head = ` ${icon[r.status] || '?'} ${name}`;
159
+ if (r.status === 'configured') lines.push(`${head} — configured${r.backup ? ` (backup: ${r.backup})` : ''}`);
160
+ else if (r.status === 'already') lines.push(`${head} — already connected`);
161
+ else if (r.status === 'would') lines.push(`${head} — would configure:\n ${(r.preview || '').split('\n').join('\n ')}`);
162
+ else if (r.status === 'manual') lines.push(`${head} — add it yourself:\n ${r.snippet.split('\n').join('\n ')}`);
163
+ else if (r.status === 'error') lines.push(`${head} — ${r.message}`);
164
+ }
165
+ const configured = results.filter((r) => r.status === 'configured').length;
166
+ lines.push('');
167
+ if (dryRun) lines.push(' (dry run — nothing was written. Re-run without --dry-run to apply.)');
168
+ else if (configured) lines.push(` Done. Restart the configured agent(s) to pick up the new server.`);
169
+ else lines.push(' Nothing to change.');
170
+ const anyManual = results.some((r) => r.status === 'manual');
171
+ if (anyManual) lines.push(' Agents marked ✎ use a config format best edited by hand — the snippet above is exact.');
172
+ return `${lines.join('\n')}\n`;
173
+ }
@@ -42,9 +42,9 @@ async function probe(url, path = '/health') {
42
42
  * A structured picture of the local runtime — for the `local` command and for the gateway
43
43
  * to log at startup. Pure except the two probes; the caller decides how to render it.
44
44
  */
45
- export async function localStatus() {
46
- const gwUrl = gatewayUrl();
47
- const brUrl = bridgeUrl();
45
+ export async function localStatus({ gatewayUrl: gwOverride, bridgeUrl: brOverride } = {}) {
46
+ const gwUrl = gwOverride || gatewayUrl();
47
+ const brUrl = brOverride || bridgeUrl();
48
48
  const [gw, br] = await Promise.all([probe(gwUrl), probe(brUrl)]);
49
49
  const skills = br.ok ? await probe(brUrl, '/skills').then((r) => (r.ok ? (r.data.skills || []).length : null)).catch(() => null) : null;
50
50
  return {
@@ -94,11 +94,12 @@ export function formatLocalStatus(s) {
94
94
  * One-line note for the gateway to log at startup, so the operator sees the unified picture
95
95
  * without running anything. Never throws; a probe failure just says "not detected".
96
96
  */
97
- export async function bridgePresenceNote() {
98
- const br = await probe(bridgeUrl());
97
+ export async function bridgePresenceNote(brOverride) {
98
+ const url = brOverride || bridgeUrl();
99
+ const br = await probe(url);
99
100
  if (br.ok) {
100
101
  const n = (br.data.skills?.count ?? null);
101
- return `bridge detected at ${bridgeUrl()} (v${br.data.version}${n != null ? `, ${n} skills` : ''}) — its agents and skills are available through this gateway.`;
102
+ return `bridge detected at ${url} (v${br.data.version}${n != null ? `, ${n} skills` : ''}) — its agents and skills are available through this gateway.`;
102
103
  }
103
- return `bridge not detected at ${bridgeUrl()} — local agents/skills are unavailable until it runs (curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash). The gateway runs fine without it.`;
104
+ return `bridge not detected at ${url} — local agents/skills are unavailable until it runs (curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash). The gateway runs fine without it.`;
104
105
  }
package/src/mcp.js CHANGED
@@ -120,9 +120,16 @@ const TOOLS = [
120
120
  ];
121
121
 
122
122
  async function gatewayJson(path, init) {
123
- const res = await fetch(baseUrl() + path, init);
123
+ let res;
124
+ try {
125
+ res = await fetch(baseUrl() + path, init);
126
+ } catch (e) {
127
+ // The most common cause by far: the gateway service is not running. Say so, and how to
128
+ // fix it, so the agent can relay something actionable instead of a bare fetch error.
129
+ throw new Error(`the ChatPanel gateway is not running at ${baseUrl()} — start it with "chatpanel-gateway --install" (or run "chatpanel-gateway"), then retry. [${e.message}]`);
130
+ }
124
131
  const data = await res.json().catch(() => ({}));
125
- if (!res.ok) throw new Error(data?.error?.message || `gateway ${res.status}`);
132
+ if (!res.ok) throw new Error(data?.error?.message || `the gateway returned HTTP ${res.status} for ${path}`);
126
133
  return data;
127
134
  }
128
135
 
@@ -153,7 +160,7 @@ async function callTool(name, args = {}) {
153
160
  if (name === 'list_skills') {
154
161
  let data;
155
162
  try { data = await bridgeJson('/skills'); }
156
- catch (e) { return `The ChatPanel bridge is not reachable (${e.message}), so installed skills are unavailable. Start it to use skills.`; }
163
+ catch (e) { return `Installed skills are unavailable because the ChatPanel bridge is not running at ${bridgeBase()}. Install/start it with:\n curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash\nThen retry. History tools work without the bridge. [${e.message}]`; }
157
164
  const rows = data.skills || [];
158
165
  if (!rows.length) return 'No skills installed on this machine yet.';
159
166
  return [`${rows.length} skill(s) installed:`, ...rows.map((r) => `- ${r.command || r.id}: ${r.description || r.name}${r.origin?.source ? ` (from ${r.origin.source})` : ''}`)].join('\n') + '\n\nUse open_skill with a name to load its instructions.';
@@ -161,7 +168,7 @@ async function callTool(name, args = {}) {
161
168
  if (name === 'open_skill') {
162
169
  let data;
163
170
  try { data = await bridgeJson(`/skills/${encodeURIComponent(String(args.name || '').trim())}`); }
164
- catch (e) { return `Could not open "${args.name}": ${e.message}`; }
171
+ catch (e) { return `Could not open "${args.name}": ${e.message}. If the bridge isn't running, start it: curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash`; }
165
172
  return data.skill?.prompt || '(this skill has no extra instructions — just apply it.)';
166
173
  }
167
174
  if (name === 'read_skill_file') {
@@ -169,7 +176,7 @@ async function callTool(name, args = {}) {
169
176
  const path = String(args.path || '').trim().split('/').map(encodeURIComponent).join('/');
170
177
  let data;
171
178
  try { data = await bridgeJson(`/skills/${skill}/file/${path}`); }
172
- catch (e) { return `Could not read ${args.path}: ${e.message}`; }
179
+ catch (e) { return `Could not read ${args.path}: ${e.message}. If the bridge isn't running, start it: curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash`; }
173
180
  return data.text || '(empty)';
174
181
  }
175
182
  throw new Error(`unknown tool: ${name}`);
package/src/server.js CHANGED
@@ -45,7 +45,7 @@ import * as openai from './openai.js';
45
45
  import * as responses from './responses.js';
46
46
  import * as anthropic from './anthropic.js';
47
47
 
48
- export const VERSION = '0.6.35';
48
+ export const VERSION = '0.6.37';
49
49
 
50
50
  // WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
51
51
  // store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.