anyagent-bridge 0.5.0 → 0.7.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 +9 -3
- package/bin/anyagent-bridge.js +7 -0
- package/bin/setup.js +206 -0
- package/client/index.html +317 -0
- package/docs/GETTING-STARTED.md +108 -0
- package/docs/INSTALL.md +3 -1
- package/docs/WALKTHROUGH.md +12 -0
- package/docs/screenshots/02-terminal-view.png +0 -0
- package/docs/screenshots/05-connect-device.png +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,15 +7,20 @@ Control your local computer's terminal — and **any** CLI AI coding agent you'v
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
#
|
|
10
|
+
# New here? A guided, first-timer setup (checks prerequisites, helps you go mobile):
|
|
11
|
+
npx anyagent-bridge setup
|
|
12
|
+
|
|
13
|
+
# Or run it directly (Node 18+):
|
|
11
14
|
npx anyagent-bridge
|
|
12
15
|
|
|
13
16
|
# Or self-host with Docker:
|
|
14
17
|
docker compose up -d --build && docker compose logs bridge
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
Open the printed URL, paste the access token from the banner, and you're in.
|
|
18
|
-
|
|
20
|
+
Open the printed URL, paste the access token from the banner, and you're in. To open
|
|
21
|
+
it on a phone or another PC, click **"📱 Connect a device"** in the top bar and scan
|
|
22
|
+
the QR. New to all this? Start with **[docs/GETTING-STARTED.md](docs/GETTING-STARTED.md)**.
|
|
23
|
+
Full install paths (npx · from source · Docker) and per-OS notes are in
|
|
19
24
|
**[docs/INSTALL.md](docs/INSTALL.md)**; read **[docs/SECURITY.md](docs/SECURITY.md)**
|
|
20
25
|
before exposing the bridge beyond localhost, and
|
|
21
26
|
**[docs/WALKTHROUGH.md](docs/WALKTHROUGH.md)** for a guided tour.
|
|
@@ -34,6 +39,7 @@ before exposing the bridge beyond localhost, and
|
|
|
34
39
|
- Persistent sessions that survive reconnects, with automatic PTY respawn and backoff.
|
|
35
40
|
- Heartbeat + dead-connection detection so stale viewers get cleaned up.
|
|
36
41
|
- File management API: browse, read, write, rename, move, delete, upload, download — all behind a path whitelist.
|
|
42
|
+
- Add projects by **browsing folders** in the UI (the 📁 Projects button) — no typing full paths.
|
|
37
43
|
- Crash guards (uncaught exceptions, signals) so the server stays up.
|
|
38
44
|
- Constant-time token comparison and basic rate limiting.
|
|
39
45
|
- Optional **login**: Google/GitHub OAuth, TOTP 2FA, and signed expiring sessions on top of the token (Stage 3).
|
package/bin/anyagent-bridge.js
CHANGED
|
@@ -28,6 +28,7 @@ function printHelp() {
|
|
|
28
28
|
'',
|
|
29
29
|
'Usage:',
|
|
30
30
|
' anyagent-bridge [options]',
|
|
31
|
+
' anyagent-bridge setup Guided, first-timer setup (recommended to start).',
|
|
31
32
|
'',
|
|
32
33
|
'Options:',
|
|
33
34
|
' -p, --port <n> Port to listen on (default 3001).',
|
|
@@ -53,6 +54,11 @@ function fail(msg) {
|
|
|
53
54
|
process.exit(1);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// Subcommand: `anyagent-bridge setup` launches the guided first-timer wizard.
|
|
58
|
+
// Anything else is the normal flag-parse + boot below.
|
|
59
|
+
if (argv[0] === 'setup') {
|
|
60
|
+
require(path.join(__dirname, 'setup.js'));
|
|
61
|
+
} else {
|
|
56
62
|
for (let i = 0; i < argv.length; i++) {
|
|
57
63
|
let arg = argv[i];
|
|
58
64
|
// Support the `--flag=value` form for long options. Splitting it out here also
|
|
@@ -125,3 +131,4 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
125
131
|
// Boot the server in-process. ROOT inside the server resolves relative to its own
|
|
126
132
|
// __dirname, so config.json and .data live alongside the installed package.
|
|
127
133
|
require(path.join(__dirname, '..', 'server', 'index.js'));
|
|
134
|
+
}
|
package/bin/setup.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* anyagent-bridge setup — a friendly, first-timer guided launcher.
|
|
4
|
+
*
|
|
5
|
+
* Invoked as `npx anyagent-bridge setup` (the launcher dispatches the `setup`
|
|
6
|
+
* subcommand here). It asks a few plain questions, checks prerequisites, helps
|
|
7
|
+
* you pick where you'll open the bridge (this computer / same Wi-Fi / phone over
|
|
8
|
+
* the internet), then boots the normal server with the matching settings — it
|
|
9
|
+
* only sets the same PORT / HOST / BRIDGE_* env vars the launcher already uses,
|
|
10
|
+
* so nothing here changes how the server runs. The scannable phone QR lives in
|
|
11
|
+
* the browser UI ("Connect a device" → Phone); this wizard points you to it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const readline = require('readline');
|
|
21
|
+
const pkg = require('../package.json');
|
|
22
|
+
|
|
23
|
+
// ── tiny ANSI helpers (no dependency) ─────────────────────────────────────────
|
|
24
|
+
const useColor = process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
25
|
+
const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
26
|
+
const bold = (s) => c('1', s);
|
|
27
|
+
const dim = (s) => c('2', s);
|
|
28
|
+
const cyan = (s) => c('36', s);
|
|
29
|
+
const green = (s) => c('32', s);
|
|
30
|
+
const yellow = (s) => c('33', s);
|
|
31
|
+
const out = (s = '') => process.stdout.write(s + '\n');
|
|
32
|
+
const rule = () => out(dim('─'.repeat(63)));
|
|
33
|
+
|
|
34
|
+
// ── prerequisite helpers ──────────────────────────────────────────────────────
|
|
35
|
+
// Cross-platform "is this command on PATH?" without spawning the command itself.
|
|
36
|
+
function onPath(bin) {
|
|
37
|
+
const exts = process.platform === 'win32'
|
|
38
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';')
|
|
39
|
+
: [''];
|
|
40
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
41
|
+
if (!dir) continue;
|
|
42
|
+
for (const ext of exts) {
|
|
43
|
+
const p = path.join(dir, bin + ext);
|
|
44
|
+
try { fs.accessSync(p, fs.constants.X_OK); return true; } catch (_) { /* keep looking */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function firstLanIPv4() {
|
|
51
|
+
const ifaces = os.networkInterfaces();
|
|
52
|
+
for (const name of Object.keys(ifaces)) {
|
|
53
|
+
for (const ni of ifaces[name] || []) {
|
|
54
|
+
if (ni.family === 'IPv4' && !ni.internal) return ni.address;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── interactive prompt (degrades gracefully without a TTY) ─────────────────────
|
|
61
|
+
let rl = null;
|
|
62
|
+
function ask(question, choices) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
if (!process.stdin.isTTY) { resolve(choices ? choices[0].key : ''); return; }
|
|
65
|
+
if (!rl) rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
66
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pick(title, choices) {
|
|
71
|
+
out();
|
|
72
|
+
out(bold(title));
|
|
73
|
+
for (const ch of choices) out(` ${cyan(ch.key)}) ${ch.label}${ch.hint ? dim(' — ' + ch.hint) : ''}`);
|
|
74
|
+
if (!process.stdin.isTTY) {
|
|
75
|
+
out(dim(' (no interactive terminal — defaulting to option ' + choices[0].key + ')'));
|
|
76
|
+
return choices[0];
|
|
77
|
+
}
|
|
78
|
+
for (;;) {
|
|
79
|
+
const a = (await ask(`${dim('choose')} [${choices[0].key}]: `)) || choices[0].key;
|
|
80
|
+
const found = choices.find((ch) => ch.key === a.toLowerCase());
|
|
81
|
+
if (found) return found;
|
|
82
|
+
out(yellow(` "${a}" isn't one of the choices — try again.`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function confirm(question, def = true) {
|
|
87
|
+
if (!process.stdin.isTTY) return def;
|
|
88
|
+
const a = (await ask(`${question} ${dim(def ? '[Y/n]' : '[y/N]')} `)).toLowerCase();
|
|
89
|
+
if (!a) return def;
|
|
90
|
+
return a.startsWith('y');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── main flow ─────────────────────────────────────────────────────────────────
|
|
94
|
+
(async function main() {
|
|
95
|
+
out();
|
|
96
|
+
rule();
|
|
97
|
+
out(' ' + bold('AnyAgent Bridge') + dim(` · setup · v${pkg.version}`));
|
|
98
|
+
out(' ' + dim('Run your terminal + AI agents from a browser, phone, or another PC.'));
|
|
99
|
+
rule();
|
|
100
|
+
|
|
101
|
+
// 1) prerequisites -----------------------------------------------------------
|
|
102
|
+
out();
|
|
103
|
+
out(bold('1. Checking what you have'));
|
|
104
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
105
|
+
out(` ${nodeMajor >= 18 ? green('✓') : yellow('!')} Node.js ${process.version}` +
|
|
106
|
+
(nodeMajor >= 18 ? '' : yellow(' (needs 18+; please upgrade)')));
|
|
107
|
+
const agents = [
|
|
108
|
+
{ id: 'claude', label: 'Claude Code (claude)' },
|
|
109
|
+
{ id: 'codex', label: 'Codex (codex)' },
|
|
110
|
+
].map((a) => ({ ...a, found: onPath(a.id) }));
|
|
111
|
+
for (const a of agents) {
|
|
112
|
+
out(` ${a.found ? green('✓') : dim('·')} ${a.label}${a.found ? '' : dim(' — not found on PATH')}`);
|
|
113
|
+
}
|
|
114
|
+
if (!agents.some((a) => a.found)) {
|
|
115
|
+
out();
|
|
116
|
+
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 install one first, e.g. Claude Code:'));
|
|
118
|
+
out(' ' + cyan('npm install -g @anthropic-ai/claude-code') + dim(' then run `claude` once to log in.'));
|
|
119
|
+
out(dim(' (Any CLI works — register it under "agents" in config.json.)'));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 2) where will you open it? --------------------------------------------------
|
|
123
|
+
const where = await pick('2. Where do you want to open the bridge?', [
|
|
124
|
+
{ key: '1', label: 'This computer', hint: "open it in this machine's browser" },
|
|
125
|
+
{ key: '2', label: 'Another device on the same Wi-Fi', hint: 'a phone or PC on your network' },
|
|
126
|
+
{ key: '3', label: 'Your phone / anywhere over the internet', hint: 'via a free tunnel' },
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// settings we will hand to the server (same env the launcher uses)
|
|
130
|
+
const port = process.env.PORT || '3001';
|
|
131
|
+
const token = process.env.BRIDGE_AUTH_TOKEN || crypto.randomBytes(32).toString('hex');
|
|
132
|
+
let host = '127.0.0.1';
|
|
133
|
+
let tunnelProvider = null;
|
|
134
|
+
const nextSteps = [];
|
|
135
|
+
|
|
136
|
+
if (where.key === '1') {
|
|
137
|
+
host = '127.0.0.1';
|
|
138
|
+
nextSteps.push(`Open ${cyan(`http://127.0.0.1:${port}/?token=…`)} in your browser (the full link with the token prints below).`);
|
|
139
|
+
nextSteps.push(`Only this computer can reach it. Nothing is exposed to your network or the internet.`);
|
|
140
|
+
} else if (where.key === '2') {
|
|
141
|
+
host = '0.0.0.0';
|
|
142
|
+
const ip = firstLanIPv4();
|
|
143
|
+
nextSteps.push(ip
|
|
144
|
+
? `On the other device's browser, go to ${cyan(`http://${ip}:${port}/`)} and paste the access token.`
|
|
145
|
+
: `Find this computer's local IP (e.g. ${dim('System Settings → Network')}), then visit ${cyan(`http://<that-ip>:${port}/`)} on the other device.`);
|
|
146
|
+
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
|
+
nextSteps.push(`Easiest on a phone: open the page on this computer, click ${bold('"Connect a device" → Phone')}, and scan the QR.`);
|
|
148
|
+
} else {
|
|
149
|
+
host = '127.0.0.1'; // the tunnel reaches in; we don't bind to the network directly
|
|
150
|
+
out();
|
|
151
|
+
out(yellow(' ⚠ Going over the internet means anyone with the link could reach the page.'));
|
|
152
|
+
out(yellow(' The access token still gates it, but before you rely on this you should'));
|
|
153
|
+
out(yellow(' turn on login / 2FA / OAuth — see docs/SECURITY.md.'));
|
|
154
|
+
const ok = await confirm(' Understood — set up an internet tunnel now?', true);
|
|
155
|
+
if (ok) {
|
|
156
|
+
const prov = await pick(' Which free tunnel?', [
|
|
157
|
+
{ key: '1', label: 'Microsoft Dev Tunnels', hint: 'needs a one-time `devtunnel user login`' },
|
|
158
|
+
{ key: '2', label: 'Cloudflare Quick Tunnel', hint: 'no account; testing-grade URL' },
|
|
159
|
+
]);
|
|
160
|
+
tunnelProvider = prov.key === '2' ? 'cloudflare-quick' : 'devtunnel';
|
|
161
|
+
const cli = tunnelProvider === 'cloudflare-quick' ? 'cloudflared' : 'devtunnel';
|
|
162
|
+
if (!onPath(cli)) {
|
|
163
|
+
out(yellow(` The "${cli}" command isn't installed yet.`));
|
|
164
|
+
out(` Install it, then re-run setup. Docs: ${cyan('docs/INSTALL.md')} (Remote access).`);
|
|
165
|
+
out(dim(' Continuing anyway — the server will fall back to localhost-only if the tunnel cannot start.'));
|
|
166
|
+
}
|
|
167
|
+
nextSteps.push(`When the tunnel connects, its public ${bold('https://…')} URL prints in the banner below and shows in the UI.`);
|
|
168
|
+
nextSteps.push(`Open that URL on your phone, or use ${bold('"Connect a device" → Phone')} in the UI to scan a QR.`);
|
|
169
|
+
nextSteps.push(`${yellow('Before sharing it:')} open the UI, enable login/2FA, and read docs/SECURITY.md.`);
|
|
170
|
+
} else {
|
|
171
|
+
host = '127.0.0.1';
|
|
172
|
+
nextSteps.push(`No tunnel started — running on this computer only. Re-run ${cyan('anyagent-bridge setup')} when you're ready to go remote.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 3) summary + launch ---------------------------------------------------------
|
|
177
|
+
out();
|
|
178
|
+
out(bold('3. Starting the bridge'));
|
|
179
|
+
out(` ${dim('mode ')} ${where.label}`);
|
|
180
|
+
out(` ${dim('listen ')} ${host}:${port}`);
|
|
181
|
+
out(` ${dim('tunnel ')} ${tunnelProvider || 'off'}`);
|
|
182
|
+
out();
|
|
183
|
+
out(bold(' Next:'));
|
|
184
|
+
for (const s of nextSteps) out(` ${green('→')} ${s}`);
|
|
185
|
+
out();
|
|
186
|
+
out(dim(' Full beginner guide: docs/GETTING-STARTED.md · Security: docs/SECURITY.md'));
|
|
187
|
+
rule();
|
|
188
|
+
out();
|
|
189
|
+
|
|
190
|
+
if (rl) rl.close();
|
|
191
|
+
|
|
192
|
+
// Hand the chosen settings to the real server via the same env vars the
|
|
193
|
+
// launcher uses, then boot it in-process. Its own banner (URL, token, and any
|
|
194
|
+
// tunnel URL) prints next.
|
|
195
|
+
process.env.PORT = port;
|
|
196
|
+
process.env.HOST = host;
|
|
197
|
+
process.env.BRIDGE_AUTH_TOKEN = token;
|
|
198
|
+
if (tunnelProvider) {
|
|
199
|
+
process.env.BRIDGE_TUNNEL_ENABLED = 'true';
|
|
200
|
+
process.env.BRIDGE_TUNNEL_PROVIDER = tunnelProvider;
|
|
201
|
+
}
|
|
202
|
+
require(path.join(__dirname, '..', 'server', 'index.js'));
|
|
203
|
+
})().catch((e) => {
|
|
204
|
+
process.stderr.write('setup failed: ' + (e && e.stack ? e.stack : e) + '\n');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
package/client/index.html
CHANGED
|
@@ -105,6 +105,75 @@
|
|
|
105
105
|
select, button, input { font-size: 12px; padding: 7px 9px; }
|
|
106
106
|
#status { margin-left: auto; }
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/* Connect-a-device onboarding */
|
|
110
|
+
#onboard {
|
|
111
|
+
position: fixed; inset: 0; z-index: 60;
|
|
112
|
+
background: rgba(13,17,23,.96);
|
|
113
|
+
display: none; align-items: center; justify-content: center; padding: 18px;
|
|
114
|
+
}
|
|
115
|
+
#onboard.open { display: flex; }
|
|
116
|
+
#onboard .card {
|
|
117
|
+
position: relative;
|
|
118
|
+
background: var(--bar); border: 1px solid var(--border); border-radius: 12px;
|
|
119
|
+
width: 100%; max-width: 460px; max-height: 90vh; overflow-y: auto;
|
|
120
|
+
padding: 20px 22px; box-shadow: 0 12px 40px rgba(0,0,0,.5);
|
|
121
|
+
}
|
|
122
|
+
#onboard h2 { margin: 0 0 2px; font-size: 17px; }
|
|
123
|
+
#onboard h2 .dot { color: var(--accent); }
|
|
124
|
+
#onboard .sub { margin: 0 0 14px; color: var(--muted); font-size: 12.5px; line-height: 1.5; }
|
|
125
|
+
#onboard .ob-close {
|
|
126
|
+
position: absolute; top: 12px; right: 14px;
|
|
127
|
+
background: transparent; border: none; color: var(--muted);
|
|
128
|
+
font-size: 22px; line-height: 1; cursor: pointer; padding: 2px 6px;
|
|
129
|
+
}
|
|
130
|
+
#onboard .ob-close:hover { color: var(--fg); }
|
|
131
|
+
#onboard .qrbox {
|
|
132
|
+
display: flex; align-items: center; justify-content: center;
|
|
133
|
+
background: #fff; border-radius: 10px; padding: 14px;
|
|
134
|
+
margin: 0 auto 8px; width: max-content;
|
|
135
|
+
}
|
|
136
|
+
#onboard .qrbox img, #onboard .qrbox canvas { display: block; }
|
|
137
|
+
#onboard .qrcap { color: var(--muted); font-size: 12px; text-align: center; margin: 0 0 6px; }
|
|
138
|
+
#onboard .blk { border-top: 1px solid var(--border); padding: 12px 0 4px; }
|
|
139
|
+
#onboard .blk:first-of-type { border-top: none; }
|
|
140
|
+
#onboard .blk h3 { margin: 0 0 6px; font-size: 13px; }
|
|
141
|
+
#onboard .blk p { margin: 0 0 8px; font-size: 12.5px; color: var(--fg); line-height: 1.55; }
|
|
142
|
+
#onboard code {
|
|
143
|
+
background: #0d1117; border: 1px solid var(--border); border-radius: 5px;
|
|
144
|
+
padding: 1px 6px; font-size: 12px; word-break: break-all;
|
|
145
|
+
}
|
|
146
|
+
#onboard .warn { color: var(--yellow); }
|
|
147
|
+
#onboard .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
|
|
148
|
+
#onboard .muted { color: var(--muted); font-size: 12px; }
|
|
149
|
+
|
|
150
|
+
/* Projects — folder-browser picker */
|
|
151
|
+
#projModal { position: fixed; inset: 0; z-index: 60; background: rgba(13,17,23,.96); display: none; align-items: center; justify-content: center; padding: 18px; }
|
|
152
|
+
#projModal.open { display: flex; }
|
|
153
|
+
#projModal .card { position: relative; background: var(--bar); border: 1px solid var(--border); border-radius: 12px; width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto; padding: 20px 22px; box-shadow: 0 12px 40px rgba(0,0,0,.5); }
|
|
154
|
+
#projModal h2 { margin: 0 0 2px; font-size: 17px; }
|
|
155
|
+
#projModal .sub { margin: 0 0 12px; color: var(--muted); font-size: 12.5px; line-height: 1.5; }
|
|
156
|
+
#projModal .pm-close { position: absolute; top: 12px; right: 14px; background: transparent; border: none; color: var(--muted); font-size: 22px; line-height: 1; cursor: pointer; padding: 2px 6px; }
|
|
157
|
+
#projModal .pm-close:hover { color: var(--fg); }
|
|
158
|
+
#projModal .pm-list { list-style: none; margin: 0 0 12px; padding: 0; }
|
|
159
|
+
#projModal .pm-list li { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 7px; margin-bottom: 6px; }
|
|
160
|
+
#projModal .pm-nm { font-size: 13px; font-weight: 600; white-space: nowrap; }
|
|
161
|
+
#projModal .pm-pa { font-size: 11px; color: var(--muted); word-break: break-all; flex: 1; }
|
|
162
|
+
#projModal .pm-del { background: transparent; border: none; color: var(--red); cursor: pointer; font-size: 14px; padding: 0 4px; }
|
|
163
|
+
#projModal .pm-empty { color: var(--muted); font-size: 12.5px; margin: 0 0 12px; }
|
|
164
|
+
#projModal .pm-add { border-top: 1px solid var(--border); padding-top: 12px; }
|
|
165
|
+
#projModal .pm-add h3 { margin: 0 0 8px; font-size: 13px; }
|
|
166
|
+
#projModal .pm-crumb { font-size: 12px; color: var(--accent); word-break: break-all; margin: 0 0 6px; }
|
|
167
|
+
#projModal .pm-browser { border: 1px solid var(--border); border-radius: 7px; max-height: 190px; overflow-y: auto; margin-bottom: 8px; background: #0d1117; }
|
|
168
|
+
#projModal .pm-row { display: flex; align-items: center; gap: 8px; padding: 7px 10px; cursor: pointer; font-size: 13px; border-bottom: 1px solid rgba(48,54,61,.5); }
|
|
169
|
+
#projModal .pm-row:last-child { border-bottom: none; }
|
|
170
|
+
#projModal .pm-row:hover { background: #161b22; }
|
|
171
|
+
#projModal .pm-row.up { color: var(--muted); }
|
|
172
|
+
#projModal .pm-sel { font-size: 12px; color: var(--fg); margin: 0 0 8px; word-break: break-all; }
|
|
173
|
+
#projModal .pm-sel b { color: var(--green); }
|
|
174
|
+
#projModal .pm-fields { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
175
|
+
#projModal .pm-fields input { flex: 1; min-width: 150px; }
|
|
176
|
+
#projModal .pm-err { color: var(--red); font-size: 12px; min-height: 15px; margin: 6px 0 0; }
|
|
108
177
|
</style>
|
|
109
178
|
</head>
|
|
110
179
|
<body>
|
|
@@ -113,7 +182,9 @@
|
|
|
113
182
|
<span class="title">AnyAgent<span class="dot">·</span>Bridge</span>
|
|
114
183
|
<select id="agentSel" title="Agent"></select>
|
|
115
184
|
<button id="startBtn">Start</button>
|
|
185
|
+
<button id="connectBtn" title="Open on a phone or another device">📱 Connect a device</button>
|
|
116
186
|
<select id="projectSel" title="Project" style="display:none"></select>
|
|
187
|
+
<button id="projBtn" title="Add or manage projects by browsing folders">📁 Projects</button>
|
|
117
188
|
<span class="spacer"></span>
|
|
118
189
|
<span id="status" class="disconnected"><span class="led"></span><span id="statusText">disconnected</span></span>
|
|
119
190
|
</div>
|
|
@@ -140,6 +211,53 @@
|
|
|
140
211
|
</div>
|
|
141
212
|
</div>
|
|
142
213
|
|
|
214
|
+
<div id="onboard">
|
|
215
|
+
<div class="card">
|
|
216
|
+
<button class="ob-close" id="obClose" aria-label="Close">×</button>
|
|
217
|
+
<h2>Connect a device</h2>
|
|
218
|
+
<p class="sub">Open this bridge on your phone or another computer. Scan the code, or follow the steps below.</p>
|
|
219
|
+
<div class="qrbox" id="obQrBox"><div id="obQr"></div></div>
|
|
220
|
+
<p class="qrcap" id="obQrCap"></p>
|
|
221
|
+
<div class="blk" id="obPhone">
|
|
222
|
+
<h3>📱 On your phone</h3>
|
|
223
|
+
<p id="obPhoneMsg">Preparing…</p>
|
|
224
|
+
<div class="row" id="obTunnelRow"></div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="blk">
|
|
227
|
+
<h3>💻 This computer</h3>
|
|
228
|
+
<p>Open <code id="obLocalUrl">…</code> in this machine's browser.</p>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="blk">
|
|
231
|
+
<h3>🖥 Another PC on the same Wi-Fi</h3>
|
|
232
|
+
<p id="obLanMsg">…</p>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="blk">
|
|
235
|
+
<p class="warn">Beyond this computer, the access token is the only lock until you turn on login — set up login / 2FA / OAuth and read <code>docs/SECURITY.md</code> before exposing it.</p>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div id="projModal">
|
|
241
|
+
<div class="card">
|
|
242
|
+
<button class="pm-close" id="pmClose" aria-label="Close">×</button>
|
|
243
|
+
<h2>Projects</h2>
|
|
244
|
+
<p class="sub">A project scopes an agent session to a folder. Add one by browsing — no typing the full path.</p>
|
|
245
|
+
<ul class="pm-list" id="pmList"></ul>
|
|
246
|
+
<p class="pm-empty" id="pmEmpty" style="display:none">No projects yet.</p>
|
|
247
|
+
<div class="pm-add">
|
|
248
|
+
<h3>Add a project</h3>
|
|
249
|
+
<p class="pm-crumb" id="pmCrumb">…</p>
|
|
250
|
+
<div class="pm-browser" id="pmBrowser"></div>
|
|
251
|
+
<p class="pm-sel">Selected folder: <b id="pmSel">…</b></p>
|
|
252
|
+
<div class="pm-fields">
|
|
253
|
+
<input id="pmName" type="text" placeholder="project name" autocomplete="off" autocapitalize="off" spellcheck="false" />
|
|
254
|
+
<button class="primary" id="pmAdd">Add project</button>
|
|
255
|
+
</div>
|
|
256
|
+
<p class="pm-err" id="pmErr"></p>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
143
261
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
144
262
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
145
263
|
<script>
|
|
@@ -483,6 +601,205 @@
|
|
|
483
601
|
} catch (_) {}
|
|
484
602
|
}
|
|
485
603
|
|
|
604
|
+
// ---------- Connect-a-device onboarding ----------
|
|
605
|
+
const connectBtn = $("connectBtn"), onboard = $("onboard"), obClose = $("obClose");
|
|
606
|
+
let qrLibPromise = null, tunnelPolling = false;
|
|
607
|
+
function loadQrLib() {
|
|
608
|
+
if (window.QRCode) return Promise.resolve();
|
|
609
|
+
if (qrLibPromise) return qrLibPromise;
|
|
610
|
+
qrLibPromise = new Promise((resolve, reject) => {
|
|
611
|
+
const s = document.createElement("script");
|
|
612
|
+
s.src = "https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js";
|
|
613
|
+
s.onload = () => resolve();
|
|
614
|
+
s.onerror = () => reject(new Error("QR library failed to load"));
|
|
615
|
+
document.head.appendChild(s);
|
|
616
|
+
});
|
|
617
|
+
return qrLibPromise;
|
|
618
|
+
}
|
|
619
|
+
// The QR carries the access token so the device logs in by scanning. With a
|
|
620
|
+
// cookie-only (OAuth) session there is no JS token, so it links to the page
|
|
621
|
+
// and the device logs in there.
|
|
622
|
+
function loginQuery() { return token ? ("/?token=" + encodeURIComponent(token)) : "/"; }
|
|
623
|
+
async function showQr(url) {
|
|
624
|
+
const box = $("obQrBox"), holder = $("obQr");
|
|
625
|
+
try {
|
|
626
|
+
await loadQrLib();
|
|
627
|
+
holder.innerHTML = "";
|
|
628
|
+
new window.QRCode(holder, { text: url, width: 196, height: 196, correctLevel: window.QRCode.CorrectLevel.M });
|
|
629
|
+
box.style.display = "flex";
|
|
630
|
+
} catch (e) {
|
|
631
|
+
box.style.display = "none";
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function openOnboard() {
|
|
635
|
+
onboard.classList.add("open");
|
|
636
|
+
const port = location.port || (location.protocol === "https:" ? "443" : "80");
|
|
637
|
+
$("obLocalUrl").textContent = location.origin + "/";
|
|
638
|
+
$("obLanMsg").innerHTML =
|
|
639
|
+
'Restart the bridge so it listens on your network — run <code>anyagent-bridge setup</code> and pick ' +
|
|
640
|
+
'"same Wi-Fi", or add <code>--host 0.0.0.0</code> — then on the other device open ' +
|
|
641
|
+
'<code>http://<this-computer-ip>:' + port + '/</code> and paste the token.';
|
|
642
|
+
const phoneMsg = $("obPhoneMsg"), tunnelRow = $("obTunnelRow"), qrCap = $("obQrCap");
|
|
643
|
+
tunnelRow.innerHTML = "";
|
|
644
|
+
let sys = null;
|
|
645
|
+
try { sys = await api("/api/system/status"); } catch (_) {}
|
|
646
|
+
const tunnelUrl = sys && sys.tunnel && sys.tunnel.url;
|
|
647
|
+
const isLocalHost = location.hostname === "127.0.0.1" || location.hostname === "localhost";
|
|
648
|
+
if (tunnelUrl) {
|
|
649
|
+
phoneMsg.textContent = "A live internet tunnel is running. Scan to open it from anywhere:";
|
|
650
|
+
qrCap.textContent = "Point your phone's camera at this code.";
|
|
651
|
+
showQr(tunnelUrl.replace(/\/+$/, "") + loginQuery());
|
|
652
|
+
} else if (!isLocalHost) {
|
|
653
|
+
phoneMsg.textContent = "Your phone must be on the same Wi-Fi as this computer. Scan to open:";
|
|
654
|
+
qrCap.textContent = "Point your phone's camera at this code.";
|
|
655
|
+
showQr(location.origin + loginQuery());
|
|
656
|
+
} else {
|
|
657
|
+
qrCap.textContent = "";
|
|
658
|
+
$("obQrBox").style.display = "none";
|
|
659
|
+
phoneMsg.innerHTML = "This bridge is only listening on this computer, so a phone can't reach it yet. Start a free internet tunnel:";
|
|
660
|
+
const btn = document.createElement("button");
|
|
661
|
+
btn.textContent = "Start internet tunnel";
|
|
662
|
+
btn.className = "primary";
|
|
663
|
+
btn.addEventListener("click", () => startTunnel(btn));
|
|
664
|
+
tunnelRow.appendChild(btn);
|
|
665
|
+
const note = document.createElement("span");
|
|
666
|
+
note.className = "muted";
|
|
667
|
+
note.textContent = "Turn on login/2FA first if anyone else could reach it.";
|
|
668
|
+
tunnelRow.appendChild(note);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function startTunnel(btn) {
|
|
672
|
+
if (tunnelPolling) return; // ignore a second click while one poll is in flight
|
|
673
|
+
tunnelPolling = true;
|
|
674
|
+
btn.disabled = true; btn.textContent = "Starting…";
|
|
675
|
+
try {
|
|
676
|
+
await fetch("/api/tunnel/start", { method: "POST", credentials: "include", headers: token ? { Authorization: "Bearer " + token } : {} });
|
|
677
|
+
} catch (_) {}
|
|
678
|
+
for (let i = 0; i < 20; i++) {
|
|
679
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
680
|
+
if (!onboard.classList.contains("open")) { tunnelPolling = false; return; } // panel closed — stop polling
|
|
681
|
+
let sys; try { sys = await api("/api/system/status"); } catch (_) { continue; }
|
|
682
|
+
const url = sys && sys.tunnel && sys.tunnel.url;
|
|
683
|
+
if (url) {
|
|
684
|
+
tunnelPolling = false;
|
|
685
|
+
btn.textContent = "Tunnel ready ✓";
|
|
686
|
+
$("obPhoneMsg").textContent = "Scan to open over the internet:";
|
|
687
|
+
$("obQrCap").textContent = "Point your phone's camera at this code.";
|
|
688
|
+
showQr(url.replace(/\/+$/, "") + loginQuery());
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
tunnelPolling = false;
|
|
693
|
+
btn.disabled = false; btn.textContent = "Start internet tunnel";
|
|
694
|
+
$("obPhoneMsg").innerHTML = "The tunnel didn't come up — the tunnel CLI may not be installed. See <code>docs/INSTALL.md</code> (Remote access).";
|
|
695
|
+
}
|
|
696
|
+
function closeOnboard() { onboard.classList.remove("open"); }
|
|
697
|
+
connectBtn.addEventListener("click", openOnboard);
|
|
698
|
+
obClose.addEventListener("click", closeOnboard);
|
|
699
|
+
onboard.addEventListener("click", (e) => { if (e.target === onboard) closeOnboard(); });
|
|
700
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && onboard.classList.contains("open")) closeOnboard(); });
|
|
701
|
+
|
|
702
|
+
// ---------- Projects: folder-browser picker (reuses GET /api/browse) ----------
|
|
703
|
+
const projBtn = $("projBtn"), projModal = $("projModal"), pmClose = $("pmClose");
|
|
704
|
+
let pmCurrent = null;
|
|
705
|
+
function pmBasename(p) { return p.replace(/[\/\\]+$/, "").split(/[\/\\]/).pop() || p; }
|
|
706
|
+
async function pmBrowse(p) {
|
|
707
|
+
const browser = $("pmBrowser"), err = $("pmErr");
|
|
708
|
+
err.textContent = "";
|
|
709
|
+
// Raw fetch (not api()) so the server's own message survives a 403 — e.g. a
|
|
710
|
+
// folder outside the configured allowedPaths returns "Access denied", which is
|
|
711
|
+
// far more useful than a generic "unauthorized".
|
|
712
|
+
let res, data;
|
|
713
|
+
try {
|
|
714
|
+
res = await fetch("/api/browse" + (p ? "?path=" + encodeURIComponent(p) : ""),
|
|
715
|
+
{ credentials: "include", headers: token ? { Authorization: "Bearer " + token } : {} });
|
|
716
|
+
data = await res.json().catch(() => ({}));
|
|
717
|
+
} catch (e) { err.textContent = "Could not reach the server."; return; }
|
|
718
|
+
if (res.status === 401) { err.textContent = "Session expired — reload to log in again."; return; }
|
|
719
|
+
if (!res.ok || data.error) { err.textContent = data.error || ("Could not open that folder (HTTP " + res.status + ")."); return; }
|
|
720
|
+
pmCurrent = data.current;
|
|
721
|
+
$("pmCrumb").textContent = data.current;
|
|
722
|
+
$("pmSel").textContent = data.current;
|
|
723
|
+
// keep the name field in sync with the folder until the user types their own
|
|
724
|
+
if ($("pmName").dataset.auto !== "0") {
|
|
725
|
+
$("pmName").value = pmBasename(data.current);
|
|
726
|
+
$("pmName").dataset.auto = "1";
|
|
727
|
+
}
|
|
728
|
+
browser.innerHTML = "";
|
|
729
|
+
if (data.parent) {
|
|
730
|
+
const up = document.createElement("div");
|
|
731
|
+
up.className = "pm-row up"; up.textContent = "⬆ ..";
|
|
732
|
+
up.addEventListener("click", () => pmBrowse(data.parent));
|
|
733
|
+
browser.appendChild(up);
|
|
734
|
+
}
|
|
735
|
+
if (!data.folders.length) {
|
|
736
|
+
const none = document.createElement("div");
|
|
737
|
+
none.className = "pm-row"; none.style.cursor = "default"; none.textContent = "(no subfolders here)";
|
|
738
|
+
browser.appendChild(none);
|
|
739
|
+
}
|
|
740
|
+
for (const f of data.folders) {
|
|
741
|
+
const row = document.createElement("div");
|
|
742
|
+
row.className = "pm-row"; row.textContent = "📁 " + f.name;
|
|
743
|
+
row.addEventListener("click", () => pmBrowse(f.path));
|
|
744
|
+
browser.appendChild(row);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function pmLoadList() {
|
|
748
|
+
const ul = $("pmList");
|
|
749
|
+
ul.innerHTML = "";
|
|
750
|
+
let list = [];
|
|
751
|
+
try { const d = await api("/api/projects"); list = d.projects || []; } catch (_) {}
|
|
752
|
+
$("pmEmpty").style.display = list.length ? "none" : "";
|
|
753
|
+
for (const p of list) {
|
|
754
|
+
const li = document.createElement("li");
|
|
755
|
+
const nm = document.createElement("span"); nm.className = "pm-nm"; nm.textContent = p.name;
|
|
756
|
+
const pa = document.createElement("span"); pa.className = "pm-pa"; pa.textContent = p.path;
|
|
757
|
+
const del = document.createElement("button"); del.className = "pm-del"; del.textContent = "✕"; del.title = "Remove";
|
|
758
|
+
del.addEventListener("click", () => pmDelete(p.name));
|
|
759
|
+
li.appendChild(nm); li.appendChild(pa); li.appendChild(del);
|
|
760
|
+
ul.appendChild(li);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function pmAddProject() {
|
|
764
|
+
const err = $("pmErr"), name = $("pmName").value.trim();
|
|
765
|
+
if (!pmCurrent) { err.textContent = "Pick a folder first."; return; }
|
|
766
|
+
if (!name) { err.textContent = "Give the project a name."; return; }
|
|
767
|
+
err.textContent = "";
|
|
768
|
+
let res, data;
|
|
769
|
+
try {
|
|
770
|
+
res = await fetch("/api/projects", { method: "POST", credentials: "include",
|
|
771
|
+
headers: Object.assign({ "Content-Type": "application/json" }, token ? { Authorization: "Bearer " + token } : {}),
|
|
772
|
+
body: JSON.stringify({ name: name, path: pmCurrent }) });
|
|
773
|
+
data = await res.json().catch(() => ({}));
|
|
774
|
+
} catch (e) { err.textContent = "Network error."; return; }
|
|
775
|
+
if (!res.ok || data.error) { err.textContent = data.error || ("HTTP " + res.status); return; }
|
|
776
|
+
$("pmName").value = ""; $("pmName").dataset.auto = "1";
|
|
777
|
+
await pmLoadList();
|
|
778
|
+
try { await loadProjects(); } catch (_) {} // refresh the toolbar dropdown
|
|
779
|
+
}
|
|
780
|
+
async function pmDelete(name) {
|
|
781
|
+
try {
|
|
782
|
+
await fetch("/api/projects/" + encodeURIComponent(name), { method: "DELETE", credentials: "include",
|
|
783
|
+
headers: token ? { Authorization: "Bearer " + token } : {} });
|
|
784
|
+
} catch (_) {}
|
|
785
|
+
await pmLoadList();
|
|
786
|
+
try { await loadProjects(); } catch (_) {}
|
|
787
|
+
}
|
|
788
|
+
function openProjects() {
|
|
789
|
+
projModal.classList.add("open");
|
|
790
|
+
$("pmErr").textContent = "";
|
|
791
|
+
$("pmName").value = ""; $("pmName").dataset.auto = "1";
|
|
792
|
+
pmLoadList();
|
|
793
|
+
pmBrowse(null); // start at the server's home / first allowed base
|
|
794
|
+
}
|
|
795
|
+
function closeProjects() { projModal.classList.remove("open"); }
|
|
796
|
+
$("pmName").addEventListener("input", () => { $("pmName").dataset.auto = "0"; });
|
|
797
|
+
projBtn.addEventListener("click", openProjects);
|
|
798
|
+
pmClose.addEventListener("click", closeProjects);
|
|
799
|
+
projModal.addEventListener("click", (e) => { if (e.target === projModal) closeProjects(); });
|
|
800
|
+
$("pmAdd").addEventListener("click", pmAddProject);
|
|
801
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && projModal.classList.contains("open")) closeProjects(); });
|
|
802
|
+
|
|
486
803
|
// ---------- Bootstrap ----------
|
|
487
804
|
(async function boot() {
|
|
488
805
|
const params = new URLSearchParams(location.search);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Getting started (for first-timers)
|
|
2
|
+
|
|
3
|
+
New to the terminal or to AI coding agents? This is the gentlest path to running
|
|
4
|
+
anyagent-bridge and opening it on your computer, another PC, or your phone. If you
|
|
5
|
+
just want the short version, the [README](../README.md) Quick start has it; for the
|
|
6
|
+
full reference see [INSTALL.md](INSTALL.md), and **before you let anything but your
|
|
7
|
+
own computer reach the bridge, read [SECURITY.md](SECURITY.md).**
|
|
8
|
+
|
|
9
|
+
## What this is, in one line
|
|
10
|
+
|
|
11
|
+
anyagent-bridge puts your computer's terminal — and any AI coding agent you've
|
|
12
|
+
installed (like Claude Code) — onto a web page, so you can drive it from a browser
|
|
13
|
+
on the same computer, another PC, or your phone.
|
|
14
|
+
|
|
15
|
+
## What you need first
|
|
16
|
+
|
|
17
|
+
1. **Node.js 18 or newer.** Check by opening a terminal and running `node -v`. No
|
|
18
|
+
Node? Install it from <https://nodejs.org> (the "LTS" button).
|
|
19
|
+
2. **An AI agent CLI, optional but recommended.** e.g. Claude Code:
|
|
20
|
+
`npm install -g @anthropic-ai/claude-code`, then run `claude` once to log in.
|
|
21
|
+
Without one you still get a normal shell in the browser; you just can't launch
|
|
22
|
+
an agent until one is installed.
|
|
23
|
+
|
|
24
|
+
## Step 1 — Run the guided setup
|
|
25
|
+
|
|
26
|
+
In a terminal, run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx anyagent-bridge setup
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The first time, npm asks to download the package — say yes. Then a friendly wizard:
|
|
33
|
+
|
|
34
|
+
- checks your Node version and which agents (`claude`, `codex`) it can find,
|
|
35
|
+
- asks **where you want to open the bridge**, and
|
|
36
|
+
- starts the server with the right settings and prints your access link.
|
|
37
|
+
|
|
38
|
+
That's it — you do **not** need to edit any config files to get going.
|
|
39
|
+
|
|
40
|
+
## Step 2 — Pick where you'll open it
|
|
41
|
+
|
|
42
|
+
The wizard asks this; here's what each choice means.
|
|
43
|
+
|
|
44
|
+
### 💻 On this same computer (simplest)
|
|
45
|
+
|
|
46
|
+
Choose "This computer". When it starts, it prints a line like
|
|
47
|
+
`URL: http://127.0.0.1:3001/?token=…`. Open that whole link (including the
|
|
48
|
+
`?token=…` part) in your browser. You're in. Nothing is exposed to your network
|
|
49
|
+
or the internet.
|
|
50
|
+
|
|
51
|
+
### 🖥 Another PC or your phone on the same Wi-Fi
|
|
52
|
+
|
|
53
|
+
Choose "same Wi-Fi". The bridge starts listening on your network and the wizard
|
|
54
|
+
prints your computer's address, like `http://192.168.0.8:3001/`. On the other
|
|
55
|
+
device's browser, open that address and paste the access token (the long string
|
|
56
|
+
the wizard printed). Both devices must be on the same Wi-Fi.
|
|
57
|
+
|
|
58
|
+
> On your network, **the token is the only lock.** Keep it private.
|
|
59
|
+
|
|
60
|
+
### 📱 Your phone from anywhere (over the internet)
|
|
61
|
+
|
|
62
|
+
Choose "phone / anywhere". The wizard helps you turn on a free **tunnel** (a safe
|
|
63
|
+
public web address that points back to your computer). When it connects, a public
|
|
64
|
+
`https://…` address appears. Open it on your phone, or use the QR (next step).
|
|
65
|
+
|
|
66
|
+
> Going over the internet means the page is reachable by anyone with the link. The
|
|
67
|
+
> token still gates it, but **turn on login / 2FA before you rely on it** — see
|
|
68
|
+
> [SECURITY.md](SECURITY.md).
|
|
69
|
+
|
|
70
|
+
## Step 3 — Open it on your phone by scanning a QR
|
|
71
|
+
|
|
72
|
+
Once the bridge is open in a browser on your computer, click
|
|
73
|
+
**"📱 Connect a device"** in the top bar. A panel pops up with a **QR code** —
|
|
74
|
+
point your phone's camera at it and it opens the bridge on your phone, already
|
|
75
|
+
logged in. The same panel shows the steps for "this computer", "same Wi-Fi", and
|
|
76
|
+
starting an internet tunnel, so you never have to type the long token on a phone.
|
|
77
|
+
|
|
78
|
+
## Step 4 — Launch an AI agent
|
|
79
|
+
|
|
80
|
+
In the web terminal you have a real shell. To start an agent, pick it from the
|
|
81
|
+
dropdown in the top bar (e.g. **Claude Code**) and click **Start** — it runs inside
|
|
82
|
+
the session, streamed live to your browser. Detaching the browser keeps it alive;
|
|
83
|
+
reconnect and you're back with full scrollback.
|
|
84
|
+
|
|
85
|
+
To run the agent inside a specific **project folder**, click **📁 Projects** in the
|
|
86
|
+
top bar and *browse* to the folder — no typing the full path. It's saved and shows
|
|
87
|
+
up in the toolbar's project dropdown; pick one before launching.
|
|
88
|
+
|
|
89
|
+
## If something goes wrong
|
|
90
|
+
|
|
91
|
+
- **`node -v` says command not found** — install Node.js (above), then reopen the
|
|
92
|
+
terminal.
|
|
93
|
+
- **The agent dropdown is empty** — the agent CLI isn't installed or isn't on your
|
|
94
|
+
PATH. Verify it runs in the same terminal (e.g. type `claude`).
|
|
95
|
+
- **"Port already in use"** — something else uses port 3001. Run
|
|
96
|
+
`npx anyagent-bridge --port 8080` (any free number) or re-run `setup`.
|
|
97
|
+
- **The phone QR / tunnel didn't work** — the tunnel CLI may not be installed; see
|
|
98
|
+
[INSTALL.md](INSTALL.md) → Remote access. On the same Wi-Fi you don't need a
|
|
99
|
+
tunnel at all — use the "same Wi-Fi" address instead.
|
|
100
|
+
- **Lost the token** — it's printed in the terminal banner; scroll up, or stop and
|
|
101
|
+
re-run. You can also pin your own with `--token`.
|
|
102
|
+
|
|
103
|
+
## Where to go next
|
|
104
|
+
|
|
105
|
+
- [INSTALL.md](INSTALL.md) — every install path (npx, source, Docker), per-OS notes.
|
|
106
|
+
- [SECURITY.md](SECURITY.md) — what to turn on before exposing the bridge. **Read
|
|
107
|
+
this before going remote.**
|
|
108
|
+
- [WALKTHROUGH.md](WALKTHROUGH.md) — a screenshot tour of the whole thing.
|
package/docs/INSTALL.md
CHANGED
|
@@ -32,7 +32,9 @@ npx anyagent-bridge
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
This downloads and runs the bridge in one step, then prints an access URL with a
|
|
35
|
-
token. Open it in your browser.
|
|
35
|
+
token. Open it in your browser. New to this? Run `npx anyagent-bridge setup` for a
|
|
36
|
+
guided, first-timer flow (prerequisite checks + help opening it on a phone or
|
|
37
|
+
another PC); see [GETTING-STARTED.md](GETTING-STARTED.md). Useful flags:
|
|
36
38
|
|
|
37
39
|
```bash
|
|
38
40
|
npx anyagent-bridge --port 8080 # listen on a different port
|
package/docs/WALKTHROUGH.md
CHANGED
|
@@ -7,6 +7,7 @@ A guided tour of using anyagent-bridge end to end. The screenshots below live in
|
|
|
7
7
|
## 1. Start the bridge
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
+
npx anyagent-bridge setup # guided first-timer setup (recommended), or:
|
|
10
11
|
npx anyagent-bridge # or: npm start / docker compose up -d --build
|
|
11
12
|
```
|
|
12
13
|
|
|
@@ -45,6 +46,10 @@ as you would in your own terminal — streamed live to the browser. Type prompts
|
|
|
45
46
|
send keys, and watch output in real time. Detaching the browser keeps the session
|
|
46
47
|
alive; reconnecting reattaches with full scrollback.
|
|
47
48
|
|
|
49
|
+
To scope a session to a folder, click **📁 Projects** in the top bar and *browse* to
|
|
50
|
+
it — no typing the path. Saved projects appear in the toolbar dropdown; pick one
|
|
51
|
+
before you launch the agent.
|
|
52
|
+
|
|
48
53
|

|
|
49
54
|
|
|
50
55
|
## 4. Browse and edit files
|
|
@@ -61,6 +66,13 @@ at startup (`--tunnel devtunnel`) or at runtime via the tunnel controls
|
|
|
61
66
|
ready. Before exposing anything, read [SECURITY.md](SECURITY.md) and turn on
|
|
62
67
|
login / 2FA / OAuth.
|
|
63
68
|
|
|
69
|
+
The easiest way onto a phone: click **"📱 Connect a device"** in the top bar. It
|
|
70
|
+
shows a scannable QR for your current address (or a live tunnel), the localhost
|
|
71
|
+
link, step-by-step same-Wi-Fi instructions, and a one-click "Start internet
|
|
72
|
+
tunnel" — so you never type the long token on a phone.
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
64
76
|

|
|
65
77
|
|
|
66
78
|
---
|
|
Binary file
|
|
Binary file
|