@chatpanel/bridge 0.10.26 → 0.10.28

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
@@ -87,6 +87,11 @@ npm start # → http://127.0.0.1:4319
87
87
  |--------|------|---------|
88
88
  | `GET` | `/health` | `{ ok, version, agents:[{id,label,available,reason}] }` |
89
89
  | `POST` | `/chat` | SSE stream — body `{ agent, system, options, messages }` |
90
+ | `GET` | `/v1/models` | OpenAI-compatible list of available local agents |
91
+ | `POST` | `/v1/chat/completions` | OpenAI-compatible Chat Completions (streaming and non-streaming) |
92
+ | `POST` | `/v1/completions` | OpenAI-compatible legacy text Completions (streaming and non-streaming) |
93
+ | `POST` | `/v1/responses` | OpenAI-compatible Responses API (streaming and non-streaming) |
94
+ | `POST` | `/v1/messages` | Anthropic-compatible Messages API (streaming and non-streaming) |
90
95
 
91
96
  `/chat` streams Server-Sent Events: `{type:'delta',text}` as the answer is
92
97
  generated, `{type:'tool',name,summary}` / `{type:'status'}` for activity, and a
@@ -102,6 +107,57 @@ final `{type:'done'}` (or `{type:'error',error}`).
102
107
  }
103
108
  ```
104
109
 
110
+ ### OpenAI and Anthropic SDK compatibility
111
+
112
+ The compatibility routes run a local coding agent; they do not call a provider
113
+ API. Use the per-install token from `~/.chatpanel/bridge-token` as the SDK API
114
+ key. The token protects endpoints that can start local processes, so do not put
115
+ it in browser-delivered code or share it.
116
+
117
+ For OpenAI clients, set the base URL to `http://127.0.0.1:4319/v1` and use an
118
+ installed agent id such as `codex` as the model:
119
+
120
+ ```js
121
+ import OpenAI from 'openai';
122
+ import { readFileSync } from 'node:fs';
123
+
124
+ const client = new OpenAI({
125
+ apiKey: readFileSync(`${process.env.HOME}/.chatpanel/bridge-token`, 'utf8').trim(),
126
+ baseURL: 'http://127.0.0.1:4319/v1',
127
+ });
128
+
129
+ const result = await client.responses.create({
130
+ model: 'codex',
131
+ input: 'Explain this project in three bullets.',
132
+ });
133
+ console.log(result.output_text);
134
+ ```
135
+
136
+ `client.chat.completions.create(...)` and legacy `client.completions.create(...)`
137
+ work through the same base URL. For the Anthropic SDK, use
138
+ `http://127.0.0.1:4319` as its base URL and the same token as its API key;
139
+ requests to `/v1/messages` can also use `model: "codex"`.
140
+
141
+ The model field selects the local agent. `codex/gpt-5.5`, for example, selects
142
+ the Codex agent and passes `gpt-5.5` as its CLI model override. Advanced bridge
143
+ settings can be supplied through an extra `chatpanel` request object:
144
+
145
+ ```jsonc
146
+ {
147
+ "model": "codex",
148
+ "messages": [{ "role": "user", "content": "Fix the failing tests." }],
149
+ "chatpanel": {
150
+ "working_dir": "/absolute/path/to/project",
151
+ "permission_mode": "acceptEdits",
152
+ "use_local_config": true
153
+ }
154
+ }
155
+ ```
156
+
157
+ The adapter currently covers text conversations. Provider-hosted function tools,
158
+ stored responses, log probabilities, and exact token accounting are not
159
+ available; unsupported tool requests return HTTP 400, and usage counts are zero.
160
+
105
161
  ## Safety
106
162
 
107
163
  - Binds to `127.0.0.1` only; CORS accepts the extension origin and localhost.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/bridge",
3
- "version": "0.10.26",
3
+ "version": "0.10.28",
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": [
@@ -36,7 +36,9 @@
36
36
  "start": "node src/server.js",
37
37
  "dev": "node --watch src/server.js",
38
38
  "test": "node --test tests/*.test.mjs",
39
- "build:bin": "bash scripts/build-binaries.sh"
39
+ "build:bin": "bash scripts/build-binaries.sh",
40
+ "sync:events": "node scripts/sync-events.mjs",
41
+ "test:events-sync": "node scripts/sync-events.mjs --check"
40
42
  },
41
43
  "dependencies": {},
