@yolo-labs/yolobridge 0.25.0 → 0.27.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 +0 -97
- package/dist/cli.js +83 -27
- package/dist/local-shell-server.js +462 -0
- package/package.json +2 -2
- package/dist/console-cmd.js +0 -888
package/dist/api-client.js
CHANGED
|
@@ -324,100 +324,3 @@ export async function deliverShare(cfg, workspaceId, attachmentId, assetId, targ
|
|
|
324
324
|
}
|
|
325
325
|
return (await res.json());
|
|
326
326
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Send raw console keystrokes toward a daemon.
|
|
329
|
-
*
|
|
330
|
-
* ⚠️ Authenticates as the USER, not as a daemon. The console runs on a machine
|
|
331
|
-
* the operator is sitting at; the daemon's workspace-scoped token belongs to the
|
|
332
|
-
* machine being typed INTO and never leaves it. The route enforces the same
|
|
333
|
-
* thing from the other side by refusing a scoped token outright.
|
|
334
|
-
*
|
|
335
|
-
* Returns the server's delivery verdict rather than a bare boolean, because the
|
|
336
|
-
* distinction is real: 'written' means a replica put the frame on the daemon's
|
|
337
|
-
* own stream, 'relayed' means it was handed off and nobody has confirmed it.
|
|
338
|
-
*/
|
|
339
|
-
export async function sendConsoleInput(cfg, workspaceId, attachmentId, data) {
|
|
340
|
-
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
341
|
-
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/attach/${attachmentId}/input`, {
|
|
342
|
-
method: 'POST',
|
|
343
|
-
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
344
|
-
body: JSON.stringify({ data }),
|
|
345
|
-
});
|
|
346
|
-
if (!res.ok) {
|
|
347
|
-
const { message, code } = await parseErrorBody(res);
|
|
348
|
-
throw new YoloBridgeApiError(message, res.status, code);
|
|
349
|
-
}
|
|
350
|
-
const body = (await res.json().catch(() => ({})));
|
|
351
|
-
return body.delivery === 'written' ? 'written' : 'relayed';
|
|
352
|
-
}
|
|
353
|
-
/** Register as an output viewer, which is what makes the daemon stream at all. */
|
|
354
|
-
export async function subscribeOutput(cfg, workspaceId, tileId, subscriptionId) {
|
|
355
|
-
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
356
|
-
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/tiles/${tileId}/output/subscribe`, {
|
|
357
|
-
method: 'POST',
|
|
358
|
-
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
359
|
-
body: JSON.stringify({ subscriptionId }),
|
|
360
|
-
});
|
|
361
|
-
if (!res.ok) {
|
|
362
|
-
const { message, code } = await parseErrorBody(res);
|
|
363
|
-
throw new YoloBridgeApiError(`could not subscribe to output: ${message}`, res.status, code);
|
|
364
|
-
}
|
|
365
|
-
return (await res.json().catch(() => ({})));
|
|
366
|
-
}
|
|
367
|
-
export async function unsubscribeOutput(cfg, workspaceId, tileId, subscriptionId) {
|
|
368
|
-
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
369
|
-
await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/tiles/${tileId}/output/unsubscribe`, {
|
|
370
|
-
method: 'POST',
|
|
371
|
-
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
372
|
-
body: JSON.stringify({ subscriptionId }),
|
|
373
|
-
}).catch(() => { });
|
|
374
|
-
}
|
|
375
|
-
/** The workspace event stream, where relayed PTY output arrives. */
|
|
376
|
-
export async function openWorkspaceEventStream(cfg, workspaceId) {
|
|
377
|
-
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
378
|
-
// ⚠️ THE TOKEN GOES IN THE QUERY STRING, not the Authorization header.
|
|
379
|
-
// `routes/events.ts` reads `req.query.token` exclusively — because browsers'
|
|
380
|
-
// EventSource cannot send custom headers — and when it is absent the route
|
|
381
|
-
// answers HTTP 200 with an SSE `error` frame saying "Token required". So a
|
|
382
|
-
// header-only client is ACCEPTED by fetch and then shows nothing, forever,
|
|
383
|
-
// with no failure anywhere to notice. (codex P1.)
|
|
384
|
-
const url = `${base(cfg)}/v1/events/stream`
|
|
385
|
-
+ `?workspaceId=${encodeURIComponent(workspaceId)}`
|
|
386
|
-
+ `&token=${encodeURIComponent(cfg.accessToken)}`;
|
|
387
|
-
const res = await fetchImpl(url, { headers: { ...authHeaders(cfg), Accept: 'text/event-stream' } });
|
|
388
|
-
if (!res.ok || !res.body) {
|
|
389
|
-
const { message, code } = await parseErrorBody(res);
|
|
390
|
-
throw new YoloBridgeApiError(`event stream failed: ${message}`, res.status, code);
|
|
391
|
-
}
|
|
392
|
-
return res;
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* Fetch the RAW replay seed for a tile — the tail of the daemon's PTY byte
|
|
396
|
-
* stream plus the absolute offsets that say where it ends.
|
|
397
|
-
*
|
|
398
|
-
* ⚠️ `mode=raw`, not the default. The default returns a SERIALIZED SCREEN, which
|
|
399
|
-
* is a rendering, not a byte stream: writing it into a terminal and then
|
|
400
|
-
* appending live chunks puts the cursor somewhere the daemon never put it. Only
|
|
401
|
-
* the raw form can be continued from.
|
|
402
|
-
*
|
|
403
|
-
* Returns undefined rather than throwing when the seed is unavailable — a
|
|
404
|
-
* detached or briefly unreachable daemon answers 409, and a console that can
|
|
405
|
-
* still stream live output should start blank rather than refuse to run.
|
|
406
|
-
*/
|
|
407
|
-
export async function readRawOutputSeed(cfg, workspaceId, tileId) {
|
|
408
|
-
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
409
|
-
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/tiles/${tileId}/output?mode=raw`, { headers: authHeaders(cfg) });
|
|
410
|
-
if (!res.ok)
|
|
411
|
-
return undefined;
|
|
412
|
-
const body = (await res.json().catch(() => undefined));
|
|
413
|
-
return body && typeof body.raw === 'string' ? body : undefined;
|
|
414
|
-
}
|
|
415
|
-
export async function resolveAttachmentTile(cfg, workspaceId, attachmentId) {
|
|
416
|
-
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
417
|
-
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}`, { headers: authHeaders(cfg) });
|
|
418
|
-
if (!res.ok)
|
|
419
|
-
return undefined;
|
|
420
|
-
const body = (await res.json().catch(() => ({})));
|
|
421
|
-
const tiles = body.tiles ?? body.workspace?.tiles ?? [];
|
|
422
|
-
return tiles.find((t) => t?.yoloBridge?.attachmentId === attachmentId)?.id;
|
|
423
|
-
}
|
package/dist/cli.js
CHANGED
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
* `yolo-bridge` — YoloBridge local daemon CLI (docs/YOLOBRIDGE_PLAN.md,
|
|
4
4
|
* Implementation Plan → build-order step 5).
|
|
5
5
|
*
|
|
6
|
-
* Subcommands
|
|
6
|
+
* Subcommands (keep in step with `printHelp` — this list is what a reader of
|
|
7
|
+
* the file sees first, and it silently fell five commands behind before):
|
|
7
8
|
* yolo-bridge login — device-authorization flow (login-cmd.ts)
|
|
8
9
|
* yolo-bridge workspaces — list selectable workspaces (workspaces-cmd.ts)
|
|
9
10
|
* yolo-bridge attach [workspaceId] — attach + hold the SSE stream (attach-cmd.ts)
|
|
10
11
|
* (omit the id for an interactive picker)
|
|
11
12
|
* yolo-bridge detach — DELETE the current attachment (detach-cmd.ts)
|
|
13
|
+
* yolo-bridge console — REMOVED in 0.26.0; prints where to go instead
|
|
14
|
+
* yolo-bridge allow <path> — approve a path the attached agent may
|
|
15
|
+
* send files from (approved-paths.ts)
|
|
16
|
+
* yolo-bridge share <path> — push a local file to the workspace,
|
|
17
|
+
* optionally into a tile (share-cmd.ts)
|
|
18
|
+
* yolo-bridge deliver <assetId> — write an ALREADY-shared file into a
|
|
19
|
+
* tile's session (share-cmd.ts)
|
|
12
20
|
* yolo-bridge status — print local login/attach state (status-cmd.ts)
|
|
21
|
+
* yolo-bridge version — print the installed version (also --version, -v)
|
|
13
22
|
*
|
|
14
23
|
* Base URLs default to this repo's real hostnames (CLAUDE.md → Project
|
|
15
24
|
* Overview): common-api `https://api.yolo.studio`, auth-service
|
|
@@ -23,16 +32,27 @@ import { hostname } from 'node:os';
|
|
|
23
32
|
import { runLogin } from './login-cmd.js';
|
|
24
33
|
import { runAttachFromDisk, pickWorkspaceFromDisk } from './attach-cmd.js';
|
|
25
34
|
import { runShare, runDeliver } from './share-cmd.js';
|
|
26
|
-
import { runConsole } from './console-cmd.js';
|
|
27
35
|
import { runAllow } from './approved-paths.js';
|
|
28
36
|
import { runDetach } from './detach-cmd.js';
|
|
29
37
|
import { getStatus, formatStatus } from './status-cmd.js';
|
|
30
38
|
import { startLocalAgent, stopLocalAgent, DEFAULT_AGENT_BIN } from './local-agent.js';
|
|
31
39
|
import { runListWorkspaces, formatWorkspacesTable } from './workspaces-cmd.js';
|
|
32
40
|
import { startMcpProxy, mcpUrl, SECRET_ENV_VAR } from './mcp-proxy.js';
|
|
41
|
+
import { startLocalShellServer } from './local-shell-server.js';
|
|
33
42
|
import { buildAgentMcpArgs } from './agent-mcp-args.js';
|
|
34
43
|
const DEFAULT_API_URL = 'https://api.yolo.studio';
|
|
44
|
+
const DEFAULT_WEBAPP_ORIGIN = 'https://yolo.studio';
|
|
35
45
|
const DEFAULT_AUTH_URL = 'https://auth.yololabs.ai';
|
|
46
|
+
/**
|
|
47
|
+
* The ONE browser origin allowed to reach the local terminal server.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ Never a wildcard: this authorises reaching a shell on the operator's
|
|
50
|
+
* machine, so it is a single exact origin. Overridable only for local
|
|
51
|
+
* development against a different webapp host.
|
|
52
|
+
*/
|
|
53
|
+
function webappOrigin() {
|
|
54
|
+
return process.env.YOLOBRIDGE_WEBAPP_ORIGIN || DEFAULT_WEBAPP_ORIGIN;
|
|
55
|
+
}
|
|
36
56
|
function apiUrl() {
|
|
37
57
|
return process.env.YOLOBRIDGE_API_URL || DEFAULT_API_URL;
|
|
38
58
|
}
|
|
@@ -105,10 +125,8 @@ function printHelp() {
|
|
|
105
125
|
' allow <path> Let the ATTACHED AGENT send files from this path. You type',
|
|
106
126
|
' this; nothing in the cloud can. Also --list and --remove <path>.',
|
|
107
127
|
' The daemon\'s own working directory is always allowed.',
|
|
108
|
-
' console
|
|
109
|
-
'
|
|
110
|
-
' and the agent keeps running. Defaults to this machine\'s',
|
|
111
|
-
' own attachment when run with no arguments.',
|
|
128
|
+
' console REMOVED in 0.26.0. Use the YoloBridge tile\'s "open terminal"',
|
|
129
|
+
' control, which spawns a terminal tile wired to the session.',
|
|
112
130
|
' share <path> Share a local file with the attached workspace, so a cloud',
|
|
113
131
|
' agent can see it. Push only — nothing reads your disk remotely.',
|
|
114
132
|
' [--to <tileId>] Also write it into that tile\'s session, so its agent can open it.',
|
|
@@ -320,6 +338,14 @@ async function cmdAttach(args) {
|
|
|
320
338
|
cliVersion: readOwnVersion(),
|
|
321
339
|
});
|
|
322
340
|
let mcpProxyHandle;
|
|
341
|
+
/**
|
|
342
|
+
* Serves terminals on 127.0.0.1 for the tile's "open terminal".
|
|
343
|
+
*
|
|
344
|
+
* ⚠️ SEPARATE FROM THE AGENT PTY. `startLocalAgent` owns the one agent
|
|
345
|
+
* session; this owns any shells the operator opens from the workspace. They
|
|
346
|
+
* share a lifetime — both die with the attach — and nothing else.
|
|
347
|
+
*/
|
|
348
|
+
let shellServerHandle;
|
|
323
349
|
// argv fragment pointing the spawned agent at the local MCP proxy, or
|
|
324
350
|
// `[]` when MCP isn't wired in — see `agent-mcp-args.ts`. Nothing else is
|
|
325
351
|
// tracked for cleanup any more: as of 2026-08-26 `attach` writes NOTHING
|
|
@@ -356,6 +382,22 @@ async function cmdAttach(args) {
|
|
|
356
382
|
// prompt. MCP access is an enhancement on a tile that already works
|
|
357
383
|
// without it; the local agent spawning is not optional.
|
|
358
384
|
try {
|
|
385
|
+
// The local terminal server. Started BEFORE the agent, like the MCP
|
|
386
|
+
// proxy, so the endpoint exists by the time the tile could ask for
|
|
387
|
+
// it. A failure here must not stop the attach: the agent and its
|
|
388
|
+
// tile are the point, a local terminal is an extra.
|
|
389
|
+
try {
|
|
390
|
+
shellServerHandle = await startLocalShellServer({ allowedOrigin: webappOrigin() });
|
|
391
|
+
// ⚠️ THE URL, NEVER THE SECRET. This line lands in the operator's
|
|
392
|
+
// scrollback, which is exactly where things get copied into bug
|
|
393
|
+
// reports and pasted into chats. The secret authorises spawning a
|
|
394
|
+
// shell on this machine; it reaches the tile over the authenticated
|
|
395
|
+
// workspace channel and is printed nowhere.
|
|
396
|
+
process.stdout.write(`yolo-bridge: local terminals ready at ${shellServerHandle.url} (127.0.0.1 only)\n`);
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
process.stdout.write(`yolo-bridge: local terminals unavailable (${err instanceof Error ? err.message : String(err)}) — the attach continues without them.\n`);
|
|
400
|
+
}
|
|
359
401
|
mcpProxyHandle = await startMcpProxy({
|
|
360
402
|
apiUrl: apiUrl(),
|
|
361
403
|
getAccessToken,
|
|
@@ -531,6 +573,17 @@ async function cmdAttach(args) {
|
|
|
531
573
|
// or a reboot, and every skipped run left a file that broke the
|
|
532
574
|
// operator's own standalone `claude` in that directory. Nothing written
|
|
533
575
|
// is nothing to clean up.
|
|
576
|
+
// Same "nothing left running detached" rule as the MCP proxy: a shell the
|
|
577
|
+
// operator opened from the workspace must not outlive the attach that
|
|
578
|
+
// served it. `close()` kills every session it owns.
|
|
579
|
+
if (shellServerHandle) {
|
|
580
|
+
try {
|
|
581
|
+
await shellServerHandle.close();
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
process.stdout.write(`yolo-bridge: local terminal shutdown failed (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
534
587
|
if (mcpProxyHandle) {
|
|
535
588
|
try {
|
|
536
589
|
await mcpProxyHandle.stop();
|
|
@@ -596,26 +649,29 @@ function cmdStatus() {
|
|
|
596
649
|
process.stdout.write(`${formatStatus(getStatus())}\n`);
|
|
597
650
|
return 0;
|
|
598
651
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
652
|
+
/**
|
|
653
|
+
* REMOVED in 0.26.0 — kept as an explicit notice rather than falling through to
|
|
654
|
+
* "unknown command".
|
|
655
|
+
*
|
|
656
|
+
* A command that vanishes without explanation is worse than one that says why:
|
|
657
|
+
* anyone with it in muscle memory or a shell script gets a dead end and no
|
|
658
|
+
* pointer. This costs a few lines and answers the question.
|
|
659
|
+
*/
|
|
660
|
+
function cmdConsoleRemoved() {
|
|
661
|
+
process.stderr.write([
|
|
662
|
+
'yolo-bridge console was removed in 0.26.0.',
|
|
663
|
+
'',
|
|
664
|
+
'Open a terminal from the workspace instead: the YoloBridge tile has an',
|
|
665
|
+
'"open terminal" control that spawns a terminal tile wired to this session.',
|
|
666
|
+
'',
|
|
667
|
+
'It was built for reaching a session from a DIFFERENT machine\'s terminal,',
|
|
668
|
+
'which turned out not to be a use case anyone had. Nothing replaced it',
|
|
669
|
+
'because the tile does the job from the machine you are already on.',
|
|
670
|
+
'',
|
|
671
|
+
'Need it back? `npm i -g @yolo-labs/yolobridge@0.25.0` still has it.',
|
|
672
|
+
'',
|
|
673
|
+
].join('\n'));
|
|
674
|
+
return 64;
|
|
619
675
|
}
|
|
620
676
|
async function cmdDeliver(args) {
|
|
621
677
|
const assetId = args.find((a) => !a.startsWith('-') && a !== args[args.indexOf('--to') + 1]);
|
|
@@ -723,7 +779,7 @@ async function main() {
|
|
|
723
779
|
case 'allow':
|
|
724
780
|
return cmdAllow(rest);
|
|
725
781
|
case 'console':
|
|
726
|
-
return
|
|
782
|
+
return cmdConsoleRemoved();
|
|
727
783
|
case 'deliver':
|
|
728
784
|
return cmdDeliver(rest);
|
|
729
785
|
case 'share':
|