anyagent-bridge 0.6.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 +1 -0
- package/client/index.html +151 -0
- package/docs/GETTING-STARTED.md +4 -0
- package/docs/WALKTHROUGH.md +4 -0
- package/docs/screenshots/02-terminal-view.png +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ before exposing the bridge beyond localhost, and
|
|
|
39
39
|
- Persistent sessions that survive reconnects, with automatic PTY respawn and backoff.
|
|
40
40
|
- Heartbeat + dead-connection detection so stale viewers get cleaned up.
|
|
41
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.
|
|
42
43
|
- Crash guards (uncaught exceptions, signals) so the server stays up.
|
|
43
44
|
- Constant-time token comparison and basic rate limiting.
|
|
44
45
|
- Optional **login**: Google/GitHub OAuth, TOTP 2FA, and signed expiring sessions on top of the token (Stage 3).
|
package/client/index.html
CHANGED
|
@@ -146,6 +146,34 @@
|
|
|
146
146
|
#onboard .warn { color: var(--yellow); }
|
|
147
147
|
#onboard .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
|
|
148
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; }
|
|
149
177
|
</style>
|
|
150
178
|
</head>
|
|
151
179
|
<body>
|
|
@@ -156,6 +184,7 @@
|
|
|
156
184
|
<button id="startBtn">Start</button>
|
|
157
185
|
<button id="connectBtn" title="Open on a phone or another device">📱 Connect a device</button>
|
|
158
186
|
<select id="projectSel" title="Project" style="display:none"></select>
|
|
187
|
+
<button id="projBtn" title="Add or manage projects by browsing folders">📁 Projects</button>
|
|
159
188
|
<span class="spacer"></span>
|
|
160
189
|
<span id="status" class="disconnected"><span class="led"></span><span id="statusText">disconnected</span></span>
|
|
161
190
|
</div>
|
|
@@ -208,6 +237,27 @@
|
|
|
208
237
|
</div>
|
|
209
238
|
</div>
|
|
210
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
|
+
|
|
211
261
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
212
262
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
213
263
|
<script>
|
|
@@ -649,6 +699,107 @@
|
|
|
649
699
|
onboard.addEventListener("click", (e) => { if (e.target === onboard) closeOnboard(); });
|
|
650
700
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && onboard.classList.contains("open")) closeOnboard(); });
|
|
651
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
|
+
|
|
652
803
|
// ---------- Bootstrap ----------
|
|
653
804
|
(async function boot() {
|
|
654
805
|
const params = new URLSearchParams(location.search);
|
package/docs/GETTING-STARTED.md
CHANGED
|
@@ -82,6 +82,10 @@ dropdown in the top bar (e.g. **Claude Code**) and click **Start** — it runs i
|
|
|
82
82
|
the session, streamed live to your browser. Detaching the browser keeps it alive;
|
|
83
83
|
reconnect and you're back with full scrollback.
|
|
84
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
|
+
|
|
85
89
|
## If something goes wrong
|
|
86
90
|
|
|
87
91
|
- **`node -v` says command not found** — install Node.js (above), then reopen the
|
package/docs/WALKTHROUGH.md
CHANGED
|
@@ -46,6 +46,10 @@ as you would in your own terminal — streamed live to the browser. Type prompts
|
|
|
46
46
|
send keys, and watch output in real time. Detaching the browser keeps the session
|
|
47
47
|
alive; reconnecting reattaches with full scrollback.
|
|
48
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
|
+
|
|
49
53
|

|
|
50
54
|
|
|
51
55
|
## 4. Browse and edit files
|
|
Binary file
|