@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +1531 -0
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +75 -0
  4. package/LICENSE +21 -0
  5. package/README.md +464 -0
  6. package/SECURITY.md +130 -0
  7. package/assets/accuracy-lab.html +2639 -0
  8. package/assets/api-clis-real.jpg +0 -0
  9. package/assets/bridge-console-hero.jpg +0 -0
  10. package/assets/browser-privacy.svg +151 -0
  11. package/assets/demo-orchestration.svg +74 -0
  12. package/assets/desktop-select-region.jpg +0 -0
  13. package/assets/in-page-chat.gif +0 -0
  14. package/assets/orchestration-hero.svg +126 -0
  15. package/assets/social-preview.png +0 -0
  16. package/assets/zara-accent.png +0 -0
  17. package/build/bootstrap.js +548 -0
  18. package/build/build.js +680 -0
  19. package/build/payload-entry.js +649 -0
  20. package/build/payload-signing-pub.json +7 -0
  21. package/docs/AGENT_GUIDE.md +259 -0
  22. package/docs/RELEASE.md +106 -0
  23. package/docs/SAFETY.md +112 -0
  24. package/docs/TESTING.md +181 -0
  25. package/installer/server.js +231 -0
  26. package/installer/ui/app.js +278 -0
  27. package/installer/ui/index.html +24 -0
  28. package/installer/ui/styles.css +146 -0
  29. package/package.json +95 -0
  30. package/scripts/bootstrap-e2e.mjs +650 -0
  31. package/scripts/certify-bridge.mjs +636 -0
  32. package/scripts/check-companion-surface.mjs +118 -0
  33. package/scripts/extract-welcome.mjs +64 -0
  34. package/scripts/gh-route-handler-check.mjs +57 -0
  35. package/scripts/gh-wire-test.mjs +107 -0
  36. package/scripts/publish-downloads.mjs +180 -0
  37. package/scripts/smoke-all-tools.mjs +509 -0
  38. package/scripts/smoke-live-bridge.mjs +696 -0
  39. package/scripts/splice-welcome.mjs +63 -0
  40. package/scripts/welcome-body.txt +2733 -0
  41. package/src/anthropic-client.ts +192 -0
  42. package/src/bootstrap-exe.ts +69 -0
  43. package/src/bridge.ts +2444 -0
  44. package/src/chat.ts +345 -0
  45. package/src/cli-runner.ts +239 -0
  46. package/src/cli.ts +649 -0
  47. package/src/config.ts +199 -0
  48. package/src/desktop-overlay.ps1 +121 -0
  49. package/src/executable-resolver.ts +330 -0
  50. package/src/handlers/agy-imagegen.ts +179 -0
  51. package/src/handlers/github-cli.ts +399 -0
  52. package/src/handlers/higgsfield-cli.ts +783 -0
  53. package/src/launch.js +337 -0
  54. package/src/mcp-server.ts +1265 -0
  55. package/src/pair-claim.ts +218 -0
  56. package/src/payload-daemon.ts +168 -0
  57. package/src/server.ts +21036 -0
  58. package/src/tool-defaults.ts +230 -0
  59. package/src/update-check.js +136 -0
  60. package/tray/build.py +76 -0
  61. package/tray/requirements.txt +2 -0
  62. 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
+ }