42
44
  "optionalDependencies": {
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // Vendors the shared contracts the bridge needs from `chatpanel-events`.
3
+ //
4
+ // The bridge has ZERO runtime dependencies on purpose — it is installed by a curl
5
+ // one-liner and compiled into a single binary, and every dependency is a thing that can
6
+ // fail at install time on someone's laptop. So shared contracts arrive the way
7
+ // @chatpanel/pii does: copied in, generated, never hand-edited.
8
+ //
9
+ // node scripts/sync-events.mjs refresh src/events/ from the package
10
+ // node scripts/sync-events.mjs --check verify they match (CI drift guard); exit 1 if not
11
+ //
12
+ // A hand-copy is how `sanitize.js` could silently diverge from the engine it came from.
13
+ // This makes divergence a failing test instead of a bug report.
14
+
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { dirname, join } from 'node:path';
18
+
19
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
20
+
21
+ // Deliberately short. `skill-manifest.js` imports only `scopes.js`, which is why that
22
+ // vocabulary was split out of `capability.js` — vendoring the capability machinery and
23
+ // the event schema to reach a five-element array would defeat the point.
24
+ const FILES = ['scopes.js', 'skill-manifest.js'];
25
+
26
+ function pkgDir() {
27
+ return [
28
+ join(ROOT, 'node_modules', '@chatpanel', 'events'),
29
+ join(ROOT, '..', 'chatpanel-events'),
30
+ ].find((d) => existsSync(join(d, 'skill-manifest.js')));
31
+ }
32
+
33
+ const check = process.argv.includes('--check');
34
+ const src = pkgDir();
35
+
36
+ if (!src) {
37
+ const msg = 'chatpanel-events not found (check out ../chatpanel-events).';
38
+ if (check) { console.error(`sync-events --check: ${msg}`); process.exit(1); }
39
+ console.warn(`sync-events: ${msg} Leaving src/events as-is.`);
40
+ process.exit(0);
41
+ }
42
+
43
+ const outDir = join(ROOT, 'src', 'events');
44
+ if (!check) mkdirSync(outDir, { recursive: true });
45
+
46
+ const banner = (f) => `// GENERATED — do not edit.\n`
47
+ + `// Source of truth: chatpanel-events/${f} (npm @chatpanel/events).\n`
48
+ + `// Edit there, then run: npm run sync:events\n`
49
+ + `//\n`
50
+ + `// Vendored rather than depended on: the bridge ships zero runtime dependencies so a\n`
51
+ + `// curl one-liner install cannot fail on someone's registry, and so the compiled\n`
52
+ + `// single-file binary has nothing to resolve.\n\n`;
53
+
54
+ let drift = 0;
55
+ for (const f of FILES) {
56
+ const want = banner(f) + readFileSync(join(src, f), 'utf8');
57
+ const dest = join(outDir, f);
58
+ const have = existsSync(dest) ? readFileSync(dest, 'utf8') : null;
59
+ if (have === want) continue;
60
+ if (check) { console.error(`sync-events --check: src/events/${f} differs from chatpanel-events`); drift += 1; continue; }
61
+ writeFileSync(dest, want);
62
+ console.log(`sync-events: updated src/events/${f}`);
63
+ }
64
+
65
+ if (check) {
66
+ if (drift) { console.error('Run `npm run sync:events` and commit the result.'); process.exit(1); }
67
+ console.log('sync-events --check: src/events matches chatpanel-events ✓');
68
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ bridge_url="${CHATPANEL_BRIDGE_URL:-http://127.0.0.1:4319}"
5
+ token_file="${CHATPANEL_BRIDGE_TOKEN_FILE:-${HOME}/.chatpanel/bridge-token}"
6
+ prompt="${*:-Reply with exactly: httpie-codex-ok}"
7
+
8
+ if ! command -v http >/dev/null 2>&1; then
9
+ echo 'HTTPie is required. Install it with: brew install httpie' >&2
10
+ exit 1
11
+ fi
12
+
13
+ if [[ ! -r "$token_file" ]]; then
14
+ echo "Bridge token is not readable: $token_file" >&2
15
+ exit 1
16
+ fi
17
+
18
+ bridge_token="$(tr -d '\r\n' < "$token_file")"
19
+ if [[ -z "$bridge_token" ]]; then
20
+ echo "Bridge token is empty: $token_file" >&2
21
+ exit 1
22
+ fi
23
+
24
+ response="$({
25
+ http --check-status --ignore-stdin --body POST "$bridge_url/v1/responses" \
26
+ Authorization:"Bearer $bridge_token" \
27
+ model=codex \
28
+ input="$prompt"
29
+ })"
30
+
31
+ if command -v jq >/dev/null 2>&1; then
32
+ jq -r '.output[]?.content[]? | select(.type == "output_text") | .text' <<< "$response"
33
+ else
34
+ printf '%s\n' "$response"
35
+ fi
@@ -0,0 +1,292 @@
1
+ // OpenAI + Anthropic wire-format adapters for ChatPanel's local agent engines.
2
+ //
3
+ // This intentionally implements the text conversation subset shared by coding
4
+ // agents. Provider-hosted features (token accounting, stored responses, remote
5
+ // function tools, logprobs, etc.) cannot be reproduced by a local CLI runner and
6
+ // are rejected when accepting them would produce misleading behavior.
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+
10
+ const AGENT_IDS = new Set(['claude', 'codex', 'antigravity', 'pi', 'opencode', 'kiro', 'copilot', 'deepseek']);
11
+
12
+ export class CompatError extends Error {
13
+ constructor(message, status = 400, param = null) {
14
+ super(message);
15
+ this.status = status;
16
+ this.param = param;
17
+ }
18
+ }
19
+
20
+ function textOf(content) {
21
+ if (content == null) return '';
22
+ if (typeof content === 'string') return content;
23
+ if (!Array.isArray(content)) return String(content);
24
+ return content.map((part) => {
25
+ if (typeof part === 'string') return part;
26
+ if (!part || typeof part !== 'object') return '';
27
+ if (typeof part.text === 'string') return part.text;
28
+ if (typeof part.content === 'string') return part.content;
29
+ if (part.type === 'tool_result') return textOf(part.content);
30
+ return '';
31
+ }).filter(Boolean).join('\n');
32
+ }
33
+
34
+ function assertTextContent(content, param) {
35
+ if (content == null || typeof content === 'string') return;
36
+ if (!Array.isArray(content)) throw new CompatError(`${param} must contain text`, 400, param);
37
+ for (const part of content) {
38
+ if (typeof part === 'string') continue;
39
+ const type = part?.type;
40
+ if (part && typeof part === 'object' && (!type || ['text', 'input_text', 'output_text'].includes(type)) && typeof part.text === 'string') continue;
41
+ if (type === 'tool_result') {
42
+ assertTextContent(part.content, param);
43
+ continue;
44
+ }
45
+ throw new CompatError(`${param} contains unsupported non-text content`, 400, param);
46
+ }
47
+ }
48
+
49
+ function normalizeMessages(items = []) {
50
+ return items.map((item) => ({
51
+ role: item?.role === 'assistant' ? 'assistant' : 'user',
52
+ content: textOf(item?.content),
53
+ })).filter((item) => item.content);
54
+ }
55
+
56
+ function resolveTarget(model, chatpanel = {}) {
57
+ const requested = String(model || 'codex').trim() || 'codex';
58
+ const slash = requested.indexOf('/');
59
+ const prefix = slash < 0 ? requested : requested.slice(0, slash);
60
+ const explicitAgent = String(chatpanel.agent || '').trim();
61
+ const agent = explicitAgent || (AGENT_IDS.has(prefix) ? prefix : 'codex');
62
+ if (!AGENT_IDS.has(agent)) throw new CompatError(`Unknown ChatPanel agent "${agent}"`, 400, 'model');
63
+
64
+ let engineModel = String(chatpanel.model || '').trim();
65
+ if (!engineModel && slash >= 0 && AGENT_IDS.has(prefix)) engineModel = requested.slice(slash + 1);
66
+ if (!engineModel && !AGENT_IDS.has(requested)) engineModel = requested;
67
+
68
+ const permissionMode = chatpanel.permission_mode || chatpanel.permissionMode || 'default';
69
+ if (!['default', 'acceptEdits', 'bypassPermissions'].includes(permissionMode)) {
70
+ throw new CompatError('chatpanel.permission_mode must be default, acceptEdits, or bypassPermissions', 400, 'chatpanel.permission_mode');
71
+ }
72
+ return {
73
+ agent,
74
+ requestedModel: requested,
75
+ options: {
76
+ workingDir: chatpanel.working_dir || chatpanel.workingDir || process.env.CHATPANEL_API_WORKING_DIR || '',
77
+ permissionMode,
78
+ useLocalConfig: chatpanel.use_local_config ?? chatpanel.useLocalConfig ?? true,
79
+ ...(engineModel ? { model: engineModel } : {}),
80
+ },
81
+ };
82
+ }
83
+
84
+ export function parseChatCompletion(body = {}) {
85
+ if (!Array.isArray(body.messages) || !body.messages.length) {
86
+ throw new CompatError('messages must be a non-empty array', 400, 'messages');
87
+ }
88
+ if (body.tools?.length) throw new CompatError('OpenAI tool calling is not supported by the bridge adapter', 400, 'tools');
89
+ for (const message of body.messages) {
90
+ if (!['system', 'developer', 'user', 'assistant'].includes(message?.role)) {
91
+ throw new CompatError(`Unsupported message role "${message?.role || ''}"`, 400, 'messages');
92
+ }
93
+ assertTextContent(message.content, 'messages');
94
+ }
95
+ const system = body.messages
96
+ .filter((m) => m?.role === 'system' || m?.role === 'developer')
97
+ .map((m) => textOf(m.content)).filter(Boolean).join('\n\n');
98
+ const messages = normalizeMessages(body.messages.filter((m) => m?.role !== 'system' && m?.role !== 'developer'));
99
+ if (!messages.length) throw new CompatError('messages must contain user or assistant text', 400, 'messages');
100
+ return { ...resolveTarget(body.model, body.chatpanel), system, messages, stream: body.stream === true };
101
+ }
102
+
103
+ export function parseCompletion(body = {}) {
104
+ if (Array.isArray(body.prompt)) {
105
+ for (const prompt of body.prompt) assertTextContent(prompt, 'prompt');
106
+ } else {
107
+ assertTextContent(body.prompt, 'prompt');
108
+ }
109
+ const prompt = Array.isArray(body.prompt) ? body.prompt.map(textOf).join('\n') : textOf(body.prompt);
110
+ if (!prompt) throw new CompatError('prompt must contain text', 400, 'prompt');
111
+ return {
112
+ ...resolveTarget(body.model, body.chatpanel),
113
+ system: '',
114
+ messages: [{ role: 'user', content: prompt }],
115
+ stream: body.stream === true,
116
+ };
117
+ }
118
+
119
+ export function parseResponse(body = {}) {
120
+ if (body.tools?.length) throw new CompatError('Responses API tool calling is not supported by the bridge adapter', 400, 'tools');
121
+ let input = body.input;
122
+ if (typeof input === 'string') input = [{ role: 'user', content: input }];
123
+ if (!Array.isArray(input) || !input.length) throw new CompatError('input must be a string or non-empty array', 400, 'input');
124
+ assertTextContent(body.instructions, 'instructions');
125
+ for (const item of input) {
126
+ if (!['system', 'developer', 'user', 'assistant'].includes(item?.role)) {
127
+ throw new CompatError('input currently supports only text message items', 400, 'input');
128
+ }
129
+ assertTextContent(item.content, 'input');
130
+ }
131
+ const systemParts = [textOf(body.instructions)];
132
+ for (const item of input) {
133
+ if (item?.role === 'system' || item?.role === 'developer') systemParts.push(textOf(item.content));
134
+ }
135
+ const messages = normalizeMessages(input.filter((item) => item?.role !== 'system' && item?.role !== 'developer'));
136
+ if (!messages.length) throw new CompatError('input must contain user or assistant text', 400, 'input');
137
+ return {
138
+ ...resolveTarget(body.model, body.chatpanel),
139
+ system: systemParts.filter(Boolean).join('\n\n'),
140
+ messages,
141
+ stream: body.stream === true,
142
+ };
143
+ }
144
+
145
+ export function parseAnthropicMessage(body = {}) {
146
+ if (!Array.isArray(body.messages) || !body.messages.length) {
147
+ throw new CompatError('messages must be a non-empty array', 400, 'messages');
148
+ }
149
+ if (body.tools?.length) throw new CompatError('Anthropic tool use is not supported by the bridge adapter', 400, 'tools');
150
+ assertTextContent(body.system, 'system');
151
+ for (const message of body.messages) {
152
+ if (!['user', 'assistant'].includes(message?.role)) {
153
+ throw new CompatError(`Unsupported message role "${message?.role || ''}"`, 400, 'messages');
154
+ }
155
+ assertTextContent(message.content, 'messages');
156
+ }
157
+ const messages = normalizeMessages(body.messages);
158
+ if (!messages.length) throw new CompatError('messages must contain text', 400, 'messages');
159
+ return {
160
+ ...resolveTarget(body.model, body.chatpanel),
161
+ system: textOf(body.system),
162
+ messages,
163
+ stream: body.stream === true,
164
+ };
165
+ }
166
+
167
+ export function openAIError(error) {
168
+ return {
169
+ error: {
170
+ message: error?.message || String(error),
171
+ type: error instanceof CompatError ? 'invalid_request_error' : 'api_error',
172
+ param: error?.param || null,
173
+ code: null,
174
+ },
175
+ };
176
+ }
177
+
178
+ export function anthropicError(error) {
179
+ return {
180
+ type: 'error',
181
+ error: {
182
+ type: error instanceof CompatError ? 'invalid_request_error' : 'api_error',
183
+ message: error?.message || String(error),
184
+ },
185
+ };
186
+ }
187
+
188
+ const zeroUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
189
+ const responseUsage = () => ({ input_tokens: 0, input_tokens_details: { cached_tokens: 0 }, output_tokens: 0, output_tokens_details: { reasoning_tokens: 0 }, total_tokens: 0 });
190
+
191
+ export function createChatCompletion(model, text, { id = `chatcmpl-${randomUUID()}`, created = Math.floor(Date.now() / 1000) } = {}) {
192
+ return {
193
+ id, object: 'chat.completion', created, model,
194
+ choices: [{ index: 0, message: { role: 'assistant', content: text, refusal: null }, logprobs: null, finish_reason: 'stop' }],
195
+ usage: zeroUsage(),
196
+ };
197
+ }
198
+
199
+ export function createCompletion(model, text, { id = `cmpl-${randomUUID()}`, created = Math.floor(Date.now() / 1000) } = {}) {
200
+ return {
201
+ id, object: 'text_completion', created, model,
202
+ choices: [{ text, index: 0, logprobs: null, finish_reason: 'stop' }],
203
+ usage: zeroUsage(),
204
+ };
205
+ }
206
+
207
+ export function completionStream(model, onWrite, { id = `cmpl-${randomUUID()}`, created = Math.floor(Date.now() / 1000) } = {}) {
208
+ const chunk = (text, finish_reason = null) => ({
209
+ id, object: 'text_completion', created, model,
210
+ choices: [{ text, index: 0, logprobs: null, finish_reason }],
211
+ });
212
+ return {
213
+ delta(text) { if (text) onWrite(chunk(text)); },
214
+ done() { onWrite(chunk('', 'stop')); },
215
+ };
216
+ }
217
+
218
+ export function chatCompletionStream(model, onWrite, { id = `chatcmpl-${randomUUID()}`, created = Math.floor(Date.now() / 1000) } = {}) {
219
+ const chunk = (delta, finish_reason = null) => ({
220
+ id, object: 'chat.completion.chunk', created, model,
221
+ choices: [{ index: 0, delta, logprobs: null, finish_reason }],
222
+ });
223
+ let started = false;
224
+ return {
225
+ delta(text) {
226
+ if (!started) { onWrite(chunk({ role: 'assistant', content: '' })); started = true; }
227
+ if (text) onWrite(chunk({ content: text }));
228
+ },
229
+ done() {
230
+ if (!started) onWrite(chunk({ role: 'assistant', content: '' }));
231
+ onWrite(chunk({}, 'stop'));
232
+ },
233
+ };
234
+ }
235
+
236
+ function responseObject(model, text, { id, created, status = 'completed' }) {
237
+ const itemId = `msg_${id.slice(-24)}`;
238
+ return {
239
+ id, object: 'response', created_at: created, status, error: null, incomplete_details: null,
240
+ instructions: null, max_output_tokens: null, model,
241
+ output: status === 'completed' ? [{ id: itemId, type: 'message', status: 'completed', role: 'assistant', content: [{ type: 'output_text', text, annotations: [], logprobs: [] }] }] : [],
242
+ parallel_tool_calls: false, previous_response_id: null, reasoning: { effort: null, summary: null }, store: false,
243
+ temperature: null, text: { format: { type: 'text' } }, tool_choice: 'none', tools: [], top_p: null,
244
+ truncation: 'disabled', usage: status === 'completed' ? responseUsage() : null, user: null, metadata: {},
245
+ };
246
+ }
247
+
248
+ export function createResponse(model, text, { id = `resp_${randomUUID()}`, created = Math.floor(Date.now() / 1000) } = {}) {
249
+ return responseObject(model, text, { id, created });
250
+ }
251
+
252
+ export function responseStream(model, onWrite, { id = `resp_${randomUUID()}`, created = Math.floor(Date.now() / 1000) } = {}) {
253
+ const itemId = `msg_${id.slice(-24)}`;
254
+ let sequence = 0;
255
+ let text = '';
256
+ onWrite({ type: 'response.created', sequence_number: sequence++, response: responseObject(model, '', { id, created, status: 'in_progress' }) });
257
+ onWrite({ type: 'response.in_progress', sequence_number: sequence++, response: responseObject(model, '', { id, created, status: 'in_progress' }) });
258
+ onWrite({ type: 'response.output_item.added', sequence_number: sequence++, output_index: 0, item: { id: itemId, type: 'message', status: 'in_progress', role: 'assistant', content: [] } });
259
+ onWrite({ type: 'response.content_part.added', sequence_number: sequence++, item_id: itemId, output_index: 0, content_index: 0, part: { type: 'output_text', text: '', annotations: [], logprobs: [] } });
260
+ return {
261
+ delta(value) {
262
+ if (!value) return;
263
+ text += value;
264
+ onWrite({ type: 'response.output_text.delta', sequence_number: sequence++, item_id: itemId, output_index: 0, content_index: 0, delta: value, logprobs: [] });
265
+ },
266
+ done() {
267
+ onWrite({ type: 'response.output_text.done', sequence_number: sequence++, item_id: itemId, output_index: 0, content_index: 0, text, logprobs: [] });
268
+ const part = { type: 'output_text', text, annotations: [], logprobs: [] };
269
+ onWrite({ type: 'response.content_part.done', sequence_number: sequence++, item_id: itemId, output_index: 0, content_index: 0, part });
270
+ const item = { id: itemId, type: 'message', status: 'completed', role: 'assistant', content: [part] };
271
+ onWrite({ type: 'response.output_item.done', sequence_number: sequence++, output_index: 0, item });
272
+ onWrite({ type: 'response.completed', sequence_number: sequence++, response: responseObject(model, text, { id, created }) });
273
+ },
274
+ };
275
+ }
276
+
277
+ export function createAnthropicMessage(model, text, { id = `msg_${randomUUID()}` } = {}) {
278
+ return { id, type: 'message', role: 'assistant', model, content: [{ type: 'text', text }], stop_reason: 'end_turn', stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } };
279
+ }
280
+
281
+ export function anthropicStream(model, onWrite, { id = `msg_${randomUUID()}` } = {}) {
282
+ onWrite('message_start', { type: 'message_start', message: { id, type: 'message', role: 'assistant', model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } });
283
+ onWrite('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } });
284
+ return {
285
+ delta(text) { if (text) onWrite('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } }); },
286
+ done() {
287
+ onWrite('content_block_stop', { type: 'content_block_stop', index: 0 });
288
+ onWrite('message_delta', { type: 'message_delta', delta: { stop_reason: 'end_turn', stop_sequence: null }, usage: { output_tokens: 0 } });
289
+ onWrite('message_stop', { type: 'message_stop' });
290
+ },
291
+ };
292
+ }
@@ -0,0 +1,20 @@
1
+ // GENERATED — do not edit.
2
+ // Source of truth: chatpanel-events/scopes.js (npm @chatpanel/events).
3
+ // Edit there, then run: npm run sync:events
4
+ //
5
+ // Vendored rather than depended on: the bridge ships zero runtime dependencies so a
6
+ // curl one-liner install cannot fail on someone's registry, and so the compiled
7
+ // single-file binary has nothing to resolve.
8
+
9
+ // scopes.js — the data-scope vocabulary, on its own so it can travel alone.
10
+ //
11
+ // One list names what anything in ChatPanel may touch. Capabilities declare `reads`
12
+ // and `writes` from it, sources declare `reads`, and a skill package declares `reads`
13
+ // — three declarations, one vocabulary, or "what may this reach" gets three answers.
14
+ //
15
+ // It is a separate module rather than a constant inside capability.js because the
16
+ // consumers have very different weights. The bridge vendors the skill contract and has
17
+ // zero runtime dependencies by design; pulling the capability machinery and the event
18
+ // schema behind it to reach a five-element array would be the transitive-graph mistake
19
+ // the extension's first-paint budget exists to prevent, one repo over.
20
+ export const DATA_SCOPES = Object.freeze(['notes', 'meetings', 'chats', 'page', 'files', 'net']);