@chatpanel/bridge 0.11.5 → 0.11.7
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/package.json +1 -1
- package/src/channels/adapters/telegram.js +48 -4
- package/src/channels/gateway.js +13 -5
- package/src/channels/service.js +29 -10
- package/src/server.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine \u2014 Claude Code (CLI), Codex (CLI), and Antigravity CLI (formerly Gemini CLI, which remains available for business/enterprise) \u2014 to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
6
|
"keywords": [
|
|
@@ -38,6 +38,29 @@ const HELP = [
|
|
|
38
38
|
* it needs is injected — a bot token, the bridge address+token, a pairing store, an event sink
|
|
39
39
|
* — so nothing here reads a secret or reaches for global state.
|
|
40
40
|
*/
|
|
41
|
+
// What a turn driven from a phone can actually DO, told to the agent up front.
|
|
42
|
+
//
|
|
43
|
+
// Without this the agent plans as if it were sitting at a terminal: asked to create a note it
|
|
44
|
+
// announced it would look for a notes folder, discovered writes were not enabled, said "let me
|
|
45
|
+
// check what notes tools are available" — and stopped mid-thought, having done nothing and
|
|
46
|
+
// explained nothing. The ceiling is real and enforced elsewhere (reach caps the toolset); the
|
|
47
|
+
// agent simply had no way to know about it, so it spent a turn discovering it and left the
|
|
48
|
+
// user staring at half a sentence.
|
|
49
|
+
export function channelSystem(userSystem, reach) {
|
|
50
|
+
const limits = !reach || reach === 'any' ? '' : [
|
|
51
|
+
"You are answering a message sent from the user's PHONE, relayed to this machine.",
|
|
52
|
+
reach === 'device'
|
|
53
|
+
? 'You cannot read files, run commands, browse the web, or change anything.'
|
|
54
|
+
: "You can READ files and search the user's ChatPanel history. You cannot write or edit "
|
|
55
|
+
+ 'files, run shell commands, or browse the web — those are switched off for messages '
|
|
56
|
+
+ 'from a phone, and no tool will grant them.',
|
|
57
|
+
'If a request needs something you cannot do, say so plainly in your FIRST reply and offer '
|
|
58
|
+
+ 'what you can do instead. Never begin work you cannot finish.',
|
|
59
|
+
'Answers are read on a phone: be brief, and put the answer first.',
|
|
60
|
+
].join(' ');
|
|
61
|
+
return [limits, userSystem].filter(Boolean).join('\n\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
export function startTelegram({
|
|
42
65
|
botToken,
|
|
43
66
|
baseUrl,
|
|
@@ -52,7 +75,17 @@ export function startTelegram({
|
|
|
52
75
|
// which it has: both expose chat()/cancel() and fold into one reply state.
|
|
53
76
|
transport = bridge,
|
|
54
77
|
model = '',
|
|
78
|
+
provider = '',
|
|
55
79
|
system = '',
|
|
80
|
+
// Read at the START OF EACH MESSAGE rather than captured when the loop starts.
|
|
81
|
+
//
|
|
82
|
+
// `chats` below holds every conversation's history AND its privacy vault, and it lives only
|
|
83
|
+
// as long as this loop. Settings used to be baked in at start, so changing the model — or
|
|
84
|
+
// the privacy mode, or anything else — had to stop and respawn the loop, which silently threw
|
|
85
|
+
// all of that away: the next message arrived as "a fresh session with no prior conversation",
|
|
86
|
+
// and PERSON_1 stopped meaning the same person. Switching which model answers is not a
|
|
87
|
+
// reason to forget what you were talking about.
|
|
88
|
+
route = null,
|
|
56
89
|
redact = { tier: 'basic' },
|
|
57
90
|
privacy = 'standard',
|
|
58
91
|
logger = console,
|
|
@@ -119,7 +152,7 @@ export function startTelegram({
|
|
|
119
152
|
}
|
|
120
153
|
if (name === 'stop') {
|
|
121
154
|
const st = chatState(norm.chatId);
|
|
122
|
-
const ok = await transport.cancel(st.runId, { baseUrl, token });
|
|
155
|
+
const ok = await ((route && route().transport) || transport).cancel(st.runId, { baseUrl, token });
|
|
123
156
|
st.runId = null;
|
|
124
157
|
return void send(norm.chatId, ok ? '⏹ stopped.' : 'nothing running.');
|
|
125
158
|
}
|
|
@@ -156,11 +189,22 @@ export function startTelegram({
|
|
|
156
189
|
// on the bridge renders all-but-last as history and the last as "answer this now".
|
|
157
190
|
const messages = [...st.history, { role: 'user', content: redacted }];
|
|
158
191
|
|
|
192
|
+
// Whatever the settings say RIGHT NOW — see `route` above.
|
|
193
|
+
const live = (route && route()) || {};
|
|
194
|
+
const useTransport = live.transport || transport;
|
|
159
195
|
try {
|
|
160
|
-
const finalState = await
|
|
161
|
-
{
|
|
196
|
+
const finalState = await useTransport.chat(
|
|
197
|
+
{
|
|
198
|
+
agent: live.agent ?? agent,
|
|
199
|
+
model: live.model ?? model,
|
|
200
|
+
provider: live.provider ?? provider,
|
|
201
|
+
system: channelSystem(live.system ?? system, reach),
|
|
202
|
+
messages,
|
|
203
|
+
images,
|
|
204
|
+
options: { reach },
|
|
205
|
+
},
|
|
162
206
|
{
|
|
163
|
-
baseUrl, token, signal,
|
|
207
|
+
baseUrl: live.baseUrl || baseUrl, token, signal,
|
|
164
208
|
onEvent: (ev, state) => {
|
|
165
209
|
if (state.runId) st.runId = state.runId;
|
|
166
210
|
// Driven by the folded STATE, not by an event's `type`: the bridge emits
|
package/src/channels/gateway.js
CHANGED
|
@@ -40,7 +40,7 @@ const inflight = new Map();
|
|
|
40
40
|
* not know which transport it has: streams events to onEvent(ev, state) and returns the folded
|
|
41
41
|
* final state.
|
|
42
42
|
*/
|
|
43
|
-
export async function chat({ model = '', system = '', messages = [], options = {} }, {
|
|
43
|
+
export async function chat({ model = '', provider = '', system = '', messages = [], options = {} }, {
|
|
44
44
|
baseUrl, signal, onEvent = () => {},
|
|
45
45
|
} = {}) {
|
|
46
46
|
const runId = `gw_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -57,7 +57,18 @@ export async function chat({ model = '', system = '', messages = [], options = {
|
|
|
57
57
|
try {
|
|
58
58
|
const res = await fetch(`${String(baseUrl).replace(/\/$/, '')}/v1/chat/completions`, {
|
|
59
59
|
method: 'POST',
|
|
60
|
-
headers: {
|
|
60
|
+
headers: {
|
|
61
|
+
'content-type': 'application/json',
|
|
62
|
+
// ChatPanel's routing metadata goes in HEADERS, never in the request body. It rode the
|
|
63
|
+
// body first and NVIDIA answered "unsupported parameters": OpenAI-compatible providers
|
|
64
|
+
// validate the body strictly and reject unknown fields, while ignoring unknown headers.
|
|
65
|
+
// The gateway strips x-chatpanel-* before forwarding, so the provider never sees it.
|
|
66
|
+
...(options?.reach ? { 'x-chatpanel-reach': options.reach } : {}),
|
|
67
|
+
// WHICH provider, not just which model. A model id is not a unique key — two providers
|
|
68
|
+
// can serve the same one — so without this the gateway picks whichever destination it
|
|
69
|
+
// lists first and the call goes out on a key the user never chose.
|
|
70
|
+
...(provider ? { 'x-chatpanel-destination': provider } : {}),
|
|
71
|
+
},
|
|
61
72
|
body: JSON.stringify({
|
|
62
73
|
model,
|
|
63
74
|
stream: true,
|
|
@@ -65,9 +76,6 @@ export async function chat({ model = '', system = '', messages = [], options = {
|
|
|
65
76
|
...(system ? [{ role: 'system', content: system }] : []),
|
|
66
77
|
...messages.map((m) => ({ role: m.role, content: String(m.content ?? '') })),
|
|
67
78
|
],
|
|
68
|
-
// Carried through so a capped phone's reach still reaches the router. The gateway
|
|
69
|
-
// ignores what it does not know, which is what keeps an older gateway working.
|
|
70
|
-
...(options?.reach ? { chatpanel: { reach: options.reach } } : {}),
|
|
71
79
|
}),
|
|
72
80
|
signal: ac.signal,
|
|
73
81
|
});
|
package/src/channels/service.js
CHANGED
|
@@ -41,7 +41,8 @@ const TELEGRAM_API = 'https://api.telegram.org';
|
|
|
41
41
|
// its own bridge backend, the same agents. They are mutually exclusive: update() clears one
|
|
42
42
|
// when the other is set, because "which thing answers" is one choice, not two.
|
|
43
43
|
export const DEFAULT_SETTINGS = Object.freeze({
|
|
44
|
-
agent: 'claude', model: '',
|
|
44
|
+
agent: 'claude', model: '', provider: '', gatewayUrl: DEFAULT_GATEWAY_URL,
|
|
45
|
+
privacy: 'standard', tier: 'basic',
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
// Restart backoff. A long-poll that dies (network drop, laptop asleep, Telegram hiccup) must
|
|
@@ -131,13 +132,28 @@ export function createChannelService({
|
|
|
131
132
|
function spawnLoop(botToken) {
|
|
132
133
|
controller = new AbortController();
|
|
133
134
|
running = true;
|
|
134
|
-
//
|
|
135
|
-
|
|
135
|
+
// Resolved PER MESSAGE, not once at start. The loop owns every conversation's history and
|
|
136
|
+
// privacy vault, so restarting it to pick up a settings change threw away the thing the
|
|
137
|
+
// user came for. Now nothing needs a restart: the next message simply reads this.
|
|
138
|
+
const route = () => {
|
|
139
|
+
const viaGateway = !!settings.model;
|
|
140
|
+
return {
|
|
141
|
+
transport: viaGateway ? gateway : bridgeTransport,
|
|
142
|
+
agent: viaGateway ? '' : settings.agent,
|
|
143
|
+
model: viaGateway ? settings.model : '',
|
|
144
|
+
provider: viaGateway ? (settings.provider || '') : '',
|
|
145
|
+
baseUrl: viaGateway ? (settings.gatewayUrl || DEFAULT_GATEWAY_URL) : bridge.baseUrl,
|
|
146
|
+
system: settings.system || '',
|
|
147
|
+
privacy: settings.privacy,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
136
150
|
const done = startAdapter({
|
|
137
151
|
botToken,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
route,
|
|
153
|
+
transport: route().transport,
|
|
154
|
+
model: route().model,
|
|
155
|
+
provider: route().provider,
|
|
156
|
+
baseUrl: route().baseUrl,
|
|
141
157
|
token: bridge.token,
|
|
142
158
|
pairing,
|
|
143
159
|
savePairing,
|
|
@@ -247,17 +263,19 @@ export function createChannelService({
|
|
|
247
263
|
async update(patch = {}) {
|
|
248
264
|
await load();
|
|
249
265
|
const next = { ...settings };
|
|
250
|
-
for (const k of ['agent', 'model', 'gatewayUrl', 'privacy', 'tier', 'system']) {
|
|
266
|
+
for (const k of ['agent', 'model', 'provider', 'gatewayUrl', 'privacy', 'tier', 'system']) {
|
|
251
267
|
if (patch[k] != null) next[k] = patch[k];
|
|
252
268
|
}
|
|
253
269
|
// Picking one target unpicks the other. Without this a stale `model` would silently win
|
|
254
270
|
// over the agent the user just chose, and the screen would disagree with the machine.
|
|
255
|
-
if (patch.agent != null && patch.model == null) next.model = '';
|
|
271
|
+
if (patch.agent != null && patch.model == null) { next.model = ''; next.provider = ''; }
|
|
256
272
|
if (patch.model) next.agent = '';
|
|
257
273
|
settings = next;
|
|
258
274
|
await saveSettings();
|
|
259
|
-
//
|
|
260
|
-
|
|
275
|
+
// NO RESTART. The loop reads settings per message through `route`, and restarting it
|
|
276
|
+
// would discard every conversation's history and vault — which is exactly what made
|
|
277
|
+
// changing the model answer the next question with "this looks like a fresh session".
|
|
278
|
+
// The only change that still needs a restart is a new bot token, and connect() does that.
|
|
261
279
|
return { settings: { ...settings } };
|
|
262
280
|
},
|
|
263
281
|
|
|
@@ -297,6 +315,7 @@ export function createChannelService({
|
|
|
297
315
|
settings: {
|
|
298
316
|
agent: settings.agent,
|
|
299
317
|
model: settings.model || '',
|
|
318
|
+
provider: settings.provider || '',
|
|
300
319
|
gatewayUrl: settings.gatewayUrl || DEFAULT_GATEWAY_URL,
|
|
301
320
|
privacy: settings.privacy,
|
|
302
321
|
tier: settings.tier,
|
package/src/server.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
// Hardcoded (not read from package.json) so it survives Bun's single-file
|
|
68
68
|
// --compile, where package.json isn't on a readable FS. CI fails the publish if
|
|
69
69
|
// this drifts from package.json, so the two can't silently diverge.
|
|
70
|
-
const VERSION = '0.11.
|
|
70
|
+
const VERSION = '0.11.7';
|
|
71
71
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
72
72
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
73
73
|
|