@chatpanel/gateway 0.6.34 → 0.6.36
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 +72 -0
- package/bin/chatpanel-gateway.js +5 -0
- package/package.json +2 -2
- package/src/local-status.js +105 -0
- package/src/mcp.js +12 -5
- package/src/server.js +8 -1
package/README.md
CHANGED
|
@@ -172,6 +172,62 @@ 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
|
+
**Claude Code** — one command (`--scope user` makes it available in every project):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
claude mcp add --scope user chatpanel chatpanel-gateway mcp
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Any other MCP client** — run the stdio server `chatpanel-gateway mcp`, or point at it the
|
|
201
|
+
way your client configures a `command` + `args` stdio server.
|
|
202
|
+
|
|
203
|
+
> If your client launches with a stripped `PATH` and cannot find `chatpanel-gateway`, use
|
|
204
|
+
> the absolute path (find it with `which chatpanel-gateway`).
|
|
205
|
+
|
|
206
|
+
### Tools it exposes
|
|
207
|
+
|
|
208
|
+
| Tool | What it does |
|
|
209
|
+
|------|--------------|
|
|
210
|
+
| `search_history` | Full-text search your chats, meetings and notes by relevance |
|
|
211
|
+
| `get_record` | Fetch one record's full text by id (`chat:…`, `meeting:…`, `note:…`) |
|
|
212
|
+
| `list_history` | Browse/page the corpus (newest first, no bodies) |
|
|
213
|
+
| `list_skills` | List every installed skill (name + one-line description, and where it came from) |
|
|
214
|
+
| `open_skill` | Load one skill's full instructions by name |
|
|
215
|
+
| `read_skill_file` | Read a reference file a skill's instructions point at |
|
|
216
|
+
|
|
217
|
+
Everything a history tool returns is **redacted** with the same engine
|
|
218
|
+
([`@chatpanel/pii`](https://github.com/chatpanel/chatpanel-pii)) the gateway uses for model
|
|
219
|
+
traffic — the real values never leave your device. Skill scripts are never served as text.
|
|
220
|
+
|
|
221
|
+
### One local view
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
chatpanel-gateway local
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
prints what is running — the gateway (this) and the bridge (your agents + skills) — so you
|
|
228
|
+
can see the whole local runtime at a glance. The bridge is an optional companion; a missing
|
|
229
|
+
one is reported plainly, never as an error.
|
|
230
|
+
|
|
175
231
|
## How it fits with ChatPanel
|
|
176
232
|
|
|
177
233
|
The [extension](https://github.com/chatpanel/chatpanel-extension) redacts inside
|
|
@@ -192,6 +248,22 @@ agent's own multi-turn loop is blinded, not just the first prompt.
|
|
|
192
248
|
edits. The default tier touches only structured secrets and (in `full`) detected
|
|
193
249
|
entities — keep your dictionary prose-focused.
|
|
194
250
|
|
|
251
|
+
**Using it as an MCP server:**
|
|
252
|
+
|
|
253
|
+
- The **gateway must be running** for any of its tools to work — it is a background
|
|
254
|
+
service (`chatpanel-gateway --install` registers it to start at login). If a tool
|
|
255
|
+
returns *"the ChatPanel gateway is not running"*, start it and retry; the message tells
|
|
256
|
+
you the command.
|
|
257
|
+
- **Skills need the bridge** (an optional companion). Without it, the history tools still
|
|
258
|
+
work and the skill tools return a one-line *"the bridge is not running — install it
|
|
259
|
+
with …"* — no silent empty result.
|
|
260
|
+
- History tools search the gateway's **warm store**, which is seeded from your ChatPanel
|
|
261
|
+
backups. If `list_history` says it is empty, the gateway has not been seeded yet — open
|
|
262
|
+
ChatPanel so a backup lands, or check the [ingest docs](#endpoints).
|
|
263
|
+
- Everything returned is **redacted** at the configured tier. A tool result may contain a
|
|
264
|
+
placeholder like `[[EMAIL_1]]` where a value was blinded — that is the privacy guarantee
|
|
265
|
+
working, not a bug.
|
|
266
|
+
|
|
195
267
|
## License
|
|
196
268
|
|
|
197
269
|
Source-available under the same license as the ChatPanel extension and bridge —
|
package/bin/chatpanel-gateway.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// chatpanel-gateway start the gateway (foreground)
|
|
5
5
|
// chatpanel-gateway mcp stdio MCP server exposing warm history as tools
|
|
6
|
+
// chatpanel-gateway local show the local runtime — bridge + gateway, one view
|
|
6
7
|
// chatpanel-gateway --install register login auto-start + start now
|
|
7
8
|
// chatpanel-gateway --uninstall remove login auto-start
|
|
8
9
|
// chatpanel-gateway --status is auto-start registered?
|
|
@@ -19,6 +20,10 @@ try {
|
|
|
19
20
|
// server.js (which would open a second handle on the warm SQLite store).
|
|
20
21
|
const { runMcpServer } = await import('../src/mcp.js');
|
|
21
22
|
await runMcpServer();
|
|
23
|
+
} else if (arg === 'local') {
|
|
24
|
+
// Read-only unified view of both services. No server.js import — just HTTP probes.
|
|
25
|
+
const { localStatus, formatLocalStatus } = await import('../src/local-status.js');
|
|
26
|
+
process.stdout.write(formatLocalStatus(await localStatus()));
|
|
22
27
|
} else {
|
|
23
28
|
const { start, VERSION } = await import('../src/server.js');
|
|
24
29
|
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.
|
|
3
|
+
"version": "0.6.36",
|
|
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
|
|
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,105 @@
|
|
|
1
|
+
// local-status.js — one view of ChatPanel's local runtime: the bridge and the gateway.
|
|
2
|
+
//
|
|
3
|
+
// U3 of docs/bridge-gateway-unification.md: a person should be able to ask "what's running
|
|
4
|
+
// locally?" once and get a straight answer, without knowing there are two services on two
|
|
5
|
+
// ports. This is the read side of that — it probes both over HTTP and reports. It does NOT
|
|
6
|
+
// control either service (start/stop stays with each module's own installer, which owns its
|
|
7
|
+
// launchd/systemd unit); managing another module's service from here would duplicate the
|
|
8
|
+
// knowledge of how to do it and drift.
|
|
9
|
+
//
|
|
10
|
+
// The bridge is OPTIONAL from the gateway's side and the gateway is OPTIONAL from the
|
|
11
|
+
// bridge's — so a missing one is reported plainly, never as an error.
|
|
12
|
+
|
|
13
|
+
import { loadConfig } from './config.js';
|
|
14
|
+
|
|
15
|
+
function gatewayUrl() {
|
|
16
|
+
try {
|
|
17
|
+
return `http://127.0.0.1:${loadConfig().port || 4320}`;
|
|
18
|
+
} catch {
|
|
19
|
+
return 'http://127.0.0.1:4320';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function bridgeUrl() {
|
|
24
|
+
try {
|
|
25
|
+
return String(loadConfig().bridge?.url || 'http://127.0.0.1:4319').replace(/\/+$/, '');
|
|
26
|
+
} catch {
|
|
27
|
+
return 'http://127.0.0.1:4319';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function probe(url, path = '/health') {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(url + path, { signal: AbortSignal.timeout(2500) });
|
|
34
|
+
if (!res.ok) return { ok: false, reason: `HTTP ${res.status}` };
|
|
35
|
+
return { ok: true, data: await res.json().catch(() => ({})) };
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return { ok: false, reason: e?.name === 'TimeoutError' ? 'no response' : (e?.message || 'unreachable') };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A structured picture of the local runtime — for the `local` command and for the gateway
|
|
43
|
+
* to log at startup. Pure except the two probes; the caller decides how to render it.
|
|
44
|
+
*/
|
|
45
|
+
export async function localStatus({ gatewayUrl: gwOverride, bridgeUrl: brOverride } = {}) {
|
|
46
|
+
const gwUrl = gwOverride || gatewayUrl();
|
|
47
|
+
const brUrl = brOverride || bridgeUrl();
|
|
48
|
+
const [gw, br] = await Promise.all([probe(gwUrl), probe(brUrl)]);
|
|
49
|
+
const skills = br.ok ? await probe(brUrl, '/skills').then((r) => (r.ok ? (r.data.skills || []).length : null)).catch(() => null) : null;
|
|
50
|
+
return {
|
|
51
|
+
gateway: {
|
|
52
|
+
url: gwUrl,
|
|
53
|
+
running: gw.ok,
|
|
54
|
+
version: gw.ok ? gw.data.version : null,
|
|
55
|
+
tier: gw.ok ? gw.data.tier : null,
|
|
56
|
+
reason: gw.ok ? null : gw.reason,
|
|
57
|
+
},
|
|
58
|
+
bridge: {
|
|
59
|
+
url: brUrl,
|
|
60
|
+
running: br.ok,
|
|
61
|
+
version: br.ok ? br.data.version : null,
|
|
62
|
+
agents: br.ok ? (br.data.agents || []).filter((a) => a.available).length : null,
|
|
63
|
+
skills: skills ?? (br.ok ? br.data.skills?.count ?? null : null),
|
|
64
|
+
reason: br.ok ? null : br.reason,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Human-readable block for the CLI. */
|
|
70
|
+
export function formatLocalStatus(s) {
|
|
71
|
+
const line = (name, m, extra) => {
|
|
72
|
+
const dot = m.running ? '●' : '○';
|
|
73
|
+
const head = m.running ? `${name} running · v${m.version}` : `${name} not running${m.reason ? ` (${m.reason})` : ''}`;
|
|
74
|
+
return ` ${dot} ${head}\n ${extra}`;
|
|
75
|
+
};
|
|
76
|
+
const gw = line('Gateway', s.gateway, s.gateway.running
|
|
77
|
+
? `Privacy layer: redaction, routing, voice. ${s.gateway.url}`
|
|
78
|
+
: `Optional upgrade (redaction, routing, voice). Start with: chatpanel-gateway --install`);
|
|
79
|
+
const brExtra = s.bridge.running
|
|
80
|
+
? `Local agents & skills${s.bridge.agents != null ? ` · ${s.bridge.agents} agent(s)` : ''}${s.bridge.skills != null ? ` · ${s.bridge.skills} skill(s)` : ''}. ${s.bridge.url}`
|
|
81
|
+
: `Runs your local coding agents and skills. Start with: curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash`;
|
|
82
|
+
const br = line('Bridge', s.bridge, brExtra);
|
|
83
|
+
const summary = s.bridge.running && s.gateway.running
|
|
84
|
+
? 'Both running — local traffic can route through the gateway\'s privacy layer.'
|
|
85
|
+
: s.bridge.running
|
|
86
|
+
? 'Bridge up. The gateway is an optional upgrade.'
|
|
87
|
+
: s.gateway.running
|
|
88
|
+
? 'Gateway up. Start the bridge to use local agents and skills.'
|
|
89
|
+
: 'Neither running.';
|
|
90
|
+
return `ChatPanel local\n\n${gw}\n\n${br}\n\n ${summary}\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One-line note for the gateway to log at startup, so the operator sees the unified picture
|
|
95
|
+
* without running anything. Never throws; a probe failure just says "not detected".
|
|
96
|
+
*/
|
|
97
|
+
export async function bridgePresenceNote(brOverride) {
|
|
98
|
+
const url = brOverride || bridgeUrl();
|
|
99
|
+
const br = await probe(url);
|
|
100
|
+
if (br.ok) {
|
|
101
|
+
const n = (br.data.skills?.count ?? null);
|
|
102
|
+
return `bridge detected at ${url} (v${br.data.version}${n != null ? `, ${n} skills` : ''}) — its agents and skills are available through this gateway.`;
|
|
103
|
+
}
|
|
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.`;
|
|
105
|
+
}
|
package/src/mcp.js
CHANGED
|
@@ -120,9 +120,16 @@ const TOOLS = [
|
|
|
120
120
|
];
|
|
121
121
|
|
|
122
122
|
async function gatewayJson(path, init) {
|
|
123
|
-
|
|
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 `
|
|
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.
|
|
48
|
+
export const VERSION = '0.6.36';
|
|
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.
|
|
@@ -1013,6 +1013,13 @@ export function start(cfg = loadConfig()) {
|
|
|
1013
1013
|
server.listen(cfg.port, cfg.host, () => {
|
|
1014
1014
|
console.log(`ChatPanel Privacy Gateway v${VERSION} on http://${cfg.host}:${cfg.port}`);
|
|
1015
1015
|
console.log(` backend : ${cfg.backend}` + (cfg.backend === 'bridge' ? ` (agent: ${cfg.bridge.agent}, via ${cfg.bridge.url})` : ''));
|
|
1016
|
+
// U3: report the bridge at startup so the operator sees the unified picture without
|
|
1017
|
+
// running anything. Detect only — never force-spawn a managed service. Best-effort and
|
|
1018
|
+
// non-fatal: a probe failure just logs "not detected".
|
|
1019
|
+
import('./local-status.js')
|
|
1020
|
+
.then((m) => m.bridgePresenceNote())
|
|
1021
|
+
.then((note) => console.log(` bridge : ${note}`))
|
|
1022
|
+
.catch(() => {});
|
|
1016
1023
|
console.log(` redaction: ${cfg.redaction.tier}` + (cfg.redaction.detection?.backend && cfg.redaction.detection.backend !== 'off'
|
|
1017
1024
|
? ` + ${cfg.redaction.detection.backend} detector` : (cfg.ner?.autostart ? ' (+ NER starting…)' : '')));
|
|
1018
1025
|
// M7: a non-loopback bind exposes the gateway on the LAN, where the per-request
|