@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,230 @@
1
+ /**
2
+ * Per-tool kill-switch defaults — single source of truth for which bridge
3
+ * tools are enabled when Claude is talking through the overlay.
4
+ *
5
+ * Read tools default ON (page inspection is safe). Interact tools default
6
+ * OFF — the user has to opt in before Claude can click/type on pages they
7
+ * have open. Same mental model as Anthropic computer-use, macOS
8
+ * Accessibility, or Chrome extension permissions: explicit user consent,
9
+ * no AI judgment.
10
+ *
11
+ * The chat loop in `chat.ts` filters the tool list it sends to the model
12
+ * based on `enabledTools` in config — disabled tools never appear in the
13
+ * model's tool inventory, so it can't even hallucinate a call to one.
14
+ * Defense-in-depth: dispatcher also short-circuits if a disabled tool
15
+ * somehow lands on the wire.
16
+ *
17
+ * `browser_evaluate` is a special case: unrestricted JS eval is effectively
18
+ * root on the current page. Defaults OFF until the read-only sandbox lands
19
+ * (separate task — AST check that blocks DOM mutation, storage writes,
20
+ * fetch/XHR). Power users opt in via /settings.
21
+ */
22
+
23
+ export type ToolGroup = 'read' | 'navigate' | 'interact' | 'desktop' | 'recordings' | 'eval' | 'advisor' | 'higgsfield' | 'providers' | 'clis';
24
+
25
+ export interface ToolMeta {
26
+ name: string;
27
+ group: ToolGroup;
28
+ defaultEnabled: boolean;
29
+ blurb: string;
30
+ }
31
+
32
+ export const TOOL_META: ToolMeta[] = [
33
+ // ── Advisor (discoverability — always on) ────────────────────
34
+ { name: 'bridge_tool_advisor', group: 'advisor', defaultEnabled: true,
35
+ blurb: 'Discoverability helper. Pass `intent` as a one-line description of what you are trying to do (e.g. "click a small icon in Photoshop", "fill in a web form", "guide the user through a tutorial") and the bridge returns the matching tool family, rationale, and example sequence. Call this FIRST when unsure which of the 47 tools fits.' },
36
+ { name: 'bridge_setup_status', group: 'advisor', defaultEnabled: true,
37
+ blurb: 'Report the first-use desktop setup checklist: overlay injection, monitor detection, saved click calibration, and recording/playback readiness.' },
38
+ { name: 'bridge_setup_save', group: 'advisor', defaultEnabled: true,
39
+ blurb: 'Save the first-use desktop setup checklist result to bridge-settings.json so MCP and empir3 agents can confirm the device was calibrated.' },
40
+ { name: 'bridge_overlay_reinject', group: 'advisor', defaultEnabled: true,
41
+ blurb: 'Repair command for the browser overlay. Re-injects the chat/recording overlay into current and future bridge browser tabs, then verifies that an overlay client connected.' },
42
+ { name: 'bridge_reliability_status', group: 'advisor', defaultEnabled: true,
43
+ blurb: 'Show bridge health, enabled tools, and recent action receipts for debugging failed or uncertain tool calls.' },
44
+ { name: 'bridge_reliability_smoke', group: 'advisor', defaultEnabled: true,
45
+ blurb: 'Run the built-in bridge reliability smoke checks.' },
46
+ { name: 'bridge_action_log', group: 'advisor', defaultEnabled: true,
47
+ blurb: 'Read recent bridge action receipts for debugging failed or uncertain tool calls.' },
48
+ { name: 'bridge_safety_status', group: 'advisor', defaultEnabled: true,
49
+ blurb: 'Show whether browser write controls, desktop controls, eval, or recordings are currently enabled.' },
50
+ { name: 'bridge_revoke_control', group: 'advisor', defaultEnabled: true,
51
+ blurb: 'Immediately disable browser interact, desktop, eval, and recording tools in bridge settings.' },
52
+
53
+ // ── Read (page inspection, no side effects) ──────────────────
54
+ { name: 'browser_status', group: 'read', defaultEnabled: true,
55
+ blurb: 'Check whether the browser bridge is connected and what URL is open.' },
56
+ { name: 'browser_text', group: 'read', defaultEnabled: true,
57
+ blurb: 'Read the page as plain text. Cheapest way to see what is on the page — use before screenshotting when you only need the words.' },
58
+ { name: 'browser_snapshot', group: 'read', defaultEnabled: true,
59
+ blurb: 'PRIMARY tool for web work. Returns the page accessibility tree with element refs (e0, e1, …) including role, name, bounds. Call before browser_click_ref / browser_type_ref. Refs are invalidated when the page changes.' },
60
+ { name: 'browser_screenshot', group: 'read', defaultEnabled: true,
61
+ blurb: 'Visual confirmation of the current viewport (JPEG). Use after a write to verify state. Prefer browser_snapshot when you actually need to act on something — refs are far more reliable than pixels.' },
62
+ { name: 'browser_tab_state', group: 'read', defaultEnabled: true,
63
+ blurb: 'List bridge browser tabs and report which one is agent-controlled versus user-focused.' },
64
+ { name: 'desktop_monitors', group: 'read', defaultEnabled: true,
65
+ blurb: 'List DPI-aware physical desktop monitor bounds, including negative coordinates.' },
66
+ { name: 'desktop_screenshot', group: 'read', defaultEnabled: true,
67
+ blurb: 'Capture desktop pixels. Pass `monitor` for a whole display, `region:{x,y,width,height}` for a tight crop at native resolution, or `grid:true` to overlay a coordinate grid. When an agent-focus region is active, defaults to the focus crop with a focus-relative coord grid (top-left = 0,0) — read targets straight off the image and pass to click/pointer with space:"focus". Pass `grid:false` to opt out.' },
68
+ { name: 'desktop_screenshot_zoom', group: 'read', defaultEnabled: true,
69
+ blurb: 'Take a tight native-resolution crop centered on (x, y) with a small marker — pixel-accurate inspection of a small area before clicking or pointing.' },
70
+ { name: 'desktop_cursor_position', group: 'read', defaultEnabled: true,
71
+ blurb: 'Read the current physical cursor position.' },
72
+ { name: 'desktop_snapshot', group: 'read', defaultEnabled: true,
73
+ blurb: 'Enumerate visible interactive elements (buttons, menus, inputs) on the desktop via UI Automation. Returns refs d0..dN for use with desktop_click_ref. Best on native Win32/UWP apps; CEF/Electron apps fall back to vision-coord targeting.' },
74
+ { name: 'desktop_snapshot_som', group: 'read', defaultEnabled: true,
75
+ blurb: 'Set-of-Mark snapshot: enumerate elements inside the agent-focus region (or explicit region) and draw numbered colored boxes on a screenshot. Agent picks the number off the image and calls desktop_click_ref. Removes pixel-coordinate guessing for native Win32 apps.' },
76
+ { name: 'desktop_focus_status', group: 'read', defaultEnabled: true,
77
+ blurb: 'Report whether the user has set an agent-focus region (via desktop_select_region) and how much TTL remains.' },
78
+ { name: 'desktop_pointer_status', group: 'read', defaultEnabled: true,
79
+ blurb: 'Report whether the agent ghost cursor is currently shown, its position and label.' },
80
+ { name: 'desktop_calibration_status', group: 'read', defaultEnabled: true,
81
+ blurb: 'Return the persisted desktop click calibration offset (or null if uncalibrated).' },
82
+
83
+ // ── Navigate (visible side effect, browser-scoped) ───────────
84
+ { name: 'browser_navigate', group: 'navigate', defaultEnabled: true,
85
+ blurb: 'Open a URL in the bridge browser tab.' },
86
+ { name: 'browser_scroll', group: 'navigate', defaultEnabled: true,
87
+ blurb: 'Scroll the page by a number of pixels.' },
88
+ { name: 'browser_refresh', group: 'navigate', defaultEnabled: true,
89
+ blurb: 'Reload the current page.' },
90
+
91
+ // ── Interact (writes input to pages you have open) ───────────
92
+ { name: 'browser_tab_focus', group: 'navigate', defaultEnabled: true,
93
+ blurb: 'Explicitly mark a tab as user focus or hand a tab to the agent by target id. Does not auto-switch when the user merely opens a tab.' },
94
+ { name: 'browser_click', group: 'interact', defaultEnabled: false,
95
+ blurb: 'Click an element by CSS selector. Use only when you already have a stable selector (e.g. from your own page). For everything else, prefer browser_click_ref after browser_snapshot.' },
96
+ { name: 'browser_click_ref', group: 'interact', defaultEnabled: false,
97
+ blurb: 'PREFERRED way to click on a webpage. Pass a ref (e.g. "e5") from the most recent browser_snapshot. Resolves to the element\'s bounds center.' },
98
+ { name: 'browser_click_xy', group: 'interact', defaultEnabled: false,
99
+ blurb: 'Click viewport coordinates with native browser mouse events. Last resort for canvas / SVG / iframe content the DOM can\'t describe.' },
100
+ { name: 'browser_type', group: 'interact', defaultEnabled: false,
101
+ blurb: 'Type text into a form field by CSS selector. Prefer browser_type_ref unless you have a stable selector.' },
102
+ { name: 'browser_type_ref', group: 'interact', defaultEnabled: false,
103
+ blurb: 'PREFERRED way to type on a webpage. Pass a ref from browser_snapshot (input/textbox role). Uses the native value setter + input/change events — works with React, Vue, plain HTML.' },
104
+ { name: 'browser_press', group: 'interact', defaultEnabled: false,
105
+ blurb: 'Press a keyboard key in the active page (Enter, Tab, Escape, Ctrl+A, etc). Use after typing to submit, after focus to navigate, or to dismiss modals.' },
106
+ { name: 'browser_highlight', group: 'interact', defaultEnabled: false,
107
+ blurb: 'Briefly outline an element on the page. Use to signal to the user where you are looking ("the email field is here") without taking action.' },
108
+ { name: 'desktop_click', group: 'desktop', defaultEnabled: false,
109
+ blurb: 'Click physical desktop coordinates. Prefer desktop_click_ref (via desktop_snapshot_som) or desktop_click_cell — they survive screen movement and DPI changes. Use raw coords only when you have a reliable pixel target.' },
110
+ { name: 'desktop_hover', group: 'desktop', defaultEnabled: false,
111
+ blurb: 'Move the cursor to physical desktop coordinates without clicking. Used for hover-revealed tooltips/menus when no UIA ref exists.' },
112
+ { name: 'desktop_drag', group: 'desktop', defaultEnabled: false,
113
+ blurb: 'Drag between two physical desktop coordinates. For moving sliders, resizing windows, or dragging files in native apps.' },
114
+ { name: 'desktop_click_ref', group: 'desktop', defaultEnabled: false,
115
+ blurb: 'PREFERRED way to click in a native desktop app. Pass a ref ("d3") from desktop_snapshot or desktop_snapshot_som. Resolves to the element bounds center and performs a real Win32 click. Refs invalidated by the next snapshot.' },
116
+ { name: 'desktop_hover_ref', group: 'desktop', defaultEnabled: false,
117
+ blurb: 'Hover over a desktop element by ref from the last desktop_snapshot. Use for hover-revealed tooltips and dropdown menus.' },
118
+ { name: 'desktop_overlay', group: 'desktop', defaultEnabled: false,
119
+ blurb: 'Toggle a click-through labeled-box overlay on top of the screen showing the elements from the most recent desktop_snapshot. Keys and clicks pass through. Useful for showing the user what the agent sees without blocking them.' },
120
+ { name: 'desktop_select_region', group: 'desktop', defaultEnabled: false,
121
+ blurb: 'Open a fullscreen overlay so the user can drag a rectangle around the area they want help with. Sets an "agent focus" — subsequent desktop_screenshot/desktop_snapshot calls auto-scope to it. 30-minute TTL.' },
122
+ { name: 'desktop_release_focus', group: 'desktop', defaultEnabled: false,
123
+ blurb: 'Clear the current agent-focus region. The on-screen chip disappears and tools revert to whole-monitor/foreground defaults.' },
124
+ { name: 'desktop_pointer_show', group: 'desktop', defaultEnabled: false,
125
+ blurb: 'Show a click-through "ghost cursor" overlay at absolute screen coords. Visual only — the user\'s real mouse is unaffected. Use to draw attention to a spot ("look here") without taking control.' },
126
+ { name: 'desktop_pointer_move', group: 'desktop', defaultEnabled: false,
127
+ blurb: 'Reposition the ghost cursor (or show it if not already visible). Updates at ~25fps.' },
128
+ { name: 'desktop_pointer_pulse', group: 'desktop', defaultEnabled: false,
129
+ blurb: 'Trigger a one-shot expanding ring animation at the ghost cursor — "look HERE now" emphasis.' },
130
+ { name: 'desktop_pointer_hide', group: 'desktop', defaultEnabled: false,
131
+ blurb: 'Hide the ghost cursor overlay.' },
132
+ { name: 'desktop_calibrate_pointer', group: 'desktop', defaultEnabled: false,
133
+ blurb: 'Run an interactive click calibration: shows the ghost cursor at primary-screen center and asks the user to click it. The delta is saved and applied to every desktop_click afterwards.' },
134
+ { name: 'desktop_click_cell', group: 'desktop', defaultEnabled: false,
135
+ blurb: 'Click a cell of the focus chess-board grid (1-indexed col/row matching the on-screen pill labels). Optional subX/subY for sub-cell offset.' },
136
+ { name: 'desktop_pointer_cell', group: 'desktop', defaultEnabled: false,
137
+ blurb: 'Show the ghost cursor at the center of a focus-grid cell (col, row).' },
138
+ { name: 'desktop_focus_grid', group: 'desktop', defaultEnabled: false,
139
+ blurb: 'Show/hide a click-through on-screen grid overlay on the focus region. Lets the user read the same chess-board labels the agent sees in screenshots — "click cell 8,7" works without screenshot round-trip.' },
140
+ { name: 'desktop_pick_point', group: 'desktop', defaultEnabled: false,
141
+ blurb: 'User clicks inside the focus area to designate a point. Bridge returns the click as focus-relative pixel, absolute pixel, and chess-board cell coords. Best tool for "click HERE" when the user can show you.' },
142
+ { name: 'desktop_click_page', group: 'desktop', defaultEnabled: false,
143
+ blurb: 'Perform a REAL OS click on an element in the bridge\'s own Chrome page, given a CSS selector, snapshot ref, or cssX/cssY. Maps page coords to physical screen pixels automatically (content-window origin + devicePixelRatio + calibration) — use when a page needs a trusted hardware click rather than a synthetic browser_click. Bridge\'s Chrome only.' },
144
+ { name: 'desktop_pointer_page', group: 'desktop', defaultEnabled: false,
145
+ blurb: 'Show the click-through ghost cursor on top of an element in the bridge\'s Chrome page (selector/ref/cssX-cssY). Same page→screen mapping as desktop_click_page but visual-only — "I\'m looking at this button" without clicking.' },
146
+ { name: 'page_to_screen', group: 'read', defaultEnabled: true,
147
+ blurb: 'Inspect-only: resolve a page element (selector/ref/cssX-cssY) in the bridge\'s Chrome to its physical virtual-screen coordinates, plus the content-window origin, devicePixelRatio, and calibrated click coord. Use to verify where a real click would land before committing.' },
148
+ { name: 'desktop_toolbar', group: 'desktop', defaultEnabled: false,
149
+ blurb: 'Open or close the movable desktop toolbar widget with focus, release, overlay chat injection, recording, playback, and monitor-local quick calibration controls.' },
150
+
151
+ // ── Eval (full JS — root on the page) ────────────────────────
152
+ { name: 'browser_evaluate', group: 'eval', defaultEnabled: false,
153
+ blurb: 'Run arbitrary JavaScript on the page. Equivalent to opening DevTools and pasting code — leave OFF unless you trust the prompt source.' },
154
+
155
+ // ── Recordings (replay tooling — niche; off by default) ──────
156
+ { name: 'browser_record_start', group: 'recordings', defaultEnabled: false,
157
+ blurb: 'Start capturing user actions (clicks, types, navigation) to a named JSON file. Stop with browser_record_stop, replay with browser_play. Useful for building reusable demo flows or automating repetitive tasks.' },
158
+ { name: 'browser_record_stop', group: 'recordings', defaultEnabled: false,
159
+ blurb: 'Stop the active recording and save it under the name passed to browser_record_start.' },
160
+ { name: 'browser_play', group: 'recordings', defaultEnabled: false,
161
+ blurb: 'Replay a saved recording by name. Optional `speed` (e.g. 2 = 2x) and `variables` for substituting values into recorded inputs.' },
162
+ { name: 'browser_recordings', group: 'recordings', defaultEnabled: false,
163
+ blurb: 'List saved recordings (name, startUrl, recorded timestamp, action count).' },
164
+ { name: 'browser_chat', group: 'recordings', defaultEnabled: false,
165
+ blurb: 'Push a message into the bridge overlay chat panel — appears as if the user typed it. Use to surface progress / questions in the same place the user reads agent output.' },
166
+ { name: 'browser_read_chat', group: 'recordings', defaultEnabled: false,
167
+ blurb: 'Read recent messages from the overlay chat panel (both user and agent). Useful for picking up where a previous conversation left off.' },
168
+
169
+ // ── Higgsfield CLI (handler-gated; off in tray by default) ────
170
+ // Read-only tools default ON, mutating defaults OFF — same pattern as
171
+ // the desktop interact / browser_evaluate split. Family is additionally
172
+ // gated by the tray "Enable Higgsfield CLI" checkbox so a user who has
173
+ // never enabled the handler never sees these in any MCP client.
174
+ { name: 'higgsfield_status', group: 'higgsfield', defaultEnabled: true,
175
+ blurb: 'Check whether the higgsfield CLI is installed, authenticated, and ready.' },
176
+ { name: 'higgsfield_list', group: 'higgsfield', defaultEnabled: true,
177
+ blurb: 'List the user\'s recent Higgsfield generations.' },
178
+ { name: 'higgsfield_models', group: 'higgsfield', defaultEnabled: true,
179
+ blurb: 'List the available Higgsfield models (job_set_types) with their media type — image, video, or text. Call this BEFORE higgsfield_generate to pick a valid model id. Optional type filter. The catalog changes as Higgsfield adds models, so always read it live rather than guessing.' },
180
+ { name: 'higgsfield_generate', group: 'higgsfield', defaultEnabled: false,
181
+ blurb: 'Generate a Higgsfield video/image from a text prompt. Returns the result URL plus a local artifact path. Costs money/quota on the user\'s Higgsfield account — defaults OFF, opt in per session.' },
182
+
183
+ // ── Lent CLIs (run another model's CLI: codex / grok / gemini / claude) ──
184
+ { name: 'cli_run', group: 'clis', defaultEnabled: true,
185
+ blurb: 'Run another model\'s lent CLI (codex / grok / gemini / claude) with a prompt and get the text back — so the driving agent can pull a second LLM into a task. Governed by each CLI\'s lend toggle (a model that isn\'t lent is refused). Spends the user\'s CLI subscription/quota. Supports cwd, agentic mode (file-writing where the CLI allows), and background runs.' },
186
+ { name: 'cli_runs', group: 'clis', defaultEnabled: true,
187
+ blurb: 'List recent cli_run invocations (id, model, status, duration, transcript path).' },
188
+ { name: 'cli_run_status', group: 'clis', defaultEnabled: true,
189
+ blurb: 'Get the status + output of a cli_run by id — used to poll a background run to completion.' },
190
+ { name: 'cli_status', group: 'clis', defaultEnabled: true,
191
+ blurb: 'Discover which lent CLIs (codex / grok / gemini / claude) are usable right now — per model: available, lent, authenticated, ready, and the blocker if not. Call before cli_run to route work without trial-and-error refusals.' },
192
+
193
+ // ── Custom LLMs (Ollama / LM Studio / OpenRouter / vLLM / etc) ──
194
+ // Single MCP tool that fans out to any custom LLM the user has added on
195
+ // the API & CLIs pane. The endpoint protocol is OpenAI-compatible, but
196
+ // the tool is NOT scoped to OpenAI — the name reflects "custom LLM
197
+ // dispatcher", not the brand of any one provider. Defaults ON; the
198
+ // family is hidden entirely when no providers are configured, so the
199
+ // toggle never appears as a phantom permission.
200
+ { name: 'custom_llm', group: 'providers', defaultEnabled: true,
201
+ blurb: 'Send a chat-completion request to any custom LLM the user configured on the API & CLIs pane (Ollama, LM Studio, OpenRouter, Groq Cloud, vLLM, etc — any OpenAI-compatible endpoint). Pass `provider` (slug), `model`, `prompt`, optional `system`. Returns the assistant text.' },
202
+ ];
203
+
204
+ // Maps a tool name to the handler-family key checked in
205
+ // settings.handlers[<family>].enabled. Family-gated tools are skipped at
206
+ // both the MCP tools/list layer and the bridge dispatcher when the family
207
+ // is disabled, regardless of per-tool enabledTools. Future families
208
+ // (Replicate, Runway, Suno) drop in here.
209
+ export const TOOL_FAMILY: Record<string, string> = {
210
+ higgsfield_status: 'higgsfield',
211
+ higgsfield_list: 'higgsfield',
212
+ higgsfield_models: 'higgsfield',
213
+ higgsfield_generate: 'higgsfield',
214
+ };
215
+
216
+ export const ALL_TOOL_NAMES: string[] = TOOL_META.map(t => t.name);
217
+
218
+ export function defaultEnabledTools(): Record<string, boolean> {
219
+ const out: Record<string, boolean> = {};
220
+ for (const t of TOOL_META) out[t.name] = t.defaultEnabled;
221
+ return out;
222
+ }
223
+
224
+ export function groupOf(name: string): ToolGroup | undefined {
225
+ return TOOL_META.find(t => t.name === name)?.group;
226
+ }
227
+
228
+ export function describe(name: string): string {
229
+ return TOOL_META.find(t => t.name === name)?.blurb || '';
230
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Bridge update check — alert-only, never auto-installs.
3
+ *
4
+ * On daemon launch, hits the GitHub releases API for `empir3hq/empir3-bridge`,
5
+ * compares the latest tag to package.json's version, and prints a yellow
6
+ * upgrade banner if a newer version exists.
7
+ *
8
+ * Cached in ~/.empir3-bridge/state.json for 24h so we don't hammer the API.
9
+ * Network failures are silent — never block startup.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const https = require('https');
16
+
17
+ const REPO = 'empir3hq/empir3-bridge';
18
+ const STATE_DIR = path.join(os.homedir(), '.empir3-bridge');
19
+ const STATE_FILE = path.join(STATE_DIR, 'state.json');
20
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
21
+ const FETCH_TIMEOUT_MS = 3000;
22
+
23
+ function readPackageVersion() {
24
+ try {
25
+ const pkg = JSON.parse(
26
+ fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8')
27
+ );
28
+ return pkg.version;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ function readState() {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+
42
+ function writeState(state) {
43
+ try {
44
+ if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
45
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
46
+ } catch {}
47
+ }
48
+
49
+ function fetchLatestRelease() {
50
+ return new Promise((resolve) => {
51
+ const opts = {
52
+ hostname: 'api.github.com',
53
+ path: `/repos/${REPO}/releases/latest`,
54
+ headers: {
55
+ 'User-Agent': 'empir3-bridge-update-check',
56
+ 'Accept': 'application/vnd.github+json',
57
+ },
58
+ timeout: FETCH_TIMEOUT_MS,
59
+ };
60
+ const req = https.get(opts, (res) => {
61
+ if (res.statusCode !== 200) {
62
+ res.resume();
63
+ return resolve(null);
64
+ }
65
+ let data = '';
66
+ res.on('data', (c) => (data += c));
67
+ res.on('end', () => {
68
+ try {
69
+ const json = JSON.parse(data);
70
+ resolve(json.tag_name || null);
71
+ } catch {
72
+ resolve(null);
73
+ }
74
+ });
75
+ });
76
+ req.on('error', () => resolve(null));
77
+ req.on('timeout', () => {
78
+ req.destroy();
79
+ resolve(null);
80
+ });
81
+ });
82
+ }
83
+
84
+ // Strip leading "v" and split, return [major, minor, patch] or null on parse failure.
85
+ function parseVersion(v) {
86
+ if (!v) return null;
87
+ const clean = String(v).replace(/^v/, '').split('-')[0]; // drop pre-release suffix
88
+ const parts = clean.split('.').map((n) => parseInt(n, 10));
89
+ if (parts.length !== 3 || parts.some((n) => Number.isNaN(n))) return null;
90
+ return parts;
91
+ }
92
+
93
+ // Returns true if `a` is strictly newer than `b`. Both must be parseable.
94
+ function isNewer(a, b) {
95
+ const pa = parseVersion(a);
96
+ const pb = parseVersion(b);
97
+ if (!pa || !pb) return false;
98
+ for (let i = 0; i < 3; i++) {
99
+ if (pa[i] > pb[i]) return true;
100
+ if (pa[i] < pb[i]) return false;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ async function checkForUpdate() {
106
+ const current = readPackageVersion();
107
+ if (!current) return; // can't check without a known current version
108
+
109
+ const state = readState();
110
+ const cached = state.updateCheck;
111
+ const now = Date.now();
112
+
113
+ let latestTag;
114
+ if (cached && cached.checkedAt && now - cached.checkedAt < CACHE_TTL_MS) {
115
+ latestTag = cached.latestTag;
116
+ } else {
117
+ latestTag = await fetchLatestRelease();
118
+ if (latestTag !== null) {
119
+ state.updateCheck = { checkedAt: now, latestTag };
120
+ writeState(state);
121
+ }
122
+ }
123
+
124
+ if (latestTag && isNewer(latestTag, current)) {
125
+ const reset = '\x1b[0m';
126
+ const yellow = '\x1b[33m';
127
+ const bold = '\x1b[1m';
128
+ console.log('');
129
+ console.log(` ${yellow}${bold}⚠ Update available${reset}${yellow}: bridge ${latestTag} (you're on v${current})${reset}`);
130
+ console.log(` ${yellow} Upgrade: cd into your bridge repo, then:${reset}`);
131
+ console.log(` ${yellow} git pull && npm install${reset}`);
132
+ console.log('');
133
+ }
134
+ }
135
+
136
+ module.exports = { checkForUpdate, isNewer, parseVersion };
package/tray/build.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ PyInstaller build for the Empir3 tray wrapper.
3
+
4
+ Produces: bridge/build/dist/Empir3Tray.exe (Windows-only for now)
5
+
6
+ Run:
7
+ cd bridge/tray
8
+ pip install -r requirements.txt pyinstaller
9
+ python build.py
10
+
11
+ The output exe is dropped beside Empir3Setup.exe in the install dir. At
12
+ runtime the tray spawns `Empir3Setup.exe --daemon-real` from its own dir.
13
+
14
+ Single-file (--onefile) so the bootstrapper payload only needs to extract one
15
+ artifact. --windowed so no console flashes when Windows autostart launches it.
16
+ """
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ HERE = Path(__file__).resolve().parent
23
+ TRAY_PY = HERE / 'tray.py'
24
+ DIST_DIR = HERE.parent / 'build' / 'dist'
25
+ WORK_DIR = HERE.parent / 'build' / 'pyinstaller-work'
26
+ SPEC_DIR = HERE.parent / 'build' / 'pyinstaller-spec'
27
+
28
+
29
+ def main():
30
+ if sys.platform != 'win32':
31
+ print('[tray-build] WARN: only tested on Windows; macOS support deferred.')
32
+
33
+ if not TRAY_PY.exists():
34
+ print(f'[tray-build] FATAL: {TRAY_PY} not found')
35
+ sys.exit(1)
36
+
37
+ # Clean prior build artifacts so the binary stamp is reproducible.
38
+ for d in (WORK_DIR, SPEC_DIR):
39
+ if d.exists():
40
+ shutil.rmtree(d, ignore_errors=True)
41
+ DIST_DIR.mkdir(parents=True, exist_ok=True)
42
+
43
+ args = [
44
+ sys.executable, '-m', 'PyInstaller',
45
+ '--noconfirm',
46
+ '--clean',
47
+ '--onefile',
48
+ '--windowed', # no console window
49
+ '--name', 'Empir3Tray',
50
+ '--distpath', str(DIST_DIR),
51
+ '--workpath', str(WORK_DIR),
52
+ '--specpath', str(SPEC_DIR),
53
+ # pystray ships a Windows backend that PyInstaller doesn't always
54
+ # auto-discover — pin it explicitly.
55
+ '--hidden-import', 'pystray._win32',
56
+ '--hidden-import', 'PIL._tkinter_finder',
57
+ str(TRAY_PY),
58
+ ]
59
+
60
+ print('[tray-build]', ' '.join(args))
61
+ r = subprocess.run(args, check=False)
62
+ if r.returncode != 0:
63
+ print(f'[tray-build] FAILED (exit {r.returncode})')
64
+ sys.exit(r.returncode)
65
+
66
+ out = DIST_DIR / ('Empir3Tray.exe' if sys.platform == 'win32' else 'Empir3Tray')
67
+ if not out.exists():
68
+ print(f'[tray-build] FAILED: expected output {out} not found')
69
+ sys.exit(1)
70
+
71
+ size_mb = out.stat().st_size / (1024 * 1024)
72
+ print(f'[tray-build] OK: {out} ({size_mb:.1f} MB)')
73
+
74
+
75
+ if __name__ == '__main__':
76
+ main()
@@ -0,0 +1,2 @@
1
+ pystray>=0.19.5
2
+ Pillow>=10.0