@chatpanel/bridge 0.10.9 → 0.10.10

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/env.js +160 -23
  3. package/src/server.js +3 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/bridge",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
4
4
  "type": "module",
5
5
  "description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (CLI), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
6
6
  "keywords": [
package/src/env.js CHANGED
@@ -14,11 +14,11 @@ let enriched = false;
14
14
 
15
15
  // The agent CLIs the bridge shells out to. Claude has its own richer resolution
16
16
  // (resolveClaude: native / cli.js / WSL / SDK) below.
17
- const AGENT_CLIS = ['codex', 'claude', 'agy', 'pi', 'opencode', 'kiro-cli'];
17
+ export const AGENT_CLIS = ['codex', 'claude', 'agy', 'pi', 'opencode', 'kiro-cli'];
18
18
 
19
19
  // Is `name` executable somewhere on the current PATH?
20
20
  function onPath(name) {
21
- const dirs = (process.env.PATH || '').split(path.delimiter);
21
+ const dirs = splitPathList(process.env.PATH || '', process.platform);
22
22
  return dirs.some((d) => d && (existsSync(path.join(d, name)) || existsSync(path.join(d, name + '.exe'))));
23
23
  }
24
24
 
@@ -27,14 +27,9 @@ function onPath(name) {
27
27
  // "is it installed/findable", NOT "does `--version` exit 0" (which can fail for
28
28
  // reasons unrelated to installation, e.g. the CLI needs login).
29
29
  export function findAgentBin(name) {
30
- // On Windows, CLIs are usually <name>.cmd / .exe / .bat (npm shims).
31
- const exts = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat', '.ps1'] : [''];
32
- const dirs = (process.env.PATH || '').split(path.delimiter);
33
- for (const d of dirs) {
34
- if (!d) continue;
35
- for (const ext of exts) {
36
- const p = path.join(d, name + ext);
37
- if (existsSync(p)) return p;
30
+ for (const p of commandCandidateFiles(name)) {
31
+ if (existsSync(p)) {
32
+ return p;
38
33
  }
39
34
  }
40
35
  return shellWhich(name) || null;
@@ -152,20 +147,20 @@ export function resolveCommand(command) {
152
147
  // own Node/Bun — clean arg passing, no cmd.exe quoting), then a real .exe, then a
153
148
  // .cmd/.bat shim launched safely via cmd.exe.
154
149
  function findCommandWindows(name) {
155
- const dirs = (process.env.PATH || '').split(path.delimiter);
156
- for (const d of dirs) {
157
- if (!d) continue;
158
- const exts = ['', '.cmd', '.exe', '.ps1', '.bat'];
159
- if (!exts.some((e) => existsSync(path.join(d, name + e)))) continue;
150
+ const p = pathFor('win32');
151
+ for (const candidate of commandCandidateFiles(name, os.homedir(), 'win32', process.env)) {
152
+ if (!existsSync(candidate)) continue;
153
+ const d = p.dirname(candidate);
154
+ const ext = p.extname(candidate).toLowerCase();
160
155
  // Running cli.js with our own interpreter only works under a real Node/Bun,
161
156
  // not inside a compiled single-file binary (which is not a JS interpreter).
162
157
  if (!isCompiledBinary()) {
163
158
  const js = (name === 'claude' && claudeCliJs(d)) || shimTarget(d, name);
164
159
  if (js) return { kind: 'script', script: js };
160
+ if (/^\.(c?js|mjs)$/.test(ext)) return { kind: 'script', script: candidate };
165
161
  }
166
- if (existsSync(path.join(d, name + '.exe'))) return { kind: 'native', bin: path.join(d, name + '.exe') };
167
- if (existsSync(path.join(d, name + '.cmd'))) return { kind: 'cmd', bin: path.join(d, name + '.cmd') };
168
- if (existsSync(path.join(d, name + '.bat'))) return { kind: 'cmd', bin: path.join(d, name + '.bat') };
162
+ if (ext === '.exe' || ext === '') return { kind: 'native', bin: candidate };
163
+ if (ext === '.cmd' || ext === '.bat') return { kind: 'cmd', bin: candidate };
169
164
  }
170
165
  return null;
171
166
  }
@@ -290,14 +285,156 @@ function versionManagerBins(home) {
290
285
  return bins;
291
286
  }
292
287
 
293
- export function enrichPath() {
294
- if (enriched || process.platform === 'win32') {
295
- enriched = true;
296
- return; // Windows scheduled tasks run as the user and inherit a fuller PATH.
288
+ function pathFor(platform) {
289
+ return platform === 'win32' ? path.win32 : path;
290
+ }
291
+
292
+ function pathDelimiterFor(platform) {
293
+ return platform === 'win32' ? ';' : path.delimiter;
294
+ }
295
+
296
+ function splitPathList(value, platform) {
297
+ return (value || '').split(pathDelimiterFor(platform)).filter(Boolean);
298
+ }
299
+
300
+ function unique(items) {
301
+ return [...new Set(items.filter(Boolean))];
302
+ }
303
+
304
+ export function agentInstallDirs(home, platform = process.platform, env = process.env) {
305
+ const p = pathFor(platform);
306
+ if (platform === 'win32') {
307
+ const appData = env.APPDATA || p.join(home, 'AppData', 'Roaming');
308
+ const localAppData = env.LOCALAPPDATA || p.join(home, 'AppData', 'Local');
309
+ const programFiles = env.ProgramFiles || 'C:\\Program Files';
310
+ const programFilesX86 = env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
311
+ const programData = env.ProgramData || 'C:\\ProgramData';
312
+ return [
313
+ p.join(home, '.local', 'bin'),
314
+ p.join(home, '.codex', 'bin'),
315
+ p.join(home, '.claude', 'bin'),
316
+ p.join(home, '.claude', 'local'),
317
+ p.join(home, '.claude', 'local', 'bin'),
318
+ p.join(appData, 'npm'),
319
+ p.join(localAppData, 'pnpm'),
320
+ p.join(localAppData, 'Yarn', 'bin'),
321
+ p.join(localAppData, 'Volta', 'bin'),
322
+ p.join(home, '.yarn', 'bin'),
323
+ p.join(home, '.bun', 'bin'),
324
+ p.join(home, '.deno', 'bin'),
325
+ p.join(home, 'scoop', 'shims'),
326
+ p.join(programData, 'chocolatey', 'bin'),
327
+ p.join(localAppData, 'Microsoft', 'WindowsApps'),
328
+ p.join(localAppData, 'Programs', 'Claude Code'),
329
+ p.join(localAppData, 'Anthropic', 'Claude Code'),
330
+ p.join(programFiles, 'Claude Code'),
331
+ p.join(programFiles, 'Anthropic', 'Claude Code'),
332
+ p.join(programFilesX86, 'Claude Code'),
333
+ p.join(programFilesX86, 'Anthropic', 'Claude Code'),
334
+ ];
297
335
  }
336
+ return [
337
+ path.join(home, '.local', 'bin'),
338
+ path.join(home, '.opencode', 'bin'),
339
+ path.join(home, '.codex', 'bin'),
340
+ path.join(home, '.claude', 'bin'),
341
+ path.join(home, '.claude', 'local'),
342
+ path.join(home, '.claude', 'local', 'bin'),
343
+ ];
344
+ }
345
+
346
+ function claudeNativeBins(home, platform = process.platform) {
347
+ const p = pathFor(platform);
348
+ const versionsDir = p.join(home, '.local', 'share', 'claude', 'versions');
349
+ let versions = [];
350
+ try {
351
+ versions = readdirSync(versionsDir)
352
+ .filter((v) => /^\d+\.\d+\.\d+/.test(v))
353
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
354
+ } catch {
355
+ /* native installer may not be present */
356
+ }
357
+ return versions.map((v) => p.join(versionsDir, v));
358
+ }
359
+
360
+ function withWindowsExts(file, platform) {
361
+ if (platform !== 'win32') return [file];
362
+ if (/\.(cmd|exe|bat|ps1)$/i.test(file)) return [file];
363
+ return [file, `${file}.cmd`, `${file}.exe`, `${file}.bat`, `${file}.ps1`];
364
+ }
365
+
366
+ export function agentCandidateBins(name, home = os.homedir(), platform = process.platform, env = process.env) {
367
+ const p = pathFor(platform);
368
+ const local = p.join(home, '.local', 'bin', name);
369
+ const localAppData = platform === 'win32' ? env.LOCALAPPDATA || p.join(home, 'AppData', 'Local') : null;
370
+ const programFiles = platform === 'win32' ? env.ProgramFiles || 'C:\\Program Files' : null;
371
+ const programFilesX86 = platform === 'win32' ? env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)' : null;
372
+ const map = {
373
+ claude: [
374
+ local,
375
+ p.join(home, '.claude', 'bin', 'claude'),
376
+ p.join(home, '.claude', 'local', 'claude'),
377
+ p.join(home, '.claude', 'local', 'bin', 'claude'),
378
+ ...(platform === 'win32'
379
+ ? [
380
+ p.join(localAppData, 'Microsoft', 'WindowsApps', 'claude.exe'),
381
+ p.join(localAppData, 'Programs', 'Claude Code', 'claude.exe'),
382
+ p.join(localAppData, 'Anthropic', 'Claude Code', 'claude.exe'),
383
+ p.join(programFiles, 'Claude Code', 'claude.exe'),
384
+ p.join(programFiles, 'Anthropic', 'Claude Code', 'claude.exe'),
385
+ p.join(programFilesX86, 'Claude Code', 'claude.exe'),
386
+ p.join(programFilesX86, 'Anthropic', 'Claude Code', 'claude.exe'),
387
+ ...claudeNativeBins(home, platform),
388
+ ]
389
+ : claudeNativeBins(home, platform)),
390
+ ],
391
+ codex: [
392
+ local,
393
+ p.join(home, '.codex', 'bin', 'codex'),
394
+ ],
395
+ agy: [
396
+ local,
397
+ '/Applications/Antigravity.app/Contents/MacOS/agy',
398
+ '/Applications/Antigravity IDE.app/Contents/MacOS/agy',
399
+ ],
400
+ pi: [
401
+ local,
402
+ ],
403
+ opencode: [
404
+ p.join(home, '.opencode', 'bin', 'opencode'),
405
+ local,
406
+ ],
407
+ 'kiro-cli': [
408
+ local,
409
+ '/Applications/Kiro CLI.app/Contents/MacOS/kiro-cli',
410
+ ],
411
+ };
412
+ return unique((map[name] || [local]).flatMap((file) => withWindowsExts(file, platform)));
413
+ }
414
+
415
+ export function commandCandidateFiles(name, home = os.homedir(), platform = process.platform, env = process.env) {
416
+ const p = pathFor(platform);
417
+ const fromDirs = unique([
418
+ ...splitPathList(env.PATH || '', platform),
419
+ ...agentInstallDirs(home, platform, env),
420
+ ]).flatMap((dir) => withWindowsExts(p.join(dir, name), platform));
421
+ return unique([...fromDirs, ...agentCandidateBins(name, home, platform, env)]);
422
+ }
423
+
424
+ export function enrichPath() {
425
+ if (enriched) return;
298
426
  enriched = true;
299
427
 
300
428
  const home = os.homedir();
429
+ if (process.platform === 'win32') {
430
+ const merged = unique([
431
+ ...splitPathList(process.env.PATH || '', 'win32'),
432
+ ...agentInstallDirs(home, 'win32', process.env),
433
+ ]);
434
+ process.env.PATH = merged.join(pathDelimiterFor('win32'));
435
+ return;
436
+ }
437
+
301
438
  const common = [
302
439
  '/opt/homebrew/bin',
303
440
  '/opt/homebrew/sbin',
@@ -312,7 +449,7 @@ export function enrichPath() {
312
449
  path.join(home, '.cargo', 'bin'),
313
450
  path.join(home, '.deno', 'bin'),
314
451
  path.join(home, '.bun', 'bin'),
315
- path.join(home, '.opencode', 'bin'),
452
+ ...agentInstallDirs(home),
316
453
  ...versionManagerBins(home),
317
454
  ];
318
455
 
package/src/server.js CHANGED
@@ -23,14 +23,14 @@ import * as antigravity from './engines/antigravity.js';
23
23
  import { pi, opencode, kiro } from './engines/cli-agents.js';
24
24
  import * as custom from './engines/custom.js';
25
25
  import { installService, uninstallService, serviceStatus, restartService } from './service.js';
26
- import { enrichPath, findAgentBin, resolveCommand } from './env.js';
26
+ import { AGENT_CLIS, enrichPath, findAgentBin, resolveCommand } from './env.js';
27
27
  import { checkForUpdate, selfUpdate } from './update.js';
28
28
  import { callLocalMcp } from './mcp-local.js';
29
29
 
30
30
  // Hardcoded (not read from package.json) so it survives Bun's single-file
31
31
  // --compile, where package.json isn't on a readable FS. CI fails the publish if
32
32
  // this drifts from package.json, so the two can't silently diverge.
33
- const VERSION = '0.10.9';
33
+ const VERSION = '0.10.10';
34
34
  const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
35
35
  const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
36
36
 
@@ -438,8 +438,7 @@ const server = createServer(async (req, res) => {
438
438
  return json(res, 200, {
439
439
  version: VERSION,
440
440
  home: os.homedir(),
441
- codex: findAgentBin('codex') || null,
442
- agy: findAgentBin('agy') || null,
441
+ agents: Object.fromEntries(AGENT_CLIS.map((name) => [name, findAgentBin(name) || null])),
443
442
  path: process.env.PATH,
444
443
  });
445
444
  }