@empir3/empir3-bridge 0.3.21
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/CHANGELOG.md +1531 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/SECURITY.md +130 -0
- package/assets/accuracy-lab.html +2639 -0
- package/assets/api-clis-real.jpg +0 -0
- package/assets/bridge-console-hero.jpg +0 -0
- package/assets/browser-privacy.svg +151 -0
- package/assets/demo-orchestration.svg +74 -0
- package/assets/desktop-select-region.jpg +0 -0
- package/assets/in-page-chat.gif +0 -0
- package/assets/orchestration-hero.svg +126 -0
- package/assets/social-preview.png +0 -0
- package/assets/zara-accent.png +0 -0
- package/build/bootstrap.js +548 -0
- package/build/build.js +680 -0
- package/build/payload-entry.js +649 -0
- package/build/payload-signing-pub.json +7 -0
- package/docs/AGENT_GUIDE.md +259 -0
- package/docs/RELEASE.md +106 -0
- package/docs/SAFETY.md +112 -0
- package/docs/TESTING.md +181 -0
- package/installer/server.js +231 -0
- package/installer/ui/app.js +278 -0
- package/installer/ui/index.html +24 -0
- package/installer/ui/styles.css +146 -0
- package/package.json +95 -0
- package/scripts/bootstrap-e2e.mjs +650 -0
- package/scripts/certify-bridge.mjs +636 -0
- package/scripts/check-companion-surface.mjs +118 -0
- package/scripts/extract-welcome.mjs +64 -0
- package/scripts/gh-route-handler-check.mjs +57 -0
- package/scripts/gh-wire-test.mjs +107 -0
- package/scripts/publish-downloads.mjs +180 -0
- package/scripts/smoke-all-tools.mjs +509 -0
- package/scripts/smoke-live-bridge.mjs +696 -0
- package/scripts/splice-welcome.mjs +63 -0
- package/scripts/welcome-body.txt +2733 -0
- package/src/anthropic-client.ts +192 -0
- package/src/bootstrap-exe.ts +69 -0
- package/src/bridge.ts +2444 -0
- package/src/chat.ts +345 -0
- package/src/cli-runner.ts +239 -0
- package/src/cli.ts +649 -0
- package/src/config.ts +199 -0
- package/src/desktop-overlay.ps1 +121 -0
- package/src/executable-resolver.ts +330 -0
- package/src/handlers/agy-imagegen.ts +179 -0
- package/src/handlers/github-cli.ts +399 -0
- package/src/handlers/higgsfield-cli.ts +783 -0
- package/src/launch.js +337 -0
- package/src/mcp-server.ts +1265 -0
- package/src/pair-claim.ts +218 -0
- package/src/payload-daemon.ts +168 -0
- package/src/server.ts +21036 -0
- package/src/tool-defaults.ts +230 -0
- package/src/update-check.js +136 -0
- package/tray/build.py +76 -0
- package/tray/requirements.txt +2 -0
- package/tray/tray.py +1843 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic Messages API streaming client — minimal, no SDK dependency.
|
|
3
|
+
*
|
|
4
|
+
* One exported function: `streamMessages` returns an async generator yielding
|
|
5
|
+
* normalized events (`text_delta`, `tool_use_start`, `tool_use_end`,
|
|
6
|
+
* `message_end`, `usage`, `error`). The chat loop in `chat.ts` walks the
|
|
7
|
+
* generator without caring about the SSE wire format.
|
|
8
|
+
*
|
|
9
|
+
* Why hand-rolled instead of @anthropic-ai/sdk: the SDK is ~1MB and pulls
|
|
10
|
+
* in `node-fetch` polyfills we don't need (Node 18+ has native fetch). This
|
|
11
|
+
* file is ~150 LOC + zero dependencies. If we ever want vision or batch
|
|
12
|
+
* we'll reach for the SDK.
|
|
13
|
+
*
|
|
14
|
+
* Tool input arrives as `input_json_delta` events with partial JSON strings
|
|
15
|
+
* that need accumulation. We buffer per content-block index and parse once
|
|
16
|
+
* at `content_block_stop` so the consumer gets a single `tool_use_start`
|
|
17
|
+
* event with fully-formed input.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const API_URL = 'https://api.anthropic.com/v1/messages';
|
|
21
|
+
const API_VERSION = '2023-06-01';
|
|
22
|
+
|
|
23
|
+
export interface AnthropicTool {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
input_schema: { type: 'object'; properties: Record<string, unknown>; required?: string[] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AnthropicMessage {
|
|
30
|
+
role: 'user' | 'assistant';
|
|
31
|
+
content: string | Array<
|
|
32
|
+
| { type: 'text'; text: string }
|
|
33
|
+
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
34
|
+
| { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean }
|
|
35
|
+
>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StreamRequest {
|
|
39
|
+
apiKey: string;
|
|
40
|
+
model: string;
|
|
41
|
+
maxTokens: number;
|
|
42
|
+
system?: string;
|
|
43
|
+
messages: AnthropicMessage[];
|
|
44
|
+
tools?: AnthropicTool[];
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type StreamEvent =
|
|
49
|
+
| { type: 'text_delta'; text: string }
|
|
50
|
+
| { type: 'tool_use_start'; id: string; name: string; input: unknown }
|
|
51
|
+
| { type: 'message_end'; stopReason: string | null }
|
|
52
|
+
| { type: 'usage'; inputTokens: number; outputTokens: number }
|
|
53
|
+
| { type: 'error'; message: string };
|
|
54
|
+
|
|
55
|
+
interface PendingToolBlock {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
jsonBuffer: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function* streamMessages(req: StreamRequest): AsyncGenerator<StreamEvent> {
|
|
62
|
+
const body: Record<string, unknown> = {
|
|
63
|
+
model: req.model,
|
|
64
|
+
max_tokens: req.maxTokens,
|
|
65
|
+
stream: true,
|
|
66
|
+
messages: req.messages,
|
|
67
|
+
};
|
|
68
|
+
if (req.system) body.system = req.system;
|
|
69
|
+
if (req.tools && req.tools.length > 0) body.tools = req.tools;
|
|
70
|
+
|
|
71
|
+
let response: Response;
|
|
72
|
+
try {
|
|
73
|
+
response = await fetch(API_URL, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'x-api-key': req.apiKey,
|
|
78
|
+
'anthropic-version': API_VERSION,
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify(body),
|
|
81
|
+
signal: req.signal,
|
|
82
|
+
});
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
yield { type: 'error', message: `Network error: ${e?.message || String(e)}` };
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
let errText = '';
|
|
90
|
+
try { errText = await response.text(); } catch { /* ignore */ }
|
|
91
|
+
yield { type: 'error', message: `API ${response.status}: ${errText.slice(0, 500) || response.statusText}` };
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!response.body) {
|
|
95
|
+
yield { type: 'error', message: 'API returned no body' };
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const reader = response.body.getReader();
|
|
100
|
+
const decoder = new TextDecoder();
|
|
101
|
+
const pendingTools = new Map<number, PendingToolBlock>();
|
|
102
|
+
let buffer = '';
|
|
103
|
+
let inputTokens = 0;
|
|
104
|
+
let outputTokens = 0;
|
|
105
|
+
let stopReason: string | null = null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
while (true) {
|
|
109
|
+
const { value, done } = await reader.read();
|
|
110
|
+
if (done) break;
|
|
111
|
+
buffer += decoder.decode(value, { stream: true });
|
|
112
|
+
|
|
113
|
+
// SSE frames are separated by blank lines. Parse complete frames out
|
|
114
|
+
// of the buffer; leave any trailing partial frame for the next chunk.
|
|
115
|
+
let sep: number;
|
|
116
|
+
while ((sep = buffer.indexOf('\n\n')) !== -1) {
|
|
117
|
+
const frame = buffer.slice(0, sep);
|
|
118
|
+
buffer = buffer.slice(sep + 2);
|
|
119
|
+
|
|
120
|
+
// A frame is one or more "event:" / "data:" lines. We only need data.
|
|
121
|
+
const dataLine = frame.split('\n').find(l => l.startsWith('data:'));
|
|
122
|
+
if (!dataLine) continue;
|
|
123
|
+
const data = dataLine.slice(5).trim();
|
|
124
|
+
if (!data || data === '[DONE]') continue;
|
|
125
|
+
|
|
126
|
+
let event: any;
|
|
127
|
+
try { event = JSON.parse(data); } catch { continue; }
|
|
128
|
+
|
|
129
|
+
switch (event.type) {
|
|
130
|
+
case 'message_start': {
|
|
131
|
+
const usage = event.message?.usage;
|
|
132
|
+
if (usage?.input_tokens) inputTokens = usage.input_tokens;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'content_block_start': {
|
|
136
|
+
const block = event.content_block;
|
|
137
|
+
if (block?.type === 'tool_use') {
|
|
138
|
+
pendingTools.set(event.index, { id: block.id, name: block.name, jsonBuffer: '' });
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'content_block_delta': {
|
|
143
|
+
const delta = event.delta;
|
|
144
|
+
if (delta?.type === 'text_delta' && typeof delta.text === 'string') {
|
|
145
|
+
yield { type: 'text_delta', text: delta.text };
|
|
146
|
+
} else if (delta?.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
147
|
+
const pending = pendingTools.get(event.index);
|
|
148
|
+
if (pending) pending.jsonBuffer += delta.partial_json;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'content_block_stop': {
|
|
153
|
+
const pending = pendingTools.get(event.index);
|
|
154
|
+
if (pending) {
|
|
155
|
+
let parsedInput: unknown = {};
|
|
156
|
+
if (pending.jsonBuffer.length > 0) {
|
|
157
|
+
try { parsedInput = JSON.parse(pending.jsonBuffer); } catch { parsedInput = {}; }
|
|
158
|
+
}
|
|
159
|
+
yield { type: 'tool_use_start', id: pending.id, name: pending.name, input: parsedInput };
|
|
160
|
+
pendingTools.delete(event.index);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'message_delta': {
|
|
165
|
+
if (event.delta?.stop_reason) stopReason = event.delta.stop_reason;
|
|
166
|
+
if (event.usage?.output_tokens) outputTokens = event.usage.output_tokens;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'message_stop': {
|
|
170
|
+
// Final event — flush usage + end signal
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
// ignore unknown event types — forward-compat
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (e: any) {
|
|
180
|
+
if (e?.name === 'AbortError') {
|
|
181
|
+
yield { type: 'message_end', stopReason: 'aborted' };
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
yield { type: 'error', message: `Stream read error: ${e?.message || String(e)}` };
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
189
|
+
yield { type: 'usage', inputTokens, outputTokens };
|
|
190
|
+
}
|
|
191
|
+
yield { type: 'message_end', stopReason };
|
|
192
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "which exe is the product?" resolver.
|
|
3
|
+
*
|
|
4
|
+
* Background: once the native Go
|
|
5
|
+
* bootstrapper spawns Node as a subprocess, `process.execPath` inside any JS is
|
|
6
|
+
* `node.exe`, NOT `Empir3Setup.exe`. So nothing may use `process.execPath` to
|
|
7
|
+
* register, advertise, or relaunch "the installer" anymore. Every such call
|
|
8
|
+
* site resolves the bootstrap exe through this one function instead.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (matches the Go stub's resolver):
|
|
11
|
+
* 1. process.env.EMPIR3_BOOTSTRAP_EXE (set by the Go stub for its Node child)
|
|
12
|
+
* 2. %APPDATA%/Empir3/bridge-bootstrap.json → bootstrapPath
|
|
13
|
+
* 3. stable %APPDATA%/Empir3/Empir3Setup.exe (if present)
|
|
14
|
+
* 4. process.execPath — ONLY for a genuine old Node-SEA process (isSea()),
|
|
15
|
+
* where execPath really is Empir3Setup.exe. Never fires under spawned Node.
|
|
16
|
+
* 5. null → caller must FAIL CLOSED (skip the registration; never write
|
|
17
|
+
* node.exe as the product path).
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { join, basename } from 'path';
|
|
22
|
+
|
|
23
|
+
function appDataEmpir3Dir(): string {
|
|
24
|
+
const roaming = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
|
|
25
|
+
return join(roaming, 'Empir3');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True only inside a genuine Node Single-Executable-Application (the old SEA
|
|
29
|
+
* bootstrapper). Under the Go stub's spawned node.exe this is false. Falls
|
|
30
|
+
* back to a basename check if node:sea is unavailable on this runtime. */
|
|
31
|
+
function isGenuineSeaBootstrap(): boolean {
|
|
32
|
+
try {
|
|
33
|
+
// node:sea exists on Node 20+. isSea() is true only in a SEA binary.
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
35
|
+
const sea = require('node:sea');
|
|
36
|
+
if (sea && typeof sea.isSea === 'function') return Boolean(sea.isSea());
|
|
37
|
+
} catch {
|
|
38
|
+
/* node:sea not present — fall through to basename heuristic */
|
|
39
|
+
}
|
|
40
|
+
return basename(process.execPath).toLowerCase() === 'empir3setup.exe';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the bootstrap exe to use for registration / relaunch.
|
|
45
|
+
* Returns an absolute path that exists, or null when nothing trustworthy
|
|
46
|
+
* resolves (caller must fail closed).
|
|
47
|
+
*/
|
|
48
|
+
export function resolveBootstrapExe(): string | null {
|
|
49
|
+
const fromEnv = (process.env.EMPIR3_BOOTSTRAP_EXE || '').trim();
|
|
50
|
+
if (fromEnv && existsSync(fromEnv)) return fromEnv;
|
|
51
|
+
|
|
52
|
+
const pointer = join(appDataEmpir3Dir(), 'bridge-bootstrap.json');
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.parse(readFileSync(pointer, 'utf8'));
|
|
55
|
+
const p = String(data?.bootstrapPath || '').trim();
|
|
56
|
+
if (p && existsSync(p)) return p;
|
|
57
|
+
} catch {
|
|
58
|
+
/* no/invalid pointer — continue */
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const stable = join(appDataEmpir3Dir(), 'Empir3Setup.exe');
|
|
62
|
+
if (existsSync(stable)) return stable;
|
|
63
|
+
|
|
64
|
+
if (isGenuineSeaBootstrap() && existsSync(process.execPath)) {
|
|
65
|
+
return process.execPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|