@yolo-labs/yolobridge 0.7.1 → 0.8.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/attach-cmd.js +49 -31
- package/dist/cli.js +17 -3
- package/package.json +1 -1
package/dist/attach-cmd.js
CHANGED
|
@@ -419,54 +419,72 @@ export async function runAttachDaemon(deps) {
|
|
|
419
419
|
return { ok: true };
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
|
-
// Cover the case where the daemon is (re)started against a token that's
|
|
423
|
-
// already within the refresh buffer of expiry (e.g. `attach` run right
|
|
424
|
-
// after a long-down period) — refresh before the very first network
|
|
425
|
-
// call, not just before subsequent reconnects.
|
|
426
|
-
const initialRefresh = await ensureFreshToken();
|
|
427
422
|
/**
|
|
428
423
|
* RESUME an existing attachment from its persisted scoped credential
|
|
429
|
-
* (card 08), instead of creating a
|
|
424
|
+
* (card 08), instead of creating a second one.
|
|
430
425
|
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
426
|
+
* **LIVENESS decides, not the account token's health** (D7). Card 08 gated
|
|
427
|
+
* this on the account refresh having failed, which meant a daemon restarting
|
|
428
|
+
* with a healthy account token ignored a perfectly good stored attachment and
|
|
429
|
+
* called `attach` again — a SECOND server-side attachment and a second tile.
|
|
430
|
+
* The first stops heartbeating and is reaped, but the operator is left
|
|
431
|
+
* looking at a duplicate. Account-token health says nothing about whether the
|
|
432
|
+
* attachment is still alive, so it was never the right question.
|
|
435
433
|
*
|
|
436
|
-
* The
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* own routes want. Without this the operator is sent back to
|
|
442
|
-
* `yolo-bridge login` for a session that never actually lost anything.
|
|
434
|
+
* The probe is card 07's own renewal route
|
|
435
|
+
* (`POST .../yolobridge/attach/:attachmentId/refresh`). That route re-reads
|
|
436
|
+
* live attachment state on every call and 403s a detached one, so it is
|
|
437
|
+
* simultaneously the liveness check AND the source of a fresh credential —
|
|
438
|
+
* there is deliberately no second probe to drift out of agreement with it.
|
|
443
439
|
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* second, possibly-drifted opinion about the window here.
|
|
440
|
+
* A failure of ANY kind (403 detached, 401 past the renewal window, a
|
|
441
|
+
* network error) is not fatal here: it just means there is nothing to
|
|
442
|
+
* resume, and the ordinary attach path below runs. The server, not this
|
|
443
|
+
* binary's copy of the window, is the authority on which it was.
|
|
449
444
|
*/
|
|
450
445
|
let resumed = false;
|
|
451
|
-
if (!
|
|
446
|
+
if (!deps.fresh) {
|
|
452
447
|
const stored = loadAttachment(env, io);
|
|
453
448
|
if (stored &&
|
|
454
449
|
stored.workspaceId === workspaceId &&
|
|
455
450
|
stored.scopedToken !== undefined &&
|
|
456
451
|
stored.scopedTokenExpiresAtMs !== undefined) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
452
|
+
try {
|
|
453
|
+
// A one-off config rather than `scopedCfg()`: the stored token is not
|
|
454
|
+
// adopted as THE credential until the server has confirmed it is
|
|
455
|
+
// still good, so a failed probe leaves the daemon's own state
|
|
456
|
+
// untouched and the fall-through is a clean ordinary attach.
|
|
457
|
+
const renewed = await apiClient.refreshScopedToken({ commonApiBaseUrl, accessToken: stored.scopedToken, fetchImpl }, workspaceId, stored.attachmentId);
|
|
458
|
+
attachmentId = stored.attachmentId;
|
|
459
|
+
tileId = stored.tileId;
|
|
460
|
+
attachedAt = stored.attachedAt;
|
|
461
|
+
rememberScopedCredential(renewed.scopedToken, renewed.scopedTokenExpiresAt);
|
|
462
|
+
resumed = true;
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
// Pre-spawn, so `log` is safe here (nothing owns the terminal yet) —
|
|
466
|
+
// and worth saying out loud: the operator is about to get a NEW tile
|
|
467
|
+
// where they may have expected the old one back.
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
log(`Stored attachment is no longer resumable (${message}) — attaching fresh.`);
|
|
470
|
+
}
|
|
462
471
|
}
|
|
463
|
-
|
|
472
|
+
}
|
|
473
|
+
if (!resumed) {
|
|
474
|
+
// ONLY the attach path needs the account credential — attach is the call
|
|
475
|
+
// that creates the scope, and nothing after it presents an account token.
|
|
476
|
+
// Keeping this refresh inside the branch is the ordering win D7 exists
|
|
477
|
+
// for: a daemon whose account token expired days ago, but whose
|
|
478
|
+
// attachment is still live, resumes above and never reaches this line.
|
|
479
|
+
// (It also still covers the original reason it existed: an `attach` run
|
|
480
|
+
// right after a long-down period, against a token already inside the
|
|
481
|
+
// refresh buffer, rotates before the very first network call.)
|
|
482
|
+
const initialRefresh = await ensureFreshToken();
|
|
483
|
+
if (!initialRefresh.ok) {
|
|
464
484
|
log(`Token refresh failed: ${initialRefresh.message}`);
|
|
465
485
|
log('Run `yolo-bridge login` again.');
|
|
466
486
|
return { ok: false, reason: 'refresh-failed', message: initialRefresh.message };
|
|
467
487
|
}
|
|
468
|
-
}
|
|
469
|
-
if (!resumed) {
|
|
470
488
|
try {
|
|
471
489
|
const result = await apiClient.attach(accountCfg, workspaceId, hostLabel, remoteHost);
|
|
472
490
|
attachmentId = result.attachmentId;
|
package/dist/cli.js
CHANGED
|
@@ -94,6 +94,10 @@ function printHelp() {
|
|
|
94
94
|
' [--agent-id <id>] Registry identity for local MCP access, if different from --agent',
|
|
95
95
|
' (e.g. a raw executable path, or an agent whose binary name differs',
|
|
96
96
|
' from its registry id like qwen-code/qwen). Defaults to --agent.',
|
|
97
|
+
' [--fresh] Always create a NEW attachment and tile. By default an attach that',
|
|
98
|
+
' finds a still-live attachment for this workspace on this machine',
|
|
99
|
+
' RESUMES it (same tile) instead of adding a duplicate; --fresh skips',
|
|
100
|
+
' that check entirely.',
|
|
97
101
|
' detach Detach the current workspace attachment.',
|
|
98
102
|
' status Print local login/attach state.',
|
|
99
103
|
' --help Print this help.',
|
|
@@ -139,8 +143,16 @@ export function parseAttachArgs(args) {
|
|
|
139
143
|
let hostLabel;
|
|
140
144
|
let agentBin;
|
|
141
145
|
let agentId;
|
|
146
|
+
let fresh = false;
|
|
142
147
|
for (let i = 0; i < args.length; i++) {
|
|
143
148
|
const a = args[i];
|
|
149
|
+
// Boolean — handled before the value-taking flags so it never swallows
|
|
150
|
+
// the following argument (`attach --fresh w1` must still see `w1` as the
|
|
151
|
+
// positional workspace id).
|
|
152
|
+
if (a === '--fresh') {
|
|
153
|
+
fresh = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
144
156
|
if (a === '--label' || a === '--agent' || a === '--agent-id') {
|
|
145
157
|
const value = args[i + 1];
|
|
146
158
|
if (value === undefined || value.startsWith('--')) {
|
|
@@ -162,7 +174,7 @@ export function parseAttachArgs(args) {
|
|
|
162
174
|
workspaceId = a;
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
|
-
return { workspaceId, hostLabel, agentBin, agentId };
|
|
177
|
+
return { workspaceId, hostLabel, agentBin, agentId, fresh };
|
|
166
178
|
}
|
|
167
179
|
/**
|
|
168
180
|
* Everything the attach handshake tells the workspace about this machine,
|
|
@@ -210,7 +222,7 @@ async function cmdAttach(args) {
|
|
|
210
222
|
const parsed = parseAttachArgs(args);
|
|
211
223
|
if ('error' in parsed) {
|
|
212
224
|
process.stderr.write(`yolo-bridge attach: ${parsed.error}\n`);
|
|
213
|
-
process.stderr.write('Usage: yolo-bridge attach [workspaceId] [--label <name>] [--agent <binary>] [--agent-id <registryId>]\n');
|
|
225
|
+
process.stderr.write('Usage: yolo-bridge attach [workspaceId] [--label <name>] [--agent <binary>] [--agent-id <registryId>] [--fresh]\n');
|
|
214
226
|
return 64;
|
|
215
227
|
}
|
|
216
228
|
let workspaceId = parsed.workspaceId;
|
|
@@ -222,6 +234,7 @@ async function cmdAttach(args) {
|
|
|
222
234
|
// match, which is every built-in agent this daemon has been used with so
|
|
223
235
|
// far (claude, codex).
|
|
224
236
|
const resolvedAgentId = parsed.agentId ?? agentBin ?? DEFAULT_AGENT_BIN;
|
|
237
|
+
const fresh = parsed.fresh === true;
|
|
225
238
|
if (workspaceId) {
|
|
226
239
|
const resolved = await resolveWorkspaceIdOrName(workspaceId, { commonApiBaseUrl: apiUrl() });
|
|
227
240
|
if (!resolved.ok) {
|
|
@@ -250,7 +263,7 @@ async function cmdAttach(args) {
|
|
|
250
263
|
process.stderr.write(`yolo-bridge attach: ${pick.message}\n`);
|
|
251
264
|
break;
|
|
252
265
|
}
|
|
253
|
-
process.stderr.write('Usage: yolo-bridge attach [workspaceId] [--label <name>] [--agent <binary>] [--agent-id <registryId>]\n');
|
|
266
|
+
process.stderr.write('Usage: yolo-bridge attach [workspaceId] [--label <name>] [--agent <binary>] [--agent-id <registryId>] [--fresh]\n');
|
|
254
267
|
return 64;
|
|
255
268
|
}
|
|
256
269
|
workspaceId = pick.workspaceId;
|
|
@@ -286,6 +299,7 @@ async function cmdAttach(args) {
|
|
|
286
299
|
commonApiBaseUrl: apiUrl(),
|
|
287
300
|
hostLabel: attachHostInfo.hostLabel,
|
|
288
301
|
remoteHost: attachHostInfo.remoteHost,
|
|
302
|
+
fresh,
|
|
289
303
|
shouldStop: () => stopRequested,
|
|
290
304
|
// Fires once the real tileId exists (docs/YOLOBRIDGE_PLAN.md's "Local
|
|
291
305
|
// MCP access" section) — starts the local MCP proxy and writes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-labs/yolobridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "YoloBridge — 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",
|