@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,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Empir3 Bridge — payload entry point.
|
|
4
|
+
*
|
|
5
|
+
* This file becomes `entry.js` inside the signed payload tarball that the
|
|
6
|
+
* bootstrapper extracts to `~/.empir3-bridge/payload/<version>/`. The
|
|
7
|
+
* bootstrapper calls our exported `start(argv)` and forwards the user's
|
|
8
|
+
* CLI args.
|
|
9
|
+
*
|
|
10
|
+
* This is everything that USED to live in `bridge/build/entry.js` minus
|
|
11
|
+
* the bootstrap-only concerns (network update, signature verification,
|
|
12
|
+
* payload cache layout). Asset paths now point at sibling files inside
|
|
13
|
+
* the extracted payload dir instead of being SEA-extracted.
|
|
14
|
+
*
|
|
15
|
+
* Subcommands (all forwarded from Empir3Setup.exe argv):
|
|
16
|
+
* <none> → First-run UX. Spawns the tray wrapper,
|
|
17
|
+
* which spawns the daemon. Daemon detects
|
|
18
|
+
* no auth file and serves the two-button
|
|
19
|
+
* splash at /welcome inside the bridge's
|
|
20
|
+
* own Chrome. (Replaces the Koba chat
|
|
21
|
+
* installer that used to live here.)
|
|
22
|
+
* --daemon → Spawn the tray wrapper (Empir3Tray.exe);
|
|
23
|
+
* tray supervises the actual daemon. This
|
|
24
|
+
* is the autostart target, so users boot
|
|
25
|
+
* into a tray icon, not a hidden process.
|
|
26
|
+
* --daemon-real → Run the bridge daemon directly, no tray.
|
|
27
|
+
* Used by the tray itself when it spawns
|
|
28
|
+
* the bootstrapper as a child.
|
|
29
|
+
* --uninstall → Reverse all the things we wrote
|
|
30
|
+
* --version | -v → Print SHIPPED version (payload version)
|
|
31
|
+
* --help | -h → Print usage
|
|
32
|
+
*
|
|
33
|
+
* Exit codes:
|
|
34
|
+
* 0 normal exit
|
|
35
|
+
* 1 fatal error inside the payload (also when the daemon dies)
|
|
36
|
+
* 69 unavailable (used by bootstrap, not us)
|
|
37
|
+
* 70 internal contract error (e.g. missing entry export — bootstrap)
|
|
38
|
+
*/
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const os = require('os');
|
|
41
|
+
const path = require('path');
|
|
42
|
+
const { spawn, spawnSync } = require('child_process');
|
|
43
|
+
|
|
44
|
+
// Inside the extracted payload dir we have:
|
|
45
|
+
// entry.js ← this file (renamed from payload-entry.js)
|
|
46
|
+
// bundle-daemon.js ← bundled bridge/index.js + handlers + ws
|
|
47
|
+
// bundle-installer.js ← bundled bridge/installer/server.js
|
|
48
|
+
// installer-ui/<files> ← Koba chat UI static files
|
|
49
|
+
// .payload-version ← plain text version stamp
|
|
50
|
+
const PAYLOAD_DIR = process.env.EMPIR3_BRIDGE_PAYLOAD_DIR
|
|
51
|
+
|| __dirname; // dev fallback when running directly via node payload-entry.js
|
|
52
|
+
|
|
53
|
+
const VERSION_FILE = path.join(PAYLOAD_DIR, '.payload-version');
|
|
54
|
+
function readVersion() {
|
|
55
|
+
try { return fs.readFileSync(VERSION_FILE, 'utf8').trim(); } catch {}
|
|
56
|
+
return process.env.EMPIR3_BRIDGE_PAYLOAD_VERSION || 'dev';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Legacy bundled-extension ID. The extension is no longer shipped or loaded
|
|
60
|
+
// (the overlay rides the CDP mailbox instead), but this ID is retained solely
|
|
61
|
+
// to scrub the old ExtensionInstallForcelist policy below from installs that
|
|
62
|
+
// received it — see the migration further down.
|
|
63
|
+
const EXTENSION_ID = 'gbigofjjgcpjkffhlfepjdglabhngeii';
|
|
64
|
+
|
|
65
|
+
// ── Asset paths surfaced to bundled daemon + installer ──────────────
|
|
66
|
+
|
|
67
|
+
function configureRuntimePaths() {
|
|
68
|
+
const uiDir = path.join(PAYLOAD_DIR, 'installer-ui');
|
|
69
|
+
process.env.EMPIR3_BRIDGE_INSTALLER_UI_DIR = uiDir;
|
|
70
|
+
process.env.EMPIR3_BRIDGE_PAYLOAD_VERSION = readVersion();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const AUTOSTART_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
|
|
74
|
+
const AUTOSTART_VALUE_NAME = 'Empir3Bridge';
|
|
75
|
+
const APPDATA_DIR = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Empir3');
|
|
76
|
+
const STABLE_BOOTSTRAP_EXE = path.join(APPDATA_DIR, 'Empir3Setup.exe');
|
|
77
|
+
const BOOTSTRAP_POINTER_FILE = path.join(APPDATA_DIR, 'bridge-bootstrap.json');
|
|
78
|
+
|
|
79
|
+
function appendBridgeLog(message) {
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(APPDATA_DIR, { recursive: true });
|
|
82
|
+
fs.appendFileSync(
|
|
83
|
+
path.join(APPDATA_DIR, 'bridge.log'),
|
|
84
|
+
`${new Date().toISOString()} [empir3-entry] ${message}\n`,
|
|
85
|
+
'utf8',
|
|
86
|
+
);
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function psQuote(value) {
|
|
91
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function copyBootstrapToStablePath() {
|
|
95
|
+
if (process.platform !== 'win32') return process.execPath;
|
|
96
|
+
if (!process.env.EMPIR3_BRIDGE_BOOTSTRAP_VERSION) return process.execPath;
|
|
97
|
+
|
|
98
|
+
// Go bootstrapper path: the stub has ALREADY reconciled stable-vs-running per
|
|
99
|
+
// the design decision table (including the locked-stale case where the
|
|
100
|
+
// authoritative exe is the running NEWER stub, not the older stable) and set
|
|
101
|
+
// EMPIR3_BOOTSTRAP_EXE to the result. That value is guaranteed >= the running
|
|
102
|
+
// stub, so we TRUST it verbatim — never copy, never fall back to an older
|
|
103
|
+
// stable exe (doing so could re-register the older one, which the design
|
|
104
|
+
// forbids). We only refresh the pointer to match.
|
|
105
|
+
const authoritative = (process.env.EMPIR3_BOOTSTRAP_EXE || '').trim();
|
|
106
|
+
if (authoritative) {
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(APPDATA_DIR, { recursive: true });
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
BOOTSTRAP_POINTER_FILE,
|
|
111
|
+
JSON.stringify({ bootstrapPath: authoritative, sourcePath: authoritative, updatedAt: new Date().toISOString() }, null, 2),
|
|
112
|
+
'utf8',
|
|
113
|
+
);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error('[empir3-bridge] stable bootstrap pointer refresh failed:', e.message);
|
|
116
|
+
}
|
|
117
|
+
return authoritative;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Legacy Node-SEA path: EMPIR3_BOOTSTRAP_EXE is unset, so the running SEA exe
|
|
121
|
+
// (process.execPath) IS the product — copy it to the stable path.
|
|
122
|
+
const source = process.execPath;
|
|
123
|
+
const target = STABLE_BOOTSTRAP_EXE;
|
|
124
|
+
try {
|
|
125
|
+
fs.mkdirSync(APPDATA_DIR, { recursive: true });
|
|
126
|
+
if (source.toLowerCase() !== target.toLowerCase()) {
|
|
127
|
+
let shouldCopy = true;
|
|
128
|
+
try {
|
|
129
|
+
const srcStat = fs.statSync(source);
|
|
130
|
+
const dstStat = fs.statSync(target);
|
|
131
|
+
shouldCopy = srcStat.size !== dstStat.size;
|
|
132
|
+
} catch {}
|
|
133
|
+
if (shouldCopy) {
|
|
134
|
+
fs.copyFileSync(source, target);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
fs.writeFileSync(
|
|
138
|
+
BOOTSTRAP_POINTER_FILE,
|
|
139
|
+
JSON.stringify({ bootstrapPath: target, sourcePath: source, updatedAt: new Date().toISOString() }, null, 2),
|
|
140
|
+
'utf8',
|
|
141
|
+
);
|
|
142
|
+
return target;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error('[empir3-bridge] stable bootstrap write failed:', e.message);
|
|
145
|
+
return fs.existsSync(target) ? target : source;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function bootstrapExeForRegistration() {
|
|
150
|
+
return copyBootstrapToStablePath();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Per-user Start Menu folder. Windows resolves the env var to e.g.
|
|
154
|
+
// C:\Users\<u>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs.
|
|
155
|
+
function startMenuShortcutPath() {
|
|
156
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
157
|
+
return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Empir3', 'Empir3.lnk');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Idempotent Start Menu shortcut. Points at the running bootstrapper exe
|
|
161
|
+
// with --daemon so clicking it boots the tray (which then supervises the
|
|
162
|
+
// real bridge daemon as a child). PowerShell's WScript.Shell ComObject is
|
|
163
|
+
// the simplest way to write a real .lnk from Node without bundling a
|
|
164
|
+
// shortcut library — every Win10/11 install ships with PowerShell.
|
|
165
|
+
function registerStartMenuShortcut() {
|
|
166
|
+
if (process.platform !== 'win32') return;
|
|
167
|
+
// Same gate as autostart — only register when running from the SEA
|
|
168
|
+
// bootstrapper, so plain `node payload-entry.js` dev runs don't pollute
|
|
169
|
+
// the user's Start Menu.
|
|
170
|
+
const fromBootstrap = !!process.env.EMPIR3_BRIDGE_BOOTSTRAP_VERSION;
|
|
171
|
+
if (!fromBootstrap) return;
|
|
172
|
+
try {
|
|
173
|
+
const exe = bootstrapExeForRegistration();
|
|
174
|
+
const lnk = startMenuShortcutPath();
|
|
175
|
+
fs.mkdirSync(path.dirname(lnk), { recursive: true });
|
|
176
|
+
|
|
177
|
+
// Skip if a shortcut already exists pointing at the same exe — no-op
|
|
178
|
+
// on every subsequent boot. Compare by reading TargetPath via the same
|
|
179
|
+
// ComObject; if the user moved Empir3Setup.exe we re-write to the new
|
|
180
|
+
// path automatically.
|
|
181
|
+
const probe = spawnSync('powershell', ['-NoProfile', '-Command',
|
|
182
|
+
`try { $ws=New-Object -ComObject WScript.Shell; $s=$ws.CreateShortcut('${lnk.replace(/'/g, "''")}'); Write-Output $s.TargetPath; Write-Output $s.Arguments } catch {}`,
|
|
183
|
+
], { encoding: 'utf8' });
|
|
184
|
+
const probeLines = probe.stdout ? probe.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
185
|
+
if (probe.status === 0 && probeLines[0] === exe && (probeLines[1] || '') === '--daemon') return;
|
|
186
|
+
|
|
187
|
+
const ps = `
|
|
188
|
+
$ws = New-Object -ComObject WScript.Shell
|
|
189
|
+
$s = $ws.CreateShortcut('${lnk.replace(/'/g, "''")}')
|
|
190
|
+
$s.TargetPath = '${exe.replace(/'/g, "''")}'
|
|
191
|
+
$s.Arguments = '--daemon'
|
|
192
|
+
$s.IconLocation = '${exe.replace(/'/g, "''")},0'
|
|
193
|
+
$s.Description = 'Empir3 Bridge — desktop companion'
|
|
194
|
+
$s.WorkingDirectory = '${path.dirname(exe).replace(/'/g, "''")}'
|
|
195
|
+
$s.Save()
|
|
196
|
+
`.trim();
|
|
197
|
+
const w = spawnSync('powershell', ['-NoProfile', '-Command', ps], { encoding: 'utf8' });
|
|
198
|
+
if (w.status === 0) {
|
|
199
|
+
console.log(`[empir3-bridge] Start Menu shortcut written: ${lnk}`);
|
|
200
|
+
} else {
|
|
201
|
+
console.error('[empir3-bridge] Start Menu shortcut write failed:', (w.stderr || w.stdout || '').trim());
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error('[empir3-bridge] Start Menu shortcut threw:', e.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Register Empir3Setup.exe --daemon (the BOOTSTRAPPER, not the payload —
|
|
209
|
+
// process.execPath here is the SEA exe) to launch automatically on
|
|
210
|
+
// Windows login. Idempotent: writes only when missing or pointing
|
|
211
|
+
// somewhere else.
|
|
212
|
+
function registerAutostart() {
|
|
213
|
+
if (process.platform !== 'win32') return;
|
|
214
|
+
// Only register autostart when running inside the SEA-packaged bootstrapper.
|
|
215
|
+
// In dev runs (plain node payload-entry.js) we don't want to install autostart.
|
|
216
|
+
const fromBootstrap = !!process.env.EMPIR3_BRIDGE_BOOTSTRAP_VERSION;
|
|
217
|
+
if (!fromBootstrap) return;
|
|
218
|
+
try {
|
|
219
|
+
const exe = bootstrapExeForRegistration();
|
|
220
|
+
const desired = `"${exe}" --daemon`;
|
|
221
|
+
|
|
222
|
+
const q = spawnSync('reg', ['query', AUTOSTART_KEY, '/v', AUTOSTART_VALUE_NAME], { encoding: 'utf8' });
|
|
223
|
+
if (q.status === 0 && q.stdout) {
|
|
224
|
+
const m = q.stdout.match(/REG_SZ\s+(.+?)\s*$/m);
|
|
225
|
+
if (m && m[1].trim() === desired) return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const w = spawnSync('reg', [
|
|
229
|
+
'add', AUTOSTART_KEY,
|
|
230
|
+
'/v', AUTOSTART_VALUE_NAME,
|
|
231
|
+
'/t', 'REG_SZ',
|
|
232
|
+
'/d', desired,
|
|
233
|
+
'/f',
|
|
234
|
+
], { encoding: 'utf8' });
|
|
235
|
+
if (w.status === 0) {
|
|
236
|
+
console.log('[empir3-bridge] autostart registered: bridge will launch on Windows login');
|
|
237
|
+
} else {
|
|
238
|
+
console.error('[empir3-bridge] autostart write failed:', (w.stderr || w.stdout || '').trim());
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error('[empir3-bridge] autostart write threw:', e.message);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// RETIRED: HKCU\Software\Policies\Google\Chrome\ExtensionInstallForcelist.
|
|
246
|
+
//
|
|
247
|
+
// The HKCU policy was a privacy violation — it force-installed the bridge
|
|
248
|
+
// extension into the USER'S MAIN Chrome (every profile, every window), not
|
|
249
|
+
// just the bridge's isolated profile. Per the load-bearing rule (memory
|
|
250
|
+
// `feedback_bridge_privacy_isolated_profile`): Vincent never touches the
|
|
251
|
+
// user's normal Chrome.
|
|
252
|
+
//
|
|
253
|
+
// Replacement: the bridge no longer ships or loads any Chrome extension at
|
|
254
|
+
// all — the in-page overlay rides the bridge's own CDP connection (a mailbox:
|
|
255
|
+
// push via window.__empir3_inbox, drain window.__empir3_outbox), which works
|
|
256
|
+
// on every page including https. The user's main Chrome stays untouched.
|
|
257
|
+
//
|
|
258
|
+
// MIGRATION CLEANUP: every --daemon boot scrubs the old HKCU entry so
|
|
259
|
+
// upgrades from <=v0.1.3 self-heal. Removing on every boot is cheap (one
|
|
260
|
+
// reg query + one reg delete on hit) and idempotent.
|
|
261
|
+
function scrubLegacyForceInstallPolicy() {
|
|
262
|
+
if (process.platform !== 'win32') return;
|
|
263
|
+
try {
|
|
264
|
+
const REG_KEY = 'HKCU\\Software\\Policies\\Google\\Chrome\\ExtensionInstallForcelist';
|
|
265
|
+
const q = spawnSync('reg', ['query', REG_KEY], { encoding: 'utf8' });
|
|
266
|
+
if (q.status !== 0 || !q.stdout) return; // key doesn't exist, nothing to do
|
|
267
|
+
|
|
268
|
+
const re = /\s+(\S+)\s+REG_SZ\s+(.+?)\s*$/gm;
|
|
269
|
+
let m;
|
|
270
|
+
let removed = 0;
|
|
271
|
+
while ((m = re.exec(q.stdout)) !== null) {
|
|
272
|
+
const [, slot, data] = m;
|
|
273
|
+
if (data.startsWith(EXTENSION_ID + ';')) {
|
|
274
|
+
const d = spawnSync('reg', ['delete', REG_KEY, '/v', slot, '/f'], { stdio: 'ignore' });
|
|
275
|
+
if (d.status === 0) removed++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (removed > 0) {
|
|
279
|
+
console.log(`[empir3-bridge] migration: scrubbed ${removed} legacy ExtensionInstallForcelist entr${removed === 1 ? 'y' : 'ies'} (extension no longer leaks into user's main Chrome)`);
|
|
280
|
+
// If the parent key is now empty, remove it too so Chrome stops
|
|
281
|
+
// showing "Managed by your organization" in the menu.
|
|
282
|
+
const q2 = spawnSync('reg', ['query', REG_KEY], { encoding: 'utf8' });
|
|
283
|
+
if (q2.status === 0 && !/\s+\d+\s+REG_SZ/.test(q2.stdout || '')) {
|
|
284
|
+
spawnSync('reg', ['delete', REG_KEY, '/f'], { stdio: 'ignore' });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.error('[empir3-bridge] policy scrub failed:', e.message);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Uninstall ──────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
async function uninstall() {
|
|
295
|
+
console.log('[empir3-bridge] uninstalling…');
|
|
296
|
+
let steps = 0;
|
|
297
|
+
|
|
298
|
+
// 1a. Kill the tray wrapper by image name. The tray supervises the daemon
|
|
299
|
+
// and would otherwise immediately respawn anything we kill on a port.
|
|
300
|
+
// Done first so step 1b's daemon kills stay dead.
|
|
301
|
+
if (process.platform === 'win32') {
|
|
302
|
+
const tk = spawnSync('taskkill', ['/F', '/IM', 'Empir3Tray.exe'], { encoding: 'utf8' });
|
|
303
|
+
if (tk.status === 0) {
|
|
304
|
+
console.log(' killed Empir3Tray.exe');
|
|
305
|
+
steps++;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 1b. Stop any running daemon (kill anything listening on 3006-3306).
|
|
310
|
+
if (process.platform === 'win32') {
|
|
311
|
+
const ports = [3006, 3106, 3206, 3306];
|
|
312
|
+
const ns = spawnSync('netstat', ['-ano'], { encoding: 'utf8' });
|
|
313
|
+
if (ns.stdout) {
|
|
314
|
+
const seen = new Set();
|
|
315
|
+
for (const port of ports) {
|
|
316
|
+
const re = new RegExp(`LISTENING\\s+(\\d+)\\s*$`);
|
|
317
|
+
for (const line of ns.stdout.split('\n')) {
|
|
318
|
+
if (line.includes(`127.0.0.1:${port}`) || line.includes(`0.0.0.0:${port}`)) {
|
|
319
|
+
const m = line.match(re);
|
|
320
|
+
if (m) seen.add(m[1]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const pid of seen) {
|
|
325
|
+
if (pid === String(process.pid)) continue;
|
|
326
|
+
spawnSync('taskkill', ['/F', '/PID', pid], { stdio: 'ignore' });
|
|
327
|
+
console.log(' killed bridge daemon pid', pid);
|
|
328
|
+
steps++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 2. Remove HKCU Chrome ExtensionInstallForcelist policy.
|
|
334
|
+
if (process.platform === 'win32') {
|
|
335
|
+
const REG_KEY = 'HKCU\\Software\\Policies\\Google\\Chrome\\ExtensionInstallForcelist';
|
|
336
|
+
const q = spawnSync('reg', ['query', REG_KEY], { encoding: 'utf8' });
|
|
337
|
+
if (q.status === 0 && q.stdout) {
|
|
338
|
+
const re = /\s+(\S+)\s+REG_SZ\s+(.+?)\s*$/gm;
|
|
339
|
+
let m;
|
|
340
|
+
while ((m = re.exec(q.stdout)) !== null) {
|
|
341
|
+
const [, slot, data] = m;
|
|
342
|
+
if (data.startsWith(EXTENSION_ID + ';')) {
|
|
343
|
+
spawnSync('reg', ['delete', REG_KEY, '/v', slot, '/f'], { stdio: 'ignore' });
|
|
344
|
+
console.log(` removed Chrome force-install policy (slot ${slot})`);
|
|
345
|
+
steps++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const q2 = spawnSync('reg', ['query', REG_KEY], { encoding: 'utf8' });
|
|
349
|
+
if (q2.status === 0 && !/\s+\d+\s+REG_SZ/.test(q2.stdout || '')) {
|
|
350
|
+
spawnSync('reg', ['delete', REG_KEY, '/f'], { stdio: 'ignore' });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 3. Remove autostart entry.
|
|
356
|
+
if (process.platform === 'win32') {
|
|
357
|
+
const q = spawnSync('reg', ['query', AUTOSTART_KEY, '/v', AUTOSTART_VALUE_NAME], { encoding: 'utf8' });
|
|
358
|
+
if (q.status === 0) {
|
|
359
|
+
spawnSync('reg', ['delete', AUTOSTART_KEY, '/v', AUTOSTART_VALUE_NAME, '/f'], { stdio: 'ignore' });
|
|
360
|
+
console.log(' removed Windows autostart');
|
|
361
|
+
steps++;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 4. Remove cached payload tree (every version).
|
|
366
|
+
try {
|
|
367
|
+
const payloadRoot = path.join(os.homedir(), '.empir3-bridge');
|
|
368
|
+
fs.rmSync(payloadRoot, { recursive: true, force: true });
|
|
369
|
+
console.log(' cleared cached payloads + extracted runtime files');
|
|
370
|
+
steps++;
|
|
371
|
+
} catch {}
|
|
372
|
+
|
|
373
|
+
// 5. Remove the bridge's dedicated Chrome profile (if it was used).
|
|
374
|
+
try {
|
|
375
|
+
const bridgeProfile = path.join(os.homedir(), '.empir3', 'bridge-chrome-profile');
|
|
376
|
+
if (fs.existsSync(bridgeProfile)) {
|
|
377
|
+
fs.rmSync(bridgeProfile, { recursive: true, force: true });
|
|
378
|
+
console.log(' cleared bridge Chrome profile');
|
|
379
|
+
steps++;
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
|
|
383
|
+
// 6. Remove %APPDATA%/Empir3/ — auth token, settings, daemon log, tray
|
|
384
|
+
// log. This was previously kept on the theory the user might re-install
|
|
385
|
+
// and want to skip re-pairing, but a "full uninstall" should leave the
|
|
386
|
+
// machine clean. They can re-pair in 6 seconds via the welcome page if
|
|
387
|
+
// they reinstall.
|
|
388
|
+
if (process.platform === 'win32') {
|
|
389
|
+
try {
|
|
390
|
+
const appdataDir = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Empir3');
|
|
391
|
+
if (fs.existsSync(appdataDir)) {
|
|
392
|
+
fs.rmSync(appdataDir, { recursive: true, force: true });
|
|
393
|
+
console.log(' cleared %APPDATA%/Empir3 (auth, settings, logs)');
|
|
394
|
+
steps++;
|
|
395
|
+
}
|
|
396
|
+
} catch (e) {
|
|
397
|
+
console.warn(' could not clear %APPDATA%/Empir3:', e.message);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 7. Remove Start Menu shortcut + parent folder (if empty).
|
|
402
|
+
if (process.platform === 'win32') {
|
|
403
|
+
try {
|
|
404
|
+
const lnk = startMenuShortcutPath();
|
|
405
|
+
if (fs.existsSync(lnk)) {
|
|
406
|
+
fs.rmSync(lnk, { force: true });
|
|
407
|
+
console.log(' removed Start Menu shortcut');
|
|
408
|
+
steps++;
|
|
409
|
+
}
|
|
410
|
+
const folder = path.dirname(lnk);
|
|
411
|
+
if (fs.existsSync(folder) && fs.readdirSync(folder).length === 0) {
|
|
412
|
+
fs.rmdirSync(folder);
|
|
413
|
+
}
|
|
414
|
+
} catch {}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(`[empir3-bridge] uninstall complete (${steps} steps).`);
|
|
418
|
+
console.log('Note: the bootstrapper exe (Empir3Setup.exe) you ran can be deleted manually.');
|
|
419
|
+
console.log('Note: the extension may persist in your Chrome until you reopen Chrome (which will see the policy is gone and remove it).');
|
|
420
|
+
|
|
421
|
+
// 8. Reassure the user it's actually done. The tray was killed in step 1a,
|
|
422
|
+
// so this native dialog is the ONLY "uninstall complete" signal they get —
|
|
423
|
+
// without it the tray just vanishes and nothing confirms success. Best
|
|
424
|
+
// effort: a popup failure must never mask a successful uninstall. Body is
|
|
425
|
+
// base64'd into the PowerShell command to sidestep all quoting/newline
|
|
426
|
+
// escaping; -STA is required for WinForms MessageBox.
|
|
427
|
+
if (process.platform === 'win32') {
|
|
428
|
+
try {
|
|
429
|
+
const body =
|
|
430
|
+
'Empir3 Bridge has been uninstalled.\n\n' +
|
|
431
|
+
`${steps} item(s) were removed. You can delete Empir3Setup.exe whenever you like.\n\n` +
|
|
432
|
+
'If Chrome is open, the helper extension disappears the next time you restart it.';
|
|
433
|
+
const b64 = Buffer.from(body, 'utf8').toString('base64');
|
|
434
|
+
const ps =
|
|
435
|
+
'Add-Type -AssemblyName System.Windows.Forms | Out-Null; ' +
|
|
436
|
+
'[System.Windows.Forms.MessageBox]::Show(' +
|
|
437
|
+
`[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64}')),` +
|
|
438
|
+
"'Empir3 Bridge','OK','Information') | Out-Null";
|
|
439
|
+
spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-STA', '-Command', ps],
|
|
440
|
+
{ stdio: 'ignore', windowsHide: true });
|
|
441
|
+
} catch {}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Subcommand dispatch ─────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
function printHelp() {
|
|
448
|
+
const v = readVersion();
|
|
449
|
+
console.log(`Empir3 Bridge ${v}`);
|
|
450
|
+
console.log('');
|
|
451
|
+
console.log('Usage:');
|
|
452
|
+
console.log(' Empir3Setup.exe Install + start the bridge (first-time setup).');
|
|
453
|
+
console.log(' Empir3Setup.exe --daemon Run the bridge daemon headless (used by autostart).');
|
|
454
|
+
console.log(' Empir3Setup.exe --uninstall Remove all bridge components from this machine.');
|
|
455
|
+
console.log(' Empir3Setup.exe --version Print version and exit.');
|
|
456
|
+
console.log('');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Locate Empir3Tray.exe inside the extracted payload. Returns null if the
|
|
460
|
+
// payload predates the tray bundle (older payloads still run --daemon
|
|
461
|
+
// directly via the no-tray fallback).
|
|
462
|
+
function findTrayExe() {
|
|
463
|
+
if (process.platform !== 'win32') return null;
|
|
464
|
+
const candidates = [
|
|
465
|
+
path.join(PAYLOAD_DIR, 'Empir3Tray.exe'),
|
|
466
|
+
// Dev fallback when running entry.js directly without an extracted payload.
|
|
467
|
+
path.join(__dirname, '..', 'tray', 'Empir3Tray.exe'),
|
|
468
|
+
];
|
|
469
|
+
for (const p of candidates) {
|
|
470
|
+
if (fs.existsSync(p)) return p;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Spawn the tray wrapper and exit. The tray takes ownership of the daemon
|
|
476
|
+
// child process and gives the user a menu/icon. Pass the bootstrapper exe
|
|
477
|
+
// path through the env so the tray can spawn `--daemon-real` reliably.
|
|
478
|
+
async function spawnTrayAndExit() {
|
|
479
|
+
const trayExe = findTrayExe();
|
|
480
|
+
if (!trayExe) {
|
|
481
|
+
// No tray exe. Under the Go bootstrapper the --daemon node has NO
|
|
482
|
+
// kill-on-close job, so running the daemon IN-PROCESS here would orphan it
|
|
483
|
+
// if the stub is killed. Instead relaunch the bootstrap exe as
|
|
484
|
+
// `--daemon-real`, which the Go stub supervises with its own kill-on-close
|
|
485
|
+
// job (no orphan). Only do this when we have a genuine bootstrap exe; dev /
|
|
486
|
+
// legacy runs (no EMPIR3_BOOTSTRAP_EXE) fall back to in-process.
|
|
487
|
+
const exe = (process.env.EMPIR3_BOOTSTRAP_EXE || '').trim();
|
|
488
|
+
if (exe && process.platform === 'win32') {
|
|
489
|
+
console.error(`[empir3-bridge] tray exe not found; relaunching ${exe} --daemon-real (supervised fallback)`);
|
|
490
|
+
const child = spawn(exe, ['--daemon-real'], {
|
|
491
|
+
detached: true,
|
|
492
|
+
stdio: 'ignore',
|
|
493
|
+
env: { ...process.env, EMPIR3_BOOTSTRAP_EXE: exe },
|
|
494
|
+
});
|
|
495
|
+
child.unref();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
console.error('[empir3-bridge] tray exe not found; running daemon headless as fallback (dev)');
|
|
499
|
+
await require('./bundle-daemon.js').start();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const bootstrapExe = bootstrapExeForRegistration();
|
|
504
|
+
const trayEnv = {
|
|
505
|
+
...process.env,
|
|
506
|
+
EMPIR3_BOOTSTRAP_EXE: bootstrapExe,
|
|
507
|
+
EMPIR3_BRIDGE_PAYLOAD_DIR: PAYLOAD_DIR,
|
|
508
|
+
EMPIR3_BRIDGE_PAYLOAD_VERSION: readVersion(),
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
console.log(`[empir3-bridge] spawning tray: ${trayExe}`);
|
|
512
|
+
appendBridgeLog(`spawning tray via node: ${trayExe}`);
|
|
513
|
+
const child = spawn(trayExe, [], {
|
|
514
|
+
detached: true,
|
|
515
|
+
stdio: 'ignore',
|
|
516
|
+
cwd: path.dirname(trayExe),
|
|
517
|
+
env: trayEnv,
|
|
518
|
+
windowsHide: true,
|
|
519
|
+
});
|
|
520
|
+
const spawned = await new Promise((resolve) => {
|
|
521
|
+
let settled = false;
|
|
522
|
+
const done = (ok) => {
|
|
523
|
+
if (settled) return;
|
|
524
|
+
settled = true;
|
|
525
|
+
resolve(ok);
|
|
526
|
+
};
|
|
527
|
+
child.once('spawn', () => {
|
|
528
|
+
appendBridgeLog(`node tray spawn returned pid=${child.pid || 0}`);
|
|
529
|
+
done(true);
|
|
530
|
+
});
|
|
531
|
+
child.once('error', (e) => {
|
|
532
|
+
appendBridgeLog(`node tray spawn failed: ${e?.message || e}`);
|
|
533
|
+
done(false);
|
|
534
|
+
});
|
|
535
|
+
setTimeout(() => done(!!child.pid), 750);
|
|
536
|
+
});
|
|
537
|
+
if (spawned) {
|
|
538
|
+
child.unref();
|
|
539
|
+
// Bootstrapper exits cleanly; the tray runs independently.
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
appendBridgeLog('node tray spawn did not start; retrying with PowerShell Start-Process');
|
|
544
|
+
const ps = `
|
|
545
|
+
$env:EMPIR3_BOOTSTRAP_EXE = ${psQuote(bootstrapExe)}
|
|
546
|
+
$env:EMPIR3_BRIDGE_PAYLOAD_DIR = ${psQuote(PAYLOAD_DIR)}
|
|
547
|
+
$env:EMPIR3_BRIDGE_PAYLOAD_VERSION = ${psQuote(readVersion())}
|
|
548
|
+
Start-Process -FilePath ${psQuote(trayExe)} -WorkingDirectory ${psQuote(path.dirname(trayExe))}
|
|
549
|
+
`.trim();
|
|
550
|
+
const retry = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], {
|
|
551
|
+
encoding: 'utf8',
|
|
552
|
+
env: trayEnv,
|
|
553
|
+
windowsHide: true,
|
|
554
|
+
});
|
|
555
|
+
if (retry.status === 0) {
|
|
556
|
+
appendBridgeLog('PowerShell Start-Process tray retry succeeded');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
appendBridgeLog(`PowerShell Start-Process tray retry failed: ${(retry.stderr || retry.stdout || '').trim()}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function start(argv) {
|
|
563
|
+
argv = argv || process.argv.slice(2);
|
|
564
|
+
|
|
565
|
+
if (argv.includes('--version') || argv.includes('-v')) {
|
|
566
|
+
console.log(readVersion());
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
570
|
+
printHelp();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (argv.includes('--uninstall')) {
|
|
574
|
+
await uninstall();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
configureRuntimePaths(); // env-only (no stdout) — safe before the --mcp gate
|
|
579
|
+
|
|
580
|
+
// --mcp runs the MCP stdio shim. Spawned by Claude Code (or any other MCP
|
|
581
|
+
// client) per the snippet our /api/install/claude-code endpoint mints. It
|
|
582
|
+
// talks JSON-RPC over stdin/stdout, so ANY stray stdout byte corrupts the
|
|
583
|
+
// stream. Branch here — BEFORE the policy scrub / autostart / Start Menu
|
|
584
|
+
// registration, all of which console.log on success — and let the shim own
|
|
585
|
+
// stdout. The daemon is already running; no tray/daemon spawn, no network
|
|
586
|
+
// update. (Design: "--mcp stdio discipline", Codex P0.)
|
|
587
|
+
if (argv.includes('--mcp')) {
|
|
588
|
+
require('./bundle-mcp-server.js');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// --pair <code>: redeem a PRE-AUTHORIZED Empir3 pairing session (the install-link
|
|
593
|
+
// flow). Empir3 mints the code for the already-logged-in user and bakes it into
|
|
594
|
+
// the install command, so we claim it on first boot and write bridge-auth.json —
|
|
595
|
+
// no second browser login. Runs here (after the --mcp stdio gate, before tray /
|
|
596
|
+
// daemon spawn) so the daemon boots already paired regardless of which launch
|
|
597
|
+
// flag carries it. Bounded + best-effort: any failure just falls through to the
|
|
598
|
+
// normal /welcome first-run pairing, never blocking the install.
|
|
599
|
+
const pairIdx = argv.indexOf('--pair');
|
|
600
|
+
if (pairIdx !== -1) {
|
|
601
|
+
try {
|
|
602
|
+
const { claimPairingCode } = require('./bundle-pair-claim.js');
|
|
603
|
+
const result = await claimPairingCode(argv[pairIdx + 1], { log: (m) => appendBridgeLog('pair: ' + m) });
|
|
604
|
+
appendBridgeLog(`pair: result=${result.status}${result.user && result.user.email ? ' (' + result.user.email + ')' : ''}`);
|
|
605
|
+
} catch (e) {
|
|
606
|
+
appendBridgeLog('pair: claim threw: ' + (e && e.message ? e.message : String(e)));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
scrubLegacyForceInstallPolicy();
|
|
611
|
+
registerAutostart();
|
|
612
|
+
registerStartMenuShortcut();
|
|
613
|
+
|
|
614
|
+
// Used by the native Go launcher path. The bootstrapper launches
|
|
615
|
+
// Empir3Tray.exe itself after this prep step returns.
|
|
616
|
+
if (argv.includes('--launcher-prep')) {
|
|
617
|
+
appendBridgeLog('launcher prep complete');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// --daemon-real runs the bridge daemon directly, no tray. The tray invokes
|
|
622
|
+
// this when it spawns the bootstrapper as a supervised child.
|
|
623
|
+
if (argv.includes('--daemon-real')) {
|
|
624
|
+
await require('./bundle-daemon.js').start();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// --daemon (the autostart target) now goes through the tray so the user
|
|
629
|
+
// sees a real desktop surface. The tray spawns the daemon as its child.
|
|
630
|
+
if (argv.includes('--daemon')) {
|
|
631
|
+
await spawnTrayAndExit();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// First-run / double-click: spawn the tray, which spawns the daemon. The
|
|
636
|
+
// daemon's splash UX (no auth → /welcome two-button page in the bridge's
|
|
637
|
+
// own Chrome) replaces the previous Koba chat installer. Same code path
|
|
638
|
+
// as --daemon, so re-running Empir3Setup.exe is always idempotent.
|
|
639
|
+
await spawnTrayAndExit();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (require.main === module) {
|
|
643
|
+
start().catch((e) => {
|
|
644
|
+
console.error('[empir3-bridge] fatal:', e.stack || e.message);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
module.exports = { start };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"algorithm": "ed25519",
|
|
3
|
+
"publicKeyHex": "a0813b51654fcb6026c0cfc9d0f367c8535b96c94b52c9fff15d2fe59f7cd68a",
|
|
4
|
+
"publicKeyB64": "oIE7UWVPy2AmwM/J0PNnyFNblslLUsn/8V0v5Z981oo=",
|
|
5
|
+
"spkiB64": "MCowBQYDK2VwAyEAoIE7UWVPy2AmwM/J0PNnyFNblslLUsn/8V0v5Z981oo=",
|
|
6
|
+
"generatedAt": "2026-05-02T17:00:36.895Z"
|
|
7
|
+
}
|