anyagent-bridge 0.8.0 → 0.9.0
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 +2 -0
- package/bin/anyagent-bridge.js +1 -0
- package/bin/setup.js +31 -10
- package/docs/GETTING-STARTED.md +7 -0
- package/package.json +1 -1
- package/server/index.js +5 -2
- package/test/stage4-smoke.js +3 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Control your local computer's terminal — and **any** CLI AI coding agent you'v
|
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
|
+
First install **Node.js 18+** (the LTS build from <https://nodejs.org>) — `npx` needs it. Then:
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
12
|
# New here? A guided, first-timer setup (checks prerequisites, helps you go mobile):
|
|
11
13
|
npx anyagent-bridge setup
|
package/bin/anyagent-bridge.js
CHANGED
|
@@ -29,6 +29,7 @@ function printHelp() {
|
|
|
29
29
|
'Usage:',
|
|
30
30
|
' anyagent-bridge [options]',
|
|
31
31
|
' anyagent-bridge setup Guided, first-timer setup (recommended to start).',
|
|
32
|
+
' anyagent-bridge setup --yes Same, non-interactive (accept defaults; for automation).',
|
|
32
33
|
'',
|
|
33
34
|
'Options:',
|
|
34
35
|
' -p, --port <n> Port to listen on (default 3001).',
|
package/bin/setup.js
CHANGED
|
@@ -18,6 +18,7 @@ const fs = require('fs');
|
|
|
18
18
|
const os = require('os');
|
|
19
19
|
const crypto = require('crypto');
|
|
20
20
|
const readline = require('readline');
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
21
22
|
const pkg = require('../package.json');
|
|
22
23
|
|
|
23
24
|
// ── tiny ANSI helpers (no dependency) ─────────────────────────────────────────
|
|
@@ -57,11 +58,15 @@ function firstLanIPv4() {
|
|
|
57
58
|
return null;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
+
// `--yes` / `-y`: run non-interactively, accept all defaults (this computer, no agent
|
|
62
|
+
// auto-install). For automation / AI-driven setup, or any non-TTY environment.
|
|
63
|
+
const AUTO = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
64
|
+
|
|
65
|
+
// ── interactive prompt (degrades gracefully without a TTY or with --yes) ──────────
|
|
61
66
|
let rl = null;
|
|
62
67
|
function ask(question, choices) {
|
|
63
68
|
return new Promise((resolve) => {
|
|
64
|
-
if (!process.stdin.isTTY) { resolve(choices ? choices[0].key : ''); return; }
|
|
69
|
+
if (AUTO || !process.stdin.isTTY) { resolve(choices ? choices[0].key : ''); return; }
|
|
65
70
|
if (!rl) rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
66
71
|
rl.question(question, (answer) => resolve(answer.trim()));
|
|
67
72
|
});
|
|
@@ -71,8 +76,8 @@ async function pick(title, choices) {
|
|
|
71
76
|
out();
|
|
72
77
|
out(bold(title));
|
|
73
78
|
for (const ch of choices) out(` ${cyan(ch.key)}) ${ch.label}${ch.hint ? dim(' — ' + ch.hint) : ''}`);
|
|
74
|
-
if (!process.stdin.isTTY) {
|
|
75
|
-
out(dim(' (
|
|
79
|
+
if (AUTO || !process.stdin.isTTY) {
|
|
80
|
+
out(dim(' (non-interactive — using option ' + choices[0].key + ')'));
|
|
76
81
|
return choices[0];
|
|
77
82
|
}
|
|
78
83
|
for (;;) {
|
|
@@ -84,7 +89,7 @@ async function pick(title, choices) {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
async function confirm(question, def = true) {
|
|
87
|
-
if (!process.stdin.isTTY) return def;
|
|
92
|
+
if (AUTO || !process.stdin.isTTY) return def;
|
|
88
93
|
const a = (await ask(`${question} ${dim(def ? '[Y/n]' : '[y/N]')} `)).toLowerCase();
|
|
89
94
|
if (!a) return def;
|
|
90
95
|
return a.startsWith('y');
|
|
@@ -114,8 +119,17 @@ async function confirm(question, def = true) {
|
|
|
114
119
|
if (!agents.some((a) => a.found)) {
|
|
115
120
|
out();
|
|
116
121
|
out(yellow(' No AI agent CLI was found. The bridge still runs (you get a plain shell),'));
|
|
117
|
-
out(yellow(' but to launch an agent
|
|
118
|
-
|
|
122
|
+
out(yellow(' but to launch an agent you need one installed.'));
|
|
123
|
+
const wantClaude = await confirm(' Install Claude Code now (npm i -g @anthropic-ai/claude-code)?', false);
|
|
124
|
+
if (wantClaude) {
|
|
125
|
+
out(dim(' Installing via npm — this may take a minute…'));
|
|
126
|
+
const r = spawnSync('npm', ['install', '-g', '@anthropic-ai/claude-code'],
|
|
127
|
+
{ stdio: 'inherit', shell: process.platform === 'win32' });
|
|
128
|
+
if (r.status === 0) out(green(' ✓ Installed. Run `claude` once in a terminal to log in, then it shows up here.'));
|
|
129
|
+
else out(yellow(' Install didn\'t finish — do it manually: ') + cyan('npm install -g @anthropic-ai/claude-code'));
|
|
130
|
+
} else {
|
|
131
|
+
out(' Install one yourself, e.g.: ' + cyan('npm install -g @anthropic-ai/claude-code') + dim(' then run `claude` once to log in.'));
|
|
132
|
+
}
|
|
119
133
|
out(dim(' (Any CLI works — register it under "agents" in config.json.)'));
|
|
120
134
|
}
|
|
121
135
|
|
|
@@ -145,6 +159,7 @@ async function confirm(question, def = true) {
|
|
|
145
159
|
: `Find this computer's local IP (e.g. ${dim('System Settings → Network')}), then visit ${cyan(`http://<that-ip>:${port}/`)} on the other device.`);
|
|
146
160
|
nextSteps.push(`${yellow('Heads up:')} on your Wi-Fi the access token is the only lock. Keep it private; only people on your network can even reach the page.`);
|
|
147
161
|
nextSteps.push(`Easiest on a phone: open the page on this computer, click ${bold('"Connect a device" → Phone')}, and scan the QR.`);
|
|
162
|
+
if (process.platform === 'win32') nextSteps.push(`${yellow('Windows:')} the first time another device connects, Windows pops a Firewall prompt — click ${bold('Allow')} (at least Private networks) or the page won't load.`);
|
|
148
163
|
} else {
|
|
149
164
|
host = '127.0.0.1'; // the tunnel reaches in; we don't bind to the network directly
|
|
150
165
|
out();
|
|
@@ -160,9 +175,15 @@ async function confirm(question, def = true) {
|
|
|
160
175
|
tunnelProvider = prov.key === '2' ? 'cloudflare-quick' : 'devtunnel';
|
|
161
176
|
const cli = tunnelProvider === 'cloudflare-quick' ? 'cloudflared' : 'devtunnel';
|
|
162
177
|
if (!onPath(cli)) {
|
|
163
|
-
out(yellow(` The "${cli}" command isn't installed yet
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
out(yellow(` The "${cli}" command isn't installed yet — install it, then re-run setup:`));
|
|
179
|
+
if (cli === 'cloudflared') {
|
|
180
|
+
if (process.platform === 'darwin') out(' ' + cyan('brew install cloudflared'));
|
|
181
|
+
else if (process.platform === 'win32') out(' ' + cyan('winget install --id Cloudflare.cloudflared') + dim(' (or grab it from github.com/cloudflare/cloudflared/releases)'));
|
|
182
|
+
else out(' ' + cyan('https://pkg.cloudflare.com') + dim(' (apt/yum) or github.com/cloudflare/cloudflared/releases'));
|
|
183
|
+
} else {
|
|
184
|
+
out(' ' + cyan('https://aka.ms/devtunnels/download') + dim(' then run `devtunnel user login` once'));
|
|
185
|
+
}
|
|
186
|
+
out(dim(' Continuing anyway — the server falls back to localhost-only if the tunnel cannot start.'));
|
|
166
187
|
}
|
|
167
188
|
nextSteps.push(`When the tunnel connects, its public ${bold('https://…')} URL prints in the banner below and shows in the UI.`);
|
|
168
189
|
nextSteps.push(`Open that URL on your phone, or use ${bold('"Connect a device" → Phone')} in the UI to scan a QR.`);
|
package/docs/GETTING-STARTED.md
CHANGED
|
@@ -103,6 +103,13 @@ so you never paste keys into the chat or hand-edit dotfiles.
|
|
|
103
103
|
tunnel at all — use the "same Wi-Fi" address instead.
|
|
104
104
|
- **Lost the token** — it's printed in the terminal banner; scroll up, or stop and
|
|
105
105
|
re-run. You can also pin your own with `--token`.
|
|
106
|
+
- **Windows: another device can't reach it on Wi-Fi** — the first inbound connection
|
|
107
|
+
triggers a Windows Firewall prompt; click **Allow** (at least for Private networks).
|
|
108
|
+
If you dismissed it, allow `node` in Windows Defender Firewall settings.
|
|
109
|
+
- **Windows: a red `'...cmd_autorun.bat' is not recognized…` line at the top of the
|
|
110
|
+
terminal** — that's not an anyagent-bridge error. It's a stale `cmd.exe` AutoRun
|
|
111
|
+
registry key on your machine (`HKCU\Software\Microsoft\Command Processor\AutoRun`);
|
|
112
|
+
the shell still works. Clear that key if you want the line gone.
|
|
106
113
|
|
|
107
114
|
## Where to go next
|
|
108
115
|
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -744,7 +744,10 @@ class TerminalSession {
|
|
|
744
744
|
console.warn(`[Session ${this.sessionId}] startAgent: invalid agent`);
|
|
745
745
|
return;
|
|
746
746
|
}
|
|
747
|
-
|
|
747
|
+
// Submit with CR, not LF: a real Enter key sends "\r" to a PTY. On Unix the line
|
|
748
|
+
// discipline maps CR→NL (ICRNL) so the command still runs; on Windows ConPTY,
|
|
749
|
+
// cmd.exe only executes a line ended with "\r" ("\n" types it but never runs it).
|
|
750
|
+
this.write(`${agent.command}\r`);
|
|
748
751
|
this.activeAgentId = agent.id;
|
|
749
752
|
console.log(`[Session ${this.sessionId}] Started agent '${agent.id}' (${agent.command})`);
|
|
750
753
|
}
|
|
@@ -752,7 +755,7 @@ class TerminalSession {
|
|
|
752
755
|
/** Send a line of text to whatever is currently running in the PTY. */
|
|
753
756
|
sendToAgent(text) {
|
|
754
757
|
if (text == null) return;
|
|
755
|
-
this.write(String(text) + '\
|
|
758
|
+
this.write(String(text) + '\r'); // CR submits on both Unix PTYs and Windows ConPTY (see startAgent)
|
|
756
759
|
}
|
|
757
760
|
|
|
758
761
|
destroy() {
|
package/test/stage4-smoke.js
CHANGED
|
@@ -132,7 +132,9 @@ t('buildDockerArgs has run --rm -it, name, mount, limits, image, shell', () => {
|
|
|
132
132
|
const j = args.join(' ');
|
|
133
133
|
assert(args[0] === 'run' && args.includes('--rm') && args.includes('-it'), j);
|
|
134
134
|
assert(j.includes('--name aab-x-sess-1-ab'), j);
|
|
135
|
-
|
|
135
|
+
// OS-aware: buildDockerArgs maps the host path via dockerMountPath (e.g. on Windows
|
|
136
|
+
// C:\tmp\proj → //c/tmp/proj), so derive the expected mount the same way.
|
|
137
|
+
assert(j.includes('-v ' + sandbox.dockerMountPath('/tmp/proj') + ':/workspace') && j.includes('-w /workspace'), j);
|
|
136
138
|
assert(j.includes('--network bridge') && j.includes('--memory 2g') && j.includes('--pids-limit 512'), j);
|
|
137
139
|
assert(j.includes('--security-opt no-new-privileges'), j);
|
|
138
140
|
assert(args[args.length - 2] === 'demo:latest' || args.includes('demo:latest'), j);
|