anyagent-bridge 0.7.0 → 0.8.1
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 +3 -0
- package/bin/setup.js +22 -5
- package/client/index.html +196 -0
- package/docs/GETTING-STARTED.md +11 -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
|
|
@@ -40,6 +42,7 @@ before exposing the bridge beyond localhost, and
|
|
|
40
42
|
- Heartbeat + dead-connection detection so stale viewers get cleaned up.
|
|
41
43
|
- File management API: browse, read, write, rename, move, delete, upload, download — all behind a path whitelist.
|
|
42
44
|
- Add projects by **browsing folders** in the UI (the 📁 Projects button) — no typing full paths.
|
|
45
|
+
- Save API keys / tokens into a project's `.env.local` from the UI (the 🔑 Secrets button) — never paste them into the chat or hand-edit files.
|
|
43
46
|
- Crash guards (uncaught exceptions, signals) so the server stays up.
|
|
44
47
|
- Constant-time token comparison and basic rate limiting.
|
|
45
48
|
- Optional **login**: Google/GitHub OAuth, TOTP 2FA, and signed expiring sessions on top of the token (Stage 3).
|
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) ─────────────────────────────────────────
|
|
@@ -114,8 +115,17 @@ async function confirm(question, def = true) {
|
|
|
114
115
|
if (!agents.some((a) => a.found)) {
|
|
115
116
|
out();
|
|
116
117
|
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
|
-
|
|
118
|
+
out(yellow(' but to launch an agent you need one installed.'));
|
|
119
|
+
const wantClaude = await confirm(' Install Claude Code now (npm i -g @anthropic-ai/claude-code)?', false);
|
|
120
|
+
if (wantClaude) {
|
|
121
|
+
out(dim(' Installing via npm — this may take a minute…'));
|
|
122
|
+
const r = spawnSync('npm', ['install', '-g', '@anthropic-ai/claude-code'],
|
|
123
|
+
{ stdio: 'inherit', shell: process.platform === 'win32' });
|
|
124
|
+
if (r.status === 0) out(green(' ✓ Installed. Run `claude` once in a terminal to log in, then it shows up here.'));
|
|
125
|
+
else out(yellow(' Install didn\'t finish — do it manually: ') + cyan('npm install -g @anthropic-ai/claude-code'));
|
|
126
|
+
} else {
|
|
127
|
+
out(' Install one yourself, e.g.: ' + cyan('npm install -g @anthropic-ai/claude-code') + dim(' then run `claude` once to log in.'));
|
|
128
|
+
}
|
|
119
129
|
out(dim(' (Any CLI works — register it under "agents" in config.json.)'));
|
|
120
130
|
}
|
|
121
131
|
|
|
@@ -145,6 +155,7 @@ async function confirm(question, def = true) {
|
|
|
145
155
|
: `Find this computer's local IP (e.g. ${dim('System Settings → Network')}), then visit ${cyan(`http://<that-ip>:${port}/`)} on the other device.`);
|
|
146
156
|
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
157
|
nextSteps.push(`Easiest on a phone: open the page on this computer, click ${bold('"Connect a device" → Phone')}, and scan the QR.`);
|
|
158
|
+
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
159
|
} else {
|
|
149
160
|
host = '127.0.0.1'; // the tunnel reaches in; we don't bind to the network directly
|
|
150
161
|
out();
|
|
@@ -160,9 +171,15 @@ async function confirm(question, def = true) {
|
|
|
160
171
|
tunnelProvider = prov.key === '2' ? 'cloudflare-quick' : 'devtunnel';
|
|
161
172
|
const cli = tunnelProvider === 'cloudflare-quick' ? 'cloudflared' : 'devtunnel';
|
|
162
173
|
if (!onPath(cli)) {
|
|
163
|
-
out(yellow(` The "${cli}" command isn't installed yet
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
out(yellow(` The "${cli}" command isn't installed yet — install it, then re-run setup:`));
|
|
175
|
+
if (cli === 'cloudflared') {
|
|
176
|
+
if (process.platform === 'darwin') out(' ' + cyan('brew install cloudflared'));
|
|
177
|
+
else if (process.platform === 'win32') out(' ' + cyan('winget install --id Cloudflare.cloudflared') + dim(' (or grab it from github.com/cloudflare/cloudflared/releases)'));
|
|
178
|
+
else out(' ' + cyan('https://pkg.cloudflare.com') + dim(' (apt/yum) or github.com/cloudflare/cloudflared/releases'));
|
|
179
|
+
} else {
|
|
180
|
+
out(' ' + cyan('https://aka.ms/devtunnels/download') + dim(' then run `devtunnel user login` once'));
|
|
181
|
+
}
|
|
182
|
+
out(dim(' Continuing anyway — the server falls back to localhost-only if the tunnel cannot start.'));
|
|
166
183
|
}
|
|
167
184
|
nextSteps.push(`When the tunnel connects, its public ${bold('https://…')} URL prints in the banner below and shows in the UI.`);
|
|
168
185
|
nextSteps.push(`Open that URL on your phone, or use ${bold('"Connect a device" → Phone')} in the UI to scan a QR.`);
|
package/client/index.html
CHANGED
|
@@ -174,6 +174,31 @@
|
|
|
174
174
|
#projModal .pm-fields { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
175
175
|
#projModal .pm-fields input { flex: 1; min-width: 150px; }
|
|
176
176
|
#projModal .pm-err { color: var(--red); font-size: 12px; min-height: 15px; margin: 6px 0 0; }
|
|
177
|
+
|
|
178
|
+
/* Secrets — per-project .env.local editor */
|
|
179
|
+
#secModal { position: fixed; inset: 0; z-index: 60; background: rgba(13,17,23,.96); display: none; align-items: center; justify-content: center; padding: 18px; }
|
|
180
|
+
#secModal.open { display: flex; }
|
|
181
|
+
#secModal .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); }
|
|
182
|
+
#secModal h2 { margin: 0 0 2px; font-size: 17px; }
|
|
183
|
+
#secModal h2 .dim { color: var(--muted); font-weight: 400; font-size: 14px; }
|
|
184
|
+
#secModal .sub { margin: 0 0 12px; color: var(--muted); font-size: 12.5px; line-height: 1.5; }
|
|
185
|
+
#secModal .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; }
|
|
186
|
+
#secModal .pm-close:hover { color: var(--fg); }
|
|
187
|
+
#secModal .pm-empty { font-size: 12.5px; color: var(--muted); margin: 0 0 10px; line-height: 1.5; }
|
|
188
|
+
#secModal .sec-field { margin-bottom: 8px; }
|
|
189
|
+
#secModal .sec-field label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
190
|
+
#secModal .sec-field select { width: 100%; }
|
|
191
|
+
#secModal .sec-target { font-size: 12px; color: var(--muted); margin: 0 0 10px; word-break: break-all; }
|
|
192
|
+
#secModal .sec-list { list-style: none; margin: 0 0 10px; padding: 0; }
|
|
193
|
+
#secModal .sec-list li { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 7px; margin-bottom: 6px; }
|
|
194
|
+
#secModal .sec-k { font-size: 12.5px; font-weight: 600; word-break: break-all; }
|
|
195
|
+
#secModal .sec-v { font-size: 12px; color: var(--muted); flex: 1; font-family: Menlo, Monaco, monospace; word-break: break-all; }
|
|
196
|
+
#secModal .sec-eye, #secModal .sec-del { background: transparent; border: none; cursor: pointer; font-size: 13px; padding: 0 3px; }
|
|
197
|
+
#secModal .sec-del { color: var(--red); }
|
|
198
|
+
#secModal .sec-add { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
199
|
+
#secModal .sec-add input { flex: 1; min-width: 130px; }
|
|
200
|
+
#secModal .pm-err { color: var(--red); font-size: 12px; min-height: 15px; margin: 6px 0 0; }
|
|
201
|
+
#secModal .sec-note { font-size: 11px; color: var(--muted); margin: 8px 0 0; line-height: 1.5; }
|
|
177
202
|
</style>
|
|
178
203
|
</head>
|
|
179
204
|
<body>
|
|
@@ -185,6 +210,7 @@
|
|
|
185
210
|
<button id="connectBtn" title="Open on a phone or another device">📱 Connect a device</button>
|
|
186
211
|
<select id="projectSel" title="Project" style="display:none"></select>
|
|
187
212
|
<button id="projBtn" title="Add or manage projects by browsing folders">📁 Projects</button>
|
|
213
|
+
<button id="secBtn" title="Save API keys / tokens into a project's .env.local">🔑 Secrets</button>
|
|
188
214
|
<span class="spacer"></span>
|
|
189
215
|
<span id="status" class="disconnected"><span class="led"></span><span id="statusText">disconnected</span></span>
|
|
190
216
|
</div>
|
|
@@ -258,6 +284,31 @@
|
|
|
258
284
|
</div>
|
|
259
285
|
</div>
|
|
260
286
|
|
|
287
|
+
<div id="secModal">
|
|
288
|
+
<div class="card">
|
|
289
|
+
<button class="pm-close" id="secClose" aria-label="Close">×</button>
|
|
290
|
+
<h2>Secrets <span class="dim">(.env.local)</span></h2>
|
|
291
|
+
<p class="sub">Save API keys and tokens into a project's <code>.env.local</code> — the agent you launch there reads them. They go straight to a file, never through the chat or terminal.</p>
|
|
292
|
+
<p class="pm-empty" id="secNoProj" style="display:none">Add a project first with the <b>📁 Projects</b> button — secrets are saved into a project folder.</p>
|
|
293
|
+
<div id="secEditor">
|
|
294
|
+
<div class="sec-field">
|
|
295
|
+
<label for="secProj">Project folder</label>
|
|
296
|
+
<select id="secProj"></select>
|
|
297
|
+
</div>
|
|
298
|
+
<p class="sec-target">Writes to <code id="secTarget">…</code></p>
|
|
299
|
+
<ul class="sec-list" id="secList"></ul>
|
|
300
|
+
<p class="pm-empty" id="secEmpty" style="display:none">No keys yet.</p>
|
|
301
|
+
<div class="sec-add">
|
|
302
|
+
<input id="secKey" type="text" placeholder="KEY (e.g. OPENAI_API_KEY)" autocomplete="off" autocapitalize="characters" spellcheck="false" />
|
|
303
|
+
<input id="secVal" type="password" placeholder="value" autocomplete="off" spellcheck="false" />
|
|
304
|
+
<button class="primary" id="secAdd">Save</button>
|
|
305
|
+
</div>
|
|
306
|
+
<p class="pm-err" id="secErr"></p>
|
|
307
|
+
<p class="sec-note">Stored unencrypted in the project folder, like any <code>.env</code> — anyone who can read that folder can read these.</p>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
261
312
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
262
313
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
263
314
|
<script>
|
|
@@ -800,6 +851,151 @@
|
|
|
800
851
|
$("pmAdd").addEventListener("click", pmAddProject);
|
|
801
852
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && projModal.classList.contains("open")) closeProjects(); });
|
|
802
853
|
|
|
854
|
+
// ---------- Secrets: write API keys into a project's .env.local ----------
|
|
855
|
+
const secBtn = $("secBtn"), secModal = $("secModal"), secClose = $("secClose");
|
|
856
|
+
let secRaw = ""; // current raw .env.local text for the selected project
|
|
857
|
+
let secPath = null; // absolute path of that .env.local
|
|
858
|
+
let secLoadFailed = false; // true when the existing file couldn't be read → block writes so we never clobber it
|
|
859
|
+
const VALID_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
860
|
+
|
|
861
|
+
function secStripQuotes(v) {
|
|
862
|
+
v = v.trim();
|
|
863
|
+
if (v.length >= 2 && ((v[0] === '"' && v.endsWith('"')) || (v[0] === "'" && v.endsWith("'")))) return v.slice(1, -1);
|
|
864
|
+
return v;
|
|
865
|
+
}
|
|
866
|
+
function secParse(raw) {
|
|
867
|
+
const out = [];
|
|
868
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
869
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/);
|
|
870
|
+
if (m) out.push({ key: m[1], value: secStripQuotes(m[2]) });
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
function secFormatValue(v) {
|
|
875
|
+
// bare when safe, else double-quote with escaping — so a value can never inject a new line/var
|
|
876
|
+
return /^[A-Za-z0-9_./:@+-]*$/.test(v) ? v : '"' + v.replace(/(["\\])/g, "\\$1") + '"';
|
|
877
|
+
}
|
|
878
|
+
function secSetLine(raw, key, value) {
|
|
879
|
+
const lines = raw.split(/\r?\n/);
|
|
880
|
+
const re = new RegExp("^\\s*" + key + "\\s*="); // key is VALID_KEY-checked → no regex injection
|
|
881
|
+
const newLine = key + "=" + secFormatValue(value);
|
|
882
|
+
let replaced = false;
|
|
883
|
+
for (let i = 0; i < lines.length; i++) { if (re.test(lines[i])) { lines[i] = newLine; replaced = true; break; } }
|
|
884
|
+
if (!replaced) {
|
|
885
|
+
if (lines.length && lines[lines.length - 1].trim() === "") lines[lines.length - 1] = newLine;
|
|
886
|
+
else lines.push(newLine);
|
|
887
|
+
}
|
|
888
|
+
return lines.join("\n").replace(/\n*$/, "\n");
|
|
889
|
+
}
|
|
890
|
+
function secDelLine(raw, key) {
|
|
891
|
+
const re = new RegExp("^\\s*" + key + "\\s*=");
|
|
892
|
+
return raw.split(/\r?\n/).filter((l) => !re.test(l)).join("\n").replace(/\n*$/, "\n");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function secLoadProjects() {
|
|
896
|
+
const sel = $("secProj");
|
|
897
|
+
sel.innerHTML = "";
|
|
898
|
+
let list = [];
|
|
899
|
+
try { const d = await api("/api/projects"); list = d.projects || []; } catch (_) {}
|
|
900
|
+
if (!list.length) { $("secNoProj").style.display = ""; $("secEditor").style.display = "none"; return; }
|
|
901
|
+
$("secNoProj").style.display = "none"; $("secEditor").style.display = "";
|
|
902
|
+
for (const p of list) {
|
|
903
|
+
const o = document.createElement("option");
|
|
904
|
+
o.value = p.path; o.textContent = p.name + " (" + p.path + ")";
|
|
905
|
+
sel.appendChild(o);
|
|
906
|
+
}
|
|
907
|
+
if (projectSel && projectSel.value && [...sel.options].some((o) => o.value === projectSel.value)) sel.value = projectSel.value;
|
|
908
|
+
await secLoadEnv();
|
|
909
|
+
}
|
|
910
|
+
function secSetEnabled(on) {
|
|
911
|
+
$("secKey").disabled = !on; $("secVal").disabled = !on; $("secAdd").disabled = !on;
|
|
912
|
+
}
|
|
913
|
+
async function secLoadEnv() {
|
|
914
|
+
secPath = $("secProj").value.replace(/[\/\\]+$/, "") + "/.env.local";
|
|
915
|
+
$("secTarget").textContent = secPath;
|
|
916
|
+
$("secErr").textContent = "";
|
|
917
|
+
secRaw = "";
|
|
918
|
+
secLoadFailed = false;
|
|
919
|
+
// Distinguish "no file yet" (404 → a fresh file is fine) from a real read failure.
|
|
920
|
+
// On any other error we do NOT know the current contents, so we block editing —
|
|
921
|
+
// otherwise the next Save would write only the new key and wipe the rest.
|
|
922
|
+
let res;
|
|
923
|
+
try {
|
|
924
|
+
res = await fetch("/api/explorer/read?path=" + encodeURIComponent(secPath),
|
|
925
|
+
{ credentials: "include", headers: token ? { Authorization: "Bearer " + token } : {} });
|
|
926
|
+
} catch (e) {
|
|
927
|
+
secLoadFailed = true;
|
|
928
|
+
$("secErr").textContent = "Couldn't reach the server — editing disabled so existing keys stay safe.";
|
|
929
|
+
secSetEnabled(false); secRender(); return;
|
|
930
|
+
}
|
|
931
|
+
if (res.status === 404) {
|
|
932
|
+
secRaw = ""; // no .env.local yet
|
|
933
|
+
} else if (res.ok) {
|
|
934
|
+
const data = await res.json().catch(() => ({}));
|
|
935
|
+
secRaw = typeof data.content === "string" ? data.content : "";
|
|
936
|
+
} else {
|
|
937
|
+
secLoadFailed = true;
|
|
938
|
+
$("secErr").textContent = res.status === 401
|
|
939
|
+
? "Session expired — reload to log in again."
|
|
940
|
+
: "Couldn't read this folder's .env.local — editing disabled so existing keys aren't overwritten.";
|
|
941
|
+
secSetEnabled(false); secRender(); return;
|
|
942
|
+
}
|
|
943
|
+
secSetEnabled(true);
|
|
944
|
+
secRender();
|
|
945
|
+
}
|
|
946
|
+
function secRender() {
|
|
947
|
+
const ul = $("secList");
|
|
948
|
+
ul.innerHTML = "";
|
|
949
|
+
const entries = secParse(secRaw);
|
|
950
|
+
$("secEmpty").style.display = entries.length ? "none" : "";
|
|
951
|
+
for (const e of entries) {
|
|
952
|
+
const li = document.createElement("li");
|
|
953
|
+
const k = document.createElement("span"); k.className = "sec-k"; k.textContent = e.key;
|
|
954
|
+
const v = document.createElement("span"); v.className = "sec-v"; v.textContent = "••••••••"; v.dataset.real = e.value; v.dataset.shown = "0";
|
|
955
|
+
const eye = document.createElement("button"); eye.className = "sec-eye"; eye.textContent = "👁"; eye.title = "Show / hide";
|
|
956
|
+
eye.addEventListener("click", () => { const sh = v.dataset.shown === "1"; v.textContent = sh ? "••••••••" : (v.dataset.real || "(empty)"); v.dataset.shown = sh ? "0" : "1"; });
|
|
957
|
+
const del = document.createElement("button"); del.className = "sec-del"; del.textContent = "✕"; del.title = "Remove";
|
|
958
|
+
del.addEventListener("click", () => secRemove(e.key));
|
|
959
|
+
li.appendChild(k); li.appendChild(v); li.appendChild(eye); li.appendChild(del);
|
|
960
|
+
ul.appendChild(li);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function secWrite() {
|
|
964
|
+
const res = await fetch("/api/explorer/write", { method: "POST", credentials: "include",
|
|
965
|
+
headers: Object.assign({ "Content-Type": "application/json" }, token ? { Authorization: "Bearer " + token } : {}),
|
|
966
|
+
body: JSON.stringify({ path: secPath, content: secRaw }) });
|
|
967
|
+
if (res.status === 401) { closeSecrets(); showGate("Session expired — please log in again."); throw new Error("session expired"); }
|
|
968
|
+
const data = await res.json().catch(() => ({}));
|
|
969
|
+
if (!res.ok || data.error) throw new Error(data.error || ("HTTP " + res.status));
|
|
970
|
+
}
|
|
971
|
+
async function secAdd() {
|
|
972
|
+
const err = $("secErr"), key = $("secKey").value.trim();
|
|
973
|
+
if (secLoadFailed) { err.textContent = "Can't save — the current .env.local couldn't be read. Re-open Secrets and try again."; return; }
|
|
974
|
+
if (!VALID_KEY.test(key)) { err.textContent = "Key must be letters/digits/underscores, e.g. OPENAI_API_KEY."; return; }
|
|
975
|
+
const val = $("secVal").value.replace(/[\r\n]+/g, ""); // a secret is single-line
|
|
976
|
+
err.textContent = "";
|
|
977
|
+
const prev = secRaw;
|
|
978
|
+
secRaw = secSetLine(secRaw, key, val);
|
|
979
|
+
try { await secWrite(); } catch (e) { secRaw = prev; err.textContent = "Could not save: " + e.message; return; }
|
|
980
|
+
$("secKey").value = ""; $("secVal").value = "";
|
|
981
|
+
secRender();
|
|
982
|
+
}
|
|
983
|
+
async function secRemove(key) {
|
|
984
|
+
const prev = secRaw;
|
|
985
|
+
secRaw = secDelLine(secRaw, key);
|
|
986
|
+
try { await secWrite(); } catch (e) { secRaw = prev; $("secErr").textContent = "Could not save: " + e.message; return; }
|
|
987
|
+
secRender();
|
|
988
|
+
}
|
|
989
|
+
function openSecrets() { secModal.classList.add("open"); $("secErr").textContent = ""; $("secKey").value = ""; $("secVal").value = ""; secLoadProjects(); }
|
|
990
|
+
function closeSecrets() { secModal.classList.remove("open"); }
|
|
991
|
+
secBtn.addEventListener("click", openSecrets);
|
|
992
|
+
secClose.addEventListener("click", closeSecrets);
|
|
993
|
+
secModal.addEventListener("click", (e) => { if (e.target === secModal) closeSecrets(); });
|
|
994
|
+
$("secProj").addEventListener("change", secLoadEnv);
|
|
995
|
+
$("secAdd").addEventListener("click", secAdd);
|
|
996
|
+
$("secVal").addEventListener("keydown", (e) => { if (e.key === "Enter") secAdd(); });
|
|
997
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && secModal.classList.contains("open")) closeSecrets(); });
|
|
998
|
+
|
|
803
999
|
// ---------- Bootstrap ----------
|
|
804
1000
|
(async function boot() {
|
|
805
1001
|
const params = new URLSearchParams(location.search);
|
package/docs/GETTING-STARTED.md
CHANGED
|
@@ -86,6 +86,10 @@ To run the agent inside a specific **project folder**, click **📁 Projects** i
|
|
|
86
86
|
top bar and *browse* to the folder — no typing the full path. It's saved and shows
|
|
87
87
|
up in the toolbar's project dropdown; pick one before launching.
|
|
88
88
|
|
|
89
|
+
If the agent needs API keys (e.g. `OPENAI_API_KEY`), click **🔑 Secrets**, pick the
|
|
90
|
+
project, and add `KEY` + value — it's written to that project's `.env.local` for you,
|
|
91
|
+
so you never paste keys into the chat or hand-edit dotfiles.
|
|
92
|
+
|
|
89
93
|
## If something goes wrong
|
|
90
94
|
|
|
91
95
|
- **`node -v` says command not found** — install Node.js (above), then reopen the
|
|
@@ -99,6 +103,13 @@ up in the toolbar's project dropdown; pick one before launching.
|
|
|
99
103
|
tunnel at all — use the "same Wi-Fi" address instead.
|
|
100
104
|
- **Lost the token** — it's printed in the terminal banner; scroll up, or stop and
|
|
101
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.
|
|
102
113
|
|
|
103
114
|
## Where to go next
|
|
104
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);
|