@chatpanel/bridge 0.2.6 → 0.2.8

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/README.md CHANGED
@@ -14,16 +14,23 @@ the ones the bridge reports as available.
14
14
 
15
15
  ## Run it
16
16
 
17
- ### Option A — one-line install (macOS / Linux, no Node.js needed)
17
+ ### Option A — one-line install (no Node.js needed)
18
18
 
19
+ **macOS / Linux:**
19
20
  ```bash
20
- curl -fsSL https://raw.githubusercontent.com/chatpanel/chatpanel-bridge/main/scripts/install.sh | bash
21
+ curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash
21
22
  ```
22
23
 
23
- This downloads the standalone binary for your OS, installs it to `~/.local/bin`,
24
- and sets it to start at login. **Recommended** — installing via curl avoids the
25
- macOS "damaged / unidentified developer" prompt that browser downloads trigger.
26
- Then open the ChatPanel side panel and your agents appear.
24
+ **Windows (PowerShell):**
25
+ ```powershell
26
+ irm https://dl.chatpanel.net/bridge/install.ps1 | iex
27
+ ```
28
+
29
+ This downloads the standalone binary for your OS, installs it, and sets it to
30
+ start at login. **Recommended** — installing this way avoids the macOS "damaged"
31
+ and Windows SmartScreen prompts that browser downloads trigger. Re-running it is a
32
+ clean in-place upgrade (no duplicate installs). Then open the ChatPanel side panel
33
+ and your agents appear.
27
34
 
28
35
  Manage it: `chatpanel-bridge --status` · `--uninstall` · run with no flags to start
29
36
  once in the foreground.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/bridge",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (Agent SDK), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
6
6
  "keywords": [
@@ -0,0 +1,39 @@
1
+ # ChatPanel Bridge installer (Windows) - no Node.js required.
2
+ #
3
+ # irm https://dl.chatpanel.net/bridge/install.ps1 | iex
4
+ $ErrorActionPreference = 'Stop'
5
+ $ProgressPreference = 'SilentlyContinue' # IWR fallback: skip the slow progress bar
6
+
7
+ $url = 'https://dl.chatpanel.net/bridge/windows-x64.exe'
8
+ $dir = Join-Path $env:LOCALAPPDATA 'ChatPanel'
9
+ $bin = Join-Path $dir 'chatpanel-bridge.exe'
10
+ $tmp = "$bin.new"
11
+
12
+ Write-Host ""
13
+ Write-Host "Installing ChatPanel Bridge" -ForegroundColor Cyan
14
+
15
+ # Stop any running bridge + its scheduled task so the .exe isn't locked (clean
16
+ # in-place upgrade, no duplicate installs).
17
+ schtasks /End /TN ChatPanelBridge *> $null
18
+ Get-Process -Name 'chatpanel-bridge' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
19
+ Start-Sleep -Milliseconds 600
20
+
21
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
22
+
23
+ Write-Host " Downloading the bridge (~60 MB)..." -ForegroundColor Gray
24
+ $curl = Get-Command curl.exe -ErrorAction SilentlyContinue
25
+ if ($curl) {
26
+ & curl.exe -fL --progress-bar -o $tmp $url # fast, with a real progress bar
27
+ } else {
28
+ Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing
29
+ }
30
+ Unblock-File -Path $tmp -ErrorAction SilentlyContinue # no SmartScreen mark-of-the-web
31
+ Move-Item -Force $tmp $bin
32
+
33
+ Write-Host " Setting it to start at login..." -ForegroundColor Gray
34
+ & $bin --install
35
+
36
+ Write-Host ""
37
+ Write-Host "Done. ChatPanel Bridge is running and starts at login." -ForegroundColor Green
38
+ Write-Host "Open the ChatPanel side panel - your agents appear automatically."
39
+ Write-Host "Manage it: `"$bin`" --status | --uninstall"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # ChatPanel Bridge installer downloads the standalone binary for your OS and
2
+ # ChatPanel Bridge installer - downloads the standalone binary for your OS and
3
3
  # sets it to start at login. No Node.js required.
4
4
  #
5
5
  # curl -fsSL https://raw.githubusercontent.com/chatpanel/chatpanel-bridge/main/scripts/install.sh | bash
@@ -8,38 +8,49 @@
8
8
  # "damaged / unidentified developer" prompt that browser downloads trigger.
9
9
  set -euo pipefail
10
10
 
11
- REPO="chatpanel/chatpanel-bridge"
12
11
  os="$(uname -s)"
13
12
  arch="$(uname -m)"
13
+ asset=""
14
14
 
15
15
  case "$os" in
16
16
  Darwin)
17
- if [ "$arch" = "arm64" ]; then asset="chatpanel-bridge-macos-arm64"
17
+ if [ "$arch" = "arm64" ]; then
18
+ asset="bridge/macos-arm64"
18
19
  else
19
- echo "Intel Mac detected no x64 binary yet. Use: npx @chatpanel/bridge (needs Node.js 18+)"
20
+ echo "Intel Mac detected - no x64 binary yet. Use: npx @chatpanel/bridge (needs Node.js 18+)"
20
21
  exit 1
21
- fi ;;
22
- Linux) asset="chatpanel-bridge-linux-x64" ;;
22
+ fi
23
+ ;;
24
+ Linux)
25
+ asset="bridge/linux-x64"
26
+ ;;
23
27
  *)
24
28
  echo "Unsupported OS ($os). Use: npx @chatpanel/bridge (needs Node.js 18+)"
25
- exit 1 ;;
29
+ exit 1
30
+ ;;
26
31
  esac
27
32
 
28
- url="https://github.com/$REPO/releases/latest/download/$asset"
29
- dest="$HOME/.local/bin"
30
- bin="$dest/chatpanel-bridge"
33
+ url="https://dl.chatpanel.net/${asset}"
34
+ dest="${HOME}/.local/bin"
35
+ bin="${dest}/chatpanel-bridge"
31
36
  mkdir -p "$dest"
32
37
 
33
- echo "Downloading $asset…"
34
- curl -fsSL "$url" -o "$bin"
38
+ echo "Downloading ChatPanel Bridge (~60 MB)..."
39
+ curl -fL --progress-bar "$url" -o "$bin" # show a progress bar (not silent)
35
40
  chmod +x "$bin"
36
- xattr -c "$bin" 2>/dev/null || true # belt-and-suspenders; curl files aren't quarantined
41
+ xattr -c "$bin" 2>/dev/null || true # belt-and-suspenders; curl files aren't quarantined
37
42
 
38
- echo "✓ Installed to $bin"
43
+ # Clean upgrade: stop any running bridge (incl. a stray npx one) so the new
44
+ # install replaces it in place — same path, same service, no duplicates.
45
+ pkill -f 'chatpanel-bridge' 2>/dev/null || true
46
+ sleep 1
47
+
48
+ echo "Installed to ${bin}"
39
49
  "$bin" --install
40
50
  echo
41
51
  echo "ChatPanel Bridge is running and will start at login."
42
- case ":$PATH:" in
43
- *":$dest:"*) : ;;
44
- *) echo "Tip: add it to your PATH → export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
52
+
53
+ case ":${PATH}:" in
54
+ *":${dest}:"*) : ;;
55
+ *) echo "Tip: add it to your PATH -> export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
45
56
  esac
@@ -9,6 +9,8 @@
9
9
 
10
10
  import path from 'node:path';
11
11
  import os from 'node:os';
12
+ import { existsSync } from 'node:fs';
13
+ import { findAgentBin, isCompiledBinary } from '../env.js';
12
14
 
13
15
  let sdkPromise = null;
14
16
  function loadSdk() {
@@ -16,14 +18,37 @@ function loadSdk() {
16
18
  return sdkPromise;
17
19
  }
18
20
 
21
+ // Where the Claude Code CLI lives. The SDK ships a bundled cli.js, but inside a
22
+ // compiled binary that file is on a virtual FS that child processes can't reach
23
+ // (it fails on Windows as "B:\~BUN\cli.js"). So in a binary we point the SDK at
24
+ // the user's INSTALLED Claude Code instead — preferring the real cli.js next to
25
+ // the npm shim (modern Node won't spawn a .cmd directly).
26
+ function claudeExecutable() {
27
+ if (process.env.CHATPANEL_CLAUDE_PATH) return process.env.CHATPANEL_CLAUDE_PATH;
28
+ if (!isCompiledBinary()) return undefined; // under node/bun the bundled cli.js works
29
+ const bin = findAgentBin('claude');
30
+ if (!bin) return undefined;
31
+ const dir = path.dirname(bin);
32
+ const candidates = [
33
+ path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
34
+ path.join(dir, '..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
35
+ ];
36
+ for (const c of candidates) if (existsSync(c)) return c;
37
+ return bin;
38
+ }
39
+
19
40
  export async function available() {
20
41
  const sdk = await loadSdk();
21
42
  if (!sdk) {
22
43
  return { ok: false, reason: 'Agent SDK not installed (npm i in bridge/)' };
23
44
  }
24
- if (!process.env.ANTHROPIC_API_KEY) {
25
- // Not fatal the SDK can use your local Claude Code login.
26
- return { ok: true };
45
+ // In a compiled binary the bundled CLI is unreachable, so Claude Code must be
46
+ // installed locally. (Under node/bun the bundled CLI works, so this is skipped.)
47
+ if (isCompiledBinary() && !process.env.CHATPANEL_CLAUDE_PATH && !findAgentBin('claude')) {
48
+ return {
49
+ ok: false,
50
+ reason: 'Claude Code not found. Install it (npm i -g @anthropic-ai/claude-code), or run the bridge with `npx @chatpanel/bridge`.',
51
+ };
27
52
  }
28
53
  return { ok: true };
29
54
  }
@@ -85,6 +110,7 @@ export async function chat({ messages, system, options }, emit) {
85
110
  ? { type: 'preset', preset: 'claude_code', append: system }
86
111
  : { type: 'preset', preset: 'claude_code' },
87
112
  ...(options.model ? { model: options.model } : {}),
113
+ ...(claudeExecutable() ? { pathToClaudeCodeExecutable: claudeExecutable() } : {}),
88
114
  ...(process.env.CHATPANEL_MAX_TURNS ? { maxTurns: Number(process.env.CHATPANEL_MAX_TURNS) } : {}),
89
115
  },
90
116
  });
@@ -140,6 +166,7 @@ export async function complete({ prompt, system, model }) {
140
166
  settingSources: [], // skip CLAUDE.md / MCP for a tiny completion
141
167
  systemPrompt: system || "Continue the user's text briefly. Reply with only the continuation.",
142
168
  model: model || 'haiku',
169
+ ...(claudeExecutable() ? { pathToClaudeCodeExecutable: claudeExecutable() } : {}),
143
170
  },
144
171
  });
145
172
  for await (const message of iterator) {
@@ -19,6 +19,7 @@ import { readFile, unlink } from 'node:fs/promises';
19
19
  import { existsSync, mkdirSync, symlinkSync } from 'node:fs';
20
20
  import os from 'node:os';
21
21
  import path from 'node:path';
22
+ import { findAgentBin } from '../env.js';
22
23
 
23
24
  const TIMEOUT_MS = Number(process.env.CHATPANEL_CODEX_TIMEOUT_MS) || 180_000;
24
25
  const REASONING = process.env.CHATPANEL_CODEX_EFFORT ?? 'low'; // '' → respect config
@@ -61,13 +62,13 @@ function ensureIsolatedHome() {
61
62
  let installed = false;
62
63
  let lastProbe = 0;
63
64
  export async function available() {
64
- // Cache a positive result, but keep re-probing (throttled) while not found, so
65
- // it self-heals once codex appears on PATH never cache a negative forever.
65
+ // Availability = "is codex findable on PATH", not "does `codex --version` exit
66
+ // 0" (which fails when the CLI just needs login). Cache positives; re-probe
67
+ // (throttled) while not found so it self-heals once codex appears on PATH.
66
68
  if (!installed && Date.now() - lastProbe > 4000) {
67
69
  lastProbe = Date.now();
68
70
  try {
69
- const r = spawnSync('codex', ['--version'], { stdio: 'ignore', timeout: 8000 });
70
- installed = r.status === 0 || (r.status === null && !r.error);
71
+ installed = !!findAgentBin('codex');
71
72
  } catch {
72
73
  installed = false;
73
74
  }
@@ -12,6 +12,7 @@ import { spawn, spawnSync } from 'node:child_process';
12
12
  import { mkdirSync } from 'node:fs';
13
13
  import os from 'node:os';
14
14
  import path from 'node:path';
15
+ import { findAgentBin } from '../env.js';
15
16
 
16
17
  const TIMEOUT_MS = Number(process.env.CHATPANEL_GEMINI_TIMEOUT_MS) || 180_000;
17
18
  const SCRATCH = path.join(os.tmpdir(), 'chatpanel-gemini-scratch');
@@ -24,8 +25,7 @@ export async function available() {
24
25
  if (!installed && Date.now() - lastProbe > 4000) {
25
26
  lastProbe = Date.now();
26
27
  try {
27
- const r = spawnSync('gemini', ['--version'], { stdio: 'ignore', timeout: 8000 });
28
- installed = r.status === 0 || (r.status === null && !r.error);
28
+ installed = !!findAgentBin('gemini');
29
29
  } catch {
30
30
  installed = false;
31
31
  }
package/src/env.js CHANGED
@@ -21,6 +21,32 @@ function onPath(name) {
21
21
  return dirs.some((d) => d && (existsSync(path.join(d, name)) || existsSync(path.join(d, name + '.exe'))));
22
22
  }
23
23
 
24
+ // Resolve an agent CLI to its absolute path: first on the (enriched) PATH, then
25
+ // by asking the login shell. Returns the path, or null. Used for availability —
26
+ // "is it installed/findable", NOT "does `--version` exit 0" (which can fail for
27
+ // reasons unrelated to installation, e.g. the CLI needs login).
28
+ export function findAgentBin(name) {
29
+ // On Windows, CLIs are usually <name>.cmd / .exe / .bat (npm shims).
30
+ const exts = process.platform === 'win32' ? ['', '.cmd', '.exe', '.bat', '.ps1'] : [''];
31
+ const dirs = (process.env.PATH || '').split(path.delimiter);
32
+ for (const d of dirs) {
33
+ if (!d) continue;
34
+ for (const ext of exts) {
35
+ const p = path.join(d, name + ext);
36
+ if (existsSync(p)) return p;
37
+ }
38
+ }
39
+ return shellWhich(name) || null;
40
+ }
41
+
42
+ // True when running as a Bun/Node single-file compiled binary (not under a
43
+ // node/bun interpreter). Inside such a binary, bundled JS files live on a virtual
44
+ // FS that child processes can't reach — notably the Claude SDK's CLI on Windows.
45
+ export function isCompiledBinary() {
46
+ const base = path.basename(process.execPath).toLowerCase();
47
+ return !(base.startsWith('node') || base.startsWith('bun'));
48
+ }
49
+
24
50
  // Ask the user's login shell to locate a command — no hardcoded locations, works
25
51
  // wherever the user actually installed it.
26
52
  function shellWhich(name) {
package/src/server.js CHANGED
@@ -15,13 +15,14 @@
15
15
  // Binds to 127.0.0.1 only and accepts requests from the extension origin.
16
16
 
17
17
  import { createServer } from 'node:http';
18
+ import os from 'node:os';
18
19
  import * as claude from './engines/claude.js';
19
20
  import * as codex from './engines/codex.js';
20
21
  import * as gemini from './engines/gemini.js';
21
22
  import { installService, uninstallService, serviceStatus } from './service.js';
22
- import { enrichPath } from './env.js';
23
+ import { enrichPath, findAgentBin } from './env.js';
23
24
 
24
- const VERSION = '0.2.6';
25
+ const VERSION = '0.2.8';
25
26
  const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
26
27
  const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
27
28
 
@@ -175,6 +176,15 @@ const server = createServer(async (req, res) => {
175
176
  const url = new URL(req.url, `http://${req.headers.host}`);
176
177
  try {
177
178
  if (req.method === 'GET' && url.pathname === '/health') return handleHealth(res);
179
+ if (req.method === 'GET' && url.pathname === '/debug') {
180
+ return json(res, 200, {
181
+ version: VERSION,
182
+ home: os.homedir(),
183
+ codex: findAgentBin('codex') || null,
184
+ gemini: findAgentBin('gemini') || null,
185
+ path: process.env.PATH,
186
+ });
187
+ }
178
188
  if (req.method === 'POST' && url.pathname === '/chat') return handleChat(req, res);
179
189
  if (req.method === 'POST' && url.pathname === '/complete') return handleComplete(req, res);
180
190
  json(res, 404, { error: 'Not found' });