@chatpanel/gateway 0.6.33 → 0.6.35

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.
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.33",
4
- "description": "Local privacy gateway 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.",
3
+ "version": "0.6.35",
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": {
7
7
  "chatpanel-gateway": "bin/chatpanel-gateway.js"
@@ -0,0 +1,104 @@
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() {
46
+ const gwUrl = gatewayUrl();
47
+ const brUrl = 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() {
98
+ const br = await probe(bridgeUrl());
99
+ if (br.ok) {
100
+ 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
+ }
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
+ }
package/src/mcp.js CHANGED
@@ -7,8 +7,15 @@
7
7
  // It PROXIES to the already-running gateway's HTTP API (127.0.0.1:<port>), so there
8
8
  // is exactly one warm store (the service's) and this process never opens the DB.
9
9
  // JSON-RPC 2.0 over stdio, newline-delimited — implemented directly (zero deps).
10
+ //
11
+ // The gateway is the SUPERSET (docs/bridge-gateway-unification.md, U2): this one MCP
12
+ // server exposes both the gateway's warm history (chats/meetings/notes, redacted) AND the
13
+ // bridge's on-disk skills — so a CLI adds ONE server and gets everything ChatPanel local
14
+ // can do. History tools proxy to the gateway; skill tools proxy to the bridge. The bridge
15
+ // is optional: if it is not running, the skill tools say so instead of failing the connect.
10
16
 
11
17
  import { loadConfig } from './config.js';
18
+ import { readBridgeToken } from './bridge.js';
12
19
 
13
20
  const PROTOCOL_VERSION = '2024-11-05';
14
21
  const SERVER = { name: 'chatpanel-history', version: '1.0.0' };
@@ -25,6 +32,32 @@ function baseUrl() {
25
32
  return `http://127.0.0.1:${port}`;
26
33
  }
27
34
 
35
+ // The bridge the gateway fronts. Its skills live on disk, so the skill tools proxy here
36
+ // rather than through the gateway's history API.
37
+ function bridgeBase() {
38
+ try {
39
+ return String(loadConfig().bridge?.url || 'http://127.0.0.1:4319').replace(/\/+$/, '');
40
+ } catch {
41
+ return 'http://127.0.0.1:4319';
42
+ }
43
+ }
44
+ function bridgeToken() {
45
+ try {
46
+ return readBridgeToken(loadConfig().bridge?.token || '');
47
+ } catch {
48
+ return readBridgeToken('');
49
+ }
50
+ }
51
+ async function bridgeJson(path) {
52
+ const token = bridgeToken();
53
+ const res = await fetch(bridgeBase() + path, {
54
+ headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
55
+ });
56
+ const data = await res.json().catch(() => ({}));
57
+ if (!res.ok || data.ok === false) throw new Error(data?.error || `bridge ${res.status}`);
58
+ return data;
59
+ }
60
+
28
61
  const TOOLS = [
29
62
  {
30
63
  name: 'search_history',
@@ -58,6 +91,32 @@ const TOOLS = [
58
91
  },
59
92
  },
60
93
  },
94
+ {
95
+ name: 'list_skills',
96
+ description: 'List the reusable skills installed on this machine (via the ChatPanel bridge) — across every agent harness (Claude Code, Codex, Copilot, Gemini, Hermes) and any configured folder. Returns each skill\'s name and one-line description. Call open_skill to load the one that fits the task.',
97
+ inputSchema: { type: 'object', properties: {} },
98
+ },
99
+ {
100
+ name: 'open_skill',
101
+ description: 'Load one skill\'s full instructions by name (from list_skills), then follow them. If the instructions point at reference files, read one with read_skill_file.',
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: { name: { type: 'string', description: 'The skill name from list_skills.' } },
105
+ required: ['name'],
106
+ },
107
+ },
108
+ {
109
+ name: 'read_skill_file',
110
+ description: 'Read one reference file a skill\'s instructions point at (any path inside the skill\'s own folder). Use only when the task needs it.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ name: { type: 'string', description: 'The skill name.' },
115
+ path: { type: 'string', description: 'The reference path as written in the instructions, e.g. references/auth.md.' },
116
+ },
117
+ required: ['name', 'path'],
118
+ },
119
+ },
61
120
  ];
62
121
 
63
122
  async function gatewayJson(path, init) {
@@ -91,6 +150,28 @@ async function callTool(name, args = {}) {
91
150
  if (!items.length) return 'History is empty (or the gateway has not been seeded yet).';
92
151
  return [`${items.length} of ${data.total} records:`, ...items.map((it) => `[${it.id}] ${it.title || '(untitled)'} · ${it.type}${it.date ? ' · ' + new Date(it.date).toISOString().slice(0, 10) : ''} · ${it.chars} chars`)].join('\n');
93
152
  }
153
+ if (name === 'list_skills') {
154
+ let data;
155
+ 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.`; }
157
+ const rows = data.skills || [];
158
+ if (!rows.length) return 'No skills installed on this machine yet.';
159
+ 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.';
160
+ }
161
+ if (name === 'open_skill') {
162
+ let data;
163
+ try { data = await bridgeJson(`/skills/${encodeURIComponent(String(args.name || '').trim())}`); }
164
+ catch (e) { return `Could not open "${args.name}": ${e.message}`; }
165
+ return data.skill?.prompt || '(this skill has no extra instructions — just apply it.)';
166
+ }
167
+ if (name === 'read_skill_file') {
168
+ const skill = encodeURIComponent(String(args.name || '').trim());
169
+ const path = String(args.path || '').trim().split('/').map(encodeURIComponent).join('/');
170
+ let data;
171
+ try { data = await bridgeJson(`/skills/${skill}/file/${path}`); }
172
+ catch (e) { return `Could not read ${args.path}: ${e.message}`; }
173
+ return data.text || '(empty)';
174
+ }
94
175
  throw new Error(`unknown tool: ${name}`);
95
176
  }
96
177
 
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.33';
48
+ export const VERSION = '0.6.35';
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