@yolo-labs/yolobridge 0.27.0 → 0.28.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/dist/api-client.js +26 -0
- package/dist/attach-cmd.js +27 -0
- package/dist/cli.js +6 -0
- package/dist/local-shell-server.js +24 -10
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -187,6 +187,32 @@ export async function openStream(cfg, workspaceId, attachmentId) {
|
|
|
187
187
|
}
|
|
188
188
|
return res;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Tell the server where this daemon's LOCAL terminal server is listening.
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ Uses the daemon's SCOPED token, and the route requires one. It is the
|
|
194
|
+
* mirror of the input route's rule: only a USER may type, and only the machine
|
|
195
|
+
* actually running the server may say where it is. A user token here would let
|
|
196
|
+
* anyone point a tile at an arbitrary address.
|
|
197
|
+
*
|
|
198
|
+
* Best-effort by design — returns false rather than throwing. A daemon whose
|
|
199
|
+
* report fails is still a perfectly good daemon; the tile simply falls back to
|
|
200
|
+
* the cloud relay, which is slower and works.
|
|
201
|
+
*/
|
|
202
|
+
export async function reportLocalEndpoint(cfg, workspaceId, attachmentId, url, secret) {
|
|
203
|
+
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/attach/${attachmentId}/local-endpoint`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({ url, secret }),
|
|
209
|
+
});
|
|
210
|
+
return res.ok;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
190
216
|
export async function postHeartbeat(cfg, workspaceId, attachmentId) {
|
|
191
217
|
const body = await postEvent(cfg, workspaceId, { attachmentId, type: 'heartbeat' });
|
|
192
218
|
return Boolean(body?.recorded);
|
package/dist/attach-cmd.js
CHANGED
|
@@ -900,6 +900,20 @@ export async function runAttachDaemon(deps) {
|
|
|
900
900
|
return;
|
|
901
901
|
}
|
|
902
902
|
await apiClient.postHeartbeat(scopedCfg(), workspaceId, attachmentId);
|
|
903
|
+
// ⚠️ RE-REPORTED ON EVERY BEAT, not once at startup. The
|
|
904
|
+
// server holds this in memory with a TTL, so it is lost
|
|
905
|
+
// on a restart or a failover — and the correct recovery
|
|
906
|
+
// is the daemon simply saying it again a few seconds
|
|
907
|
+
// later, not the server persisting a secret. Best-effort:
|
|
908
|
+
// a failed report costs the tile its fast path, nothing
|
|
909
|
+
// more.
|
|
910
|
+
// Read LAZILY: the shell server starts inside
|
|
911
|
+
// `onAttached`, after these deps were built, so a
|
|
912
|
+
// snapshot taken at construction would always be empty.
|
|
913
|
+
const local = deps.localEndpoint?.();
|
|
914
|
+
if (local) {
|
|
915
|
+
await apiClient.reportLocalEndpoint(scopedCfg(), workspaceId, attachmentId, local.url, local.secret);
|
|
916
|
+
}
|
|
903
917
|
}, (err) => {
|
|
904
918
|
// The heartbeat hits the SAME boundary as the stream open
|
|
905
919
|
// and gets the SAME 403, so it needs the same answer: a
|
|
@@ -912,6 +926,19 @@ export async function runAttachDaemon(deps) {
|
|
|
912
926
|
detail: `heartbeat error: ${err instanceof Error ? err.message : String(err)}`,
|
|
913
927
|
});
|
|
914
928
|
}, undefined, deps.timers);
|
|
929
|
+
// ⚠️ REPORT THE LOCAL ENDPOINT IMMEDIATELY TOO, not only on
|
|
930
|
+
// the interval. Waiting ~10s means a tile opened in that
|
|
931
|
+
// window gets a 404 and silently downgrades to the ~200ms
|
|
932
|
+
// relay, while a perfectly good local server is already
|
|
933
|
+
// listening — and it would stay downgraded for that whole
|
|
934
|
+
// session. Same reasoning as the immediate heartbeat below.
|
|
935
|
+
// (codex P2.)
|
|
936
|
+
{
|
|
937
|
+
const local = deps.localEndpoint?.();
|
|
938
|
+
if (local) {
|
|
939
|
+
apiClient.reportLocalEndpoint(scopedCfg(), workspaceId, attachmentId, local.url, local.secret).catch(() => { });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
915
942
|
// Send one immediately so status isn't stale for the first ~10s.
|
|
916
943
|
apiClient.postHeartbeat(scopedCfg(), workspaceId, attachmentId).catch((err) => {
|
|
917
944
|
if (noteCredentialRejection(err))
|
package/dist/cli.js
CHANGED
|
@@ -356,6 +356,12 @@ async function cmdAttach(args) {
|
|
|
356
356
|
let result;
|
|
357
357
|
try {
|
|
358
358
|
result = await runAttachFromDisk({
|
|
359
|
+
// Read lazily on each heartbeat — the shell server starts in
|
|
360
|
+
// `onAttached`, after this object is built, so a value here would
|
|
361
|
+
// always be undefined.
|
|
362
|
+
localEndpoint: () => (shellServerHandle
|
|
363
|
+
? { url: shellServerHandle.url, secret: shellServerHandle.secret }
|
|
364
|
+
: undefined),
|
|
359
365
|
workspaceId,
|
|
360
366
|
commonApiBaseUrl: apiUrl(),
|
|
361
367
|
hostLabel: attachHostInfo.hostLabel,
|
|
@@ -38,16 +38,30 @@
|
|
|
38
38
|
* why the client must treat "no answer" as a timeout and fall back, never wait
|
|
39
39
|
* forever.
|
|
40
40
|
*
|
|
41
|
-
* ⚠️
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
41
|
+
* ⚠️ THE BACKLOG DOES NOT RECONSTRUCT A TUI SCREEN — AND NOTHING CURRENTLY
|
|
42
|
+
* NEEDS IT TO. Worth stating precisely, because the obvious next step here is
|
|
43
|
+
* speculative work.
|
|
44
|
+
*
|
|
45
|
+
* The backlog is a byte TAIL cut at a parser-safe boundary. It cannot rebuild a
|
|
46
|
+
* full-screen TUI that painted its layout once and then emitted more than
|
|
47
|
+
* `BACKLOG_CHARS` of cursor-addressed updates: a viewer joining mid-session
|
|
48
|
+
* would get the updates without the screen they address. The cloud path solved
|
|
49
|
+
* that properly — `local-agent.ts` keeps an `@xterm/headless` mirror and serves
|
|
50
|
+
* a serialized screen plus a mode `prologue` — and that dependency is already
|
|
51
|
+
* here, so doing the same looks like the natural next slice.
|
|
52
|
+
*
|
|
53
|
+
* IT IS NOT, because no client reconnects. `openLocalTerminal` establishes the
|
|
54
|
+
* stream exactly once; a stream that ends marks the session DEAD rather than
|
|
55
|
+
* retrying, and the tile never reuses a session id — every mount opens a fresh
|
|
56
|
+
* shell. So the replay only ever covers output produced between `/open` and
|
|
57
|
+
* `/stream`, a window of milliseconds inside a single connection, where a byte
|
|
58
|
+
* tail is exactly right.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ WHAT WOULD MAKE IT MATTER: adding session RESUMPTION — a tile that
|
|
61
|
+
* reattaches to its shell across a remount, a dropped stream, or a page
|
|
62
|
+
* refresh. That is a real feature and a reasonable one to want. The headless
|
|
63
|
+
* mirror is a prerequisite FOR IT, not an improvement on its own; building the
|
|
64
|
+
* mirror first would be solving the second half of a problem nobody has yet.
|
|
51
65
|
*
|
|
52
66
|
* SECURITY, deliberately narrow because this hands out shells:
|
|
53
67
|
* · bound to 127.0.0.1 ONLY — never 0.0.0.0, so it is off the local network
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-labs/yolobridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "YoloBridge \u2014 local coding-agent daemon that attaches a user's own Claude Code/Codex session to a YOLO Studio workspace as a first-class tile (docs/YOLOBRIDGE_PLAN.md, build-order Phase 5).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|