@yolo-labs/yolobridge 0.20.0 → 0.21.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 +21 -0
- package/dist/cli.js +33 -2
- package/dist/local-mcp-tools.js +49 -3
- package/dist/share-cmd.js +80 -4
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -303,3 +303,24 @@ export async function finalizeShare(cfg, workspaceId, attachmentId, assetId) {
|
|
|
303
303
|
const body = (await res.json());
|
|
304
304
|
return { assetId: body?.asset?.assetId ?? assetId };
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Phase 3, optional: write an already-shared asset into ANOTHER tile's session
|
|
308
|
+
* pod, so the agent there can open it as a real file.
|
|
309
|
+
*
|
|
310
|
+
* Only DELIVERY crosses to another tile. The asset itself stays on this
|
|
311
|
+
* attachment's tile — the server enforces that, and nothing here can ask
|
|
312
|
+
* otherwise.
|
|
313
|
+
*/
|
|
314
|
+
export async function deliverShare(cfg, workspaceId, attachmentId, assetId, targetTileId) {
|
|
315
|
+
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
316
|
+
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/attach/${attachmentId}/deliver`, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
319
|
+
body: JSON.stringify({ assetId, targetTileId }),
|
|
320
|
+
});
|
|
321
|
+
if (!res.ok) {
|
|
322
|
+
const { message, code } = await parseErrorBody(res);
|
|
323
|
+
throw new YoloBridgeApiError(message, res.status, code);
|
|
324
|
+
}
|
|
325
|
+
return (await res.json());
|
|
326
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { realpathSync, existsSync, readFileSync } from 'node:fs';
|
|
|
22
22
|
import { hostname } from 'node:os';
|
|
23
23
|
import { runLogin } from './login-cmd.js';
|
|
24
24
|
import { runAttachFromDisk, pickWorkspaceFromDisk } from './attach-cmd.js';
|
|
25
|
-
import { runShare } from './share-cmd.js';
|
|
25
|
+
import { runShare, runDeliver } from './share-cmd.js';
|
|
26
26
|
import { runAllow } from './approved-paths.js';
|
|
27
27
|
import { runDetach } from './detach-cmd.js';
|
|
28
28
|
import { getStatus, formatStatus } from './status-cmd.js';
|
|
@@ -106,6 +106,10 @@ function printHelp() {
|
|
|
106
106
|
' The daemon\'s own working directory is always allowed.',
|
|
107
107
|
' share <path> Share a local file with the attached workspace, so a cloud',
|
|
108
108
|
' agent can see it. Push only — nothing reads your disk remotely.',
|
|
109
|
+
' [--to <tileId>] Also write it into that tile\'s session, so its agent can open it.',
|
|
110
|
+
' deliver <assetId> Write an ALREADY-shared file into a tile\'s session, without',
|
|
111
|
+
' --to <tileId> uploading it again. This is the retry path when a share',
|
|
112
|
+
' uploaded fine but the delivery failed.',
|
|
109
113
|
' status Print local login/attach state.',
|
|
110
114
|
' version Print the installed yolo-bridge version (also --version, -v).',
|
|
111
115
|
' --help Print this help.',
|
|
@@ -566,6 +570,22 @@ function cmdStatus() {
|
|
|
566
570
|
process.stdout.write(`${formatStatus(getStatus())}\n`);
|
|
567
571
|
return 0;
|
|
568
572
|
}
|
|
573
|
+
async function cmdDeliver(args) {
|
|
574
|
+
const assetId = args.find((a) => !a.startsWith('-') && a !== args[args.indexOf('--to') + 1]);
|
|
575
|
+
const toIdx = args.indexOf('--to');
|
|
576
|
+
const targetTileId = toIdx >= 0 ? args[toIdx + 1] : undefined;
|
|
577
|
+
if (!assetId || !targetTileId) {
|
|
578
|
+
process.stderr.write('yolo-bridge deliver: usage — yolo-bridge deliver <assetId> --to <tileId>\n');
|
|
579
|
+
return 64;
|
|
580
|
+
}
|
|
581
|
+
const result = await runDeliver(assetId, targetTileId, { commonApiBaseUrl: apiUrl() });
|
|
582
|
+
if (!result.ok) {
|
|
583
|
+
process.stderr.write(`yolo-bridge deliver: ${result.message}\n`);
|
|
584
|
+
return 1;
|
|
585
|
+
}
|
|
586
|
+
process.stdout.write(`${result.path}\n`);
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
569
589
|
function cmdAllow(args) {
|
|
570
590
|
const result = runAllow(args);
|
|
571
591
|
if (!result.ok) {
|
|
@@ -581,13 +601,22 @@ async function cmdShare(args) {
|
|
|
581
601
|
process.stderr.write('yolo-bridge share: a file path is required.\n\n yolo-bridge share ./cut.mp4\n');
|
|
582
602
|
return 64;
|
|
583
603
|
}
|
|
584
|
-
|
|
604
|
+
// `--to <tileId>` also writes the file into that tile's session pod.
|
|
605
|
+
const toIdx = args.indexOf('--to');
|
|
606
|
+
const targetTileId = toIdx >= 0 ? args[toIdx + 1] : undefined;
|
|
607
|
+
if (toIdx >= 0 && (!targetTileId || targetTileId.startsWith('-'))) {
|
|
608
|
+
process.stderr.write('yolo-bridge share: `--to` needs a tile id.\n');
|
|
609
|
+
return 64;
|
|
610
|
+
}
|
|
611
|
+
const result = await runShare(rawPath, { commonApiBaseUrl: apiUrl(), targetTileId });
|
|
585
612
|
if (!result.ok) {
|
|
586
613
|
// Every one of these is an operator-actionable condition, not a bug, so it
|
|
587
614
|
// prints as a sentence with no stack trace.
|
|
588
615
|
process.stderr.write(`yolo-bridge share: ${result.message}\n`);
|
|
589
616
|
return 1;
|
|
590
617
|
}
|
|
618
|
+
if (result.deliveredPath)
|
|
619
|
+
process.stdout.write(`${result.deliveredPath}\n`);
|
|
591
620
|
return 0;
|
|
592
621
|
}
|
|
593
622
|
async function cmdWorkspaces() {
|
|
@@ -646,6 +675,8 @@ async function main() {
|
|
|
646
675
|
return cmdDetach();
|
|
647
676
|
case 'allow':
|
|
648
677
|
return cmdAllow(rest);
|
|
678
|
+
case 'deliver':
|
|
679
|
+
return cmdDeliver(rest);
|
|
649
680
|
case 'share':
|
|
650
681
|
return cmdShare(rest);
|
|
651
682
|
case 'status':
|
package/dist/local-mcp-tools.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* Do not write a message here claiming more than that.
|
|
26
26
|
*/
|
|
27
27
|
import { checkPathApproved } from './approved-paths.js';
|
|
28
|
-
import { runShare, describeShareFailure } from './share-cmd.js';
|
|
28
|
+
import { runShare, runDeliver, describeShareFailure } from './share-cmd.js';
|
|
29
29
|
/** Namespaced so it can never collide with a forwarded cloud tool name. */
|
|
30
30
|
export const SHARE_FILE_TOOL = 'yolobridge_share_file';
|
|
31
31
|
export const LOCAL_TOOL_DEFINITIONS = [
|
|
@@ -41,8 +41,30 @@ export const LOCAL_TOOL_DEFINITIONS = [
|
|
|
41
41
|
type: 'string',
|
|
42
42
|
description: 'Path to the local file to send. Must be inside an approved directory.',
|
|
43
43
|
},
|
|
44
|
+
assetId: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Optional. Deliver an ALREADY-shared asset instead of uploading again — use this to '
|
|
47
|
+
+ 'retry a delivery that failed after a successful upload. Requires targetTileId; '
|
|
48
|
+
+ '`path` is ignored when this is given.',
|
|
49
|
+
},
|
|
50
|
+
targetTileId: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Optional. A tile in this workspace to ALSO write the file into, so the agent running '
|
|
53
|
+
+ 'there can open it directly. Use studio_list_tiles to find the id. Omit to leave the '
|
|
54
|
+
+ 'file in the workspace only.',
|
|
55
|
+
},
|
|
44
56
|
},
|
|
45
|
-
required:
|
|
57
|
+
// TWO CALL SHAPES, so `path` cannot be unconditionally required:
|
|
58
|
+
// { path } — share a local file
|
|
59
|
+
// { assetId, targetTileId } — deliver one already shared, no re-upload
|
|
60
|
+
// A schema-aware client validates before dispatching, so requiring `path`
|
|
61
|
+
// outright made the retry form unreachable no matter what the handler
|
|
62
|
+
// accepted. (codex P1, gpt-5.6-sol — the unit tests call the handler
|
|
63
|
+
// directly and never saw it.)
|
|
64
|
+
anyOf: [
|
|
65
|
+
{ required: ['path'] },
|
|
66
|
+
{ required: ['assetId', 'targetTileId'] },
|
|
67
|
+
],
|
|
46
68
|
additionalProperties: false,
|
|
47
69
|
},
|
|
48
70
|
},
|
|
@@ -82,6 +104,24 @@ export async function runLocalToolCall(msg, ctx) {
|
|
|
82
104
|
if (name !== SHARE_FILE_TOOL) {
|
|
83
105
|
return isNotification ? undefined : toolResult(id, `Unknown local tool: ${String(name)}`, true);
|
|
84
106
|
}
|
|
107
|
+
const targetTileId = typeof args.targetTileId === 'string' && args.targetTileId ? args.targetTileId : undefined;
|
|
108
|
+
const retryAssetId = typeof args.assetId === 'string' && args.assetId ? args.assetId : undefined;
|
|
109
|
+
// Delivery-only retry: the bytes are already in the workspace, so there is no
|
|
110
|
+
// path to approve and nothing to read from disk.
|
|
111
|
+
if (retryAssetId) {
|
|
112
|
+
if (!targetTileId) {
|
|
113
|
+
return isNotification ? undefined : toolResult(id, '`targetTileId` is required when retrying delivery of an assetId.', true);
|
|
114
|
+
}
|
|
115
|
+
const redeliver = await (ctx.deliverImpl ?? runDeliver)(retryAssetId, targetTileId, {
|
|
116
|
+
commonApiBaseUrl: ctx.commonApiBaseUrl,
|
|
117
|
+
expectedWorkspaceId: ctx.workspaceId,
|
|
118
|
+
});
|
|
119
|
+
if (isNotification)
|
|
120
|
+
return undefined;
|
|
121
|
+
return redeliver.ok
|
|
122
|
+
? toolResult(id, `Delivered asset ${retryAssetId} to tile ${targetTileId} at ${redeliver.path}.`)
|
|
123
|
+
: toolResult(id, redeliver.message, true);
|
|
124
|
+
}
|
|
85
125
|
const rawPath = typeof args.path === 'string' ? args.path : '';
|
|
86
126
|
if (!rawPath)
|
|
87
127
|
return isNotification ? undefined : toolResult(id, 'A `path` is required.', true);
|
|
@@ -93,6 +133,7 @@ export async function runLocalToolCall(msg, ctx) {
|
|
|
93
133
|
// Property 2: open what the check RESOLVED, not what the caller passed.
|
|
94
134
|
const result = await (ctx.shareImpl ?? runShare)(check.resolvedPath, {
|
|
95
135
|
commonApiBaseUrl: ctx.commonApiBaseUrl,
|
|
136
|
+
targetTileId,
|
|
96
137
|
// The approval was checked against THIS workspace. The attachment on disk
|
|
97
138
|
// can have been replaced by a second `attach` since the proxy started, so
|
|
98
139
|
// bind the upload to the same workspace or refuse. (codex P2.)
|
|
@@ -103,7 +144,12 @@ export async function runLocalToolCall(msg, ctx) {
|
|
|
103
144
|
return undefined;
|
|
104
145
|
if (!result.ok)
|
|
105
146
|
return toolResult(id, result.message, true);
|
|
106
|
-
|
|
147
|
+
// When it was delivered, the PATH is the useful half — it is what the
|
|
148
|
+
// target agent needs to be told in order to open the file.
|
|
149
|
+
return toolResult(id, result.deliveredPath
|
|
150
|
+
? `Shared as asset ${result.assetId} and written into tile ${targetTileId} at ${result.deliveredPath}. `
|
|
151
|
+
+ 'Tell that tile\'s agent to open that path.'
|
|
152
|
+
: `Shared as asset ${result.assetId}. It is now visible in the cloud workspace.`);
|
|
107
153
|
}
|
|
108
154
|
catch (err) {
|
|
109
155
|
return isNotification ? undefined : toolResult(id, describeShareFailure(err), true);
|
package/dist/share-cmd.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { createReadStream } from 'node:fs';
|
|
16
16
|
import { stat } from 'node:fs/promises';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import { presignShare, finalizeShare, YoloBridgeApiError, } from './api-client.js';
|
|
18
|
+
import { presignShare, finalizeShare, deliverShare, YoloBridgeApiError, } from './api-client.js';
|
|
19
19
|
import { loadAuth, loadAttachment } from './config-store.js';
|
|
20
20
|
/**
|
|
21
21
|
* Mirrors common-api's `MAX_ASSET_BYTES`. The server is authoritative and
|
|
@@ -147,7 +147,22 @@ export async function shareFile(rawPath, deps) {
|
|
|
147
147
|
}
|
|
148
148
|
const finalized = await finalizeShare(cfg, deps.workspaceId, deps.attachmentId, presigned.assetId);
|
|
149
149
|
write(`Shared ${file.filename} → ${finalized.assetId}`);
|
|
150
|
-
|
|
150
|
+
if (!deps.targetTileId)
|
|
151
|
+
return finalized;
|
|
152
|
+
// The upload has ALREADY SUCCEEDED. If delivery fails the file is genuinely
|
|
153
|
+
// in the workspace, so say that rather than reporting a flat failure and
|
|
154
|
+
// inviting a re-upload of something already there.
|
|
155
|
+
try {
|
|
156
|
+
const delivered = await deliverShare(cfg, deps.workspaceId, deps.attachmentId, finalized.assetId, deps.targetTileId);
|
|
157
|
+
write(`Delivered to ${deps.targetTileId} at ${delivered.path}`);
|
|
158
|
+
return { ...finalized, deliveredPath: delivered.path };
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
const why = err instanceof YoloBridgeApiError ? err.message : err?.message || 'unknown error';
|
|
162
|
+
throw new ShareError(`${file.filename} was uploaded (asset ${finalized.assetId}) but could not be delivered to `
|
|
163
|
+
+ `${deps.targetTileId}: ${why}. The file IS in the workspace — retry the DELIVERY only, with `
|
|
164
|
+
+ `\`yolo-bridge deliver ${finalized.assetId} --to ${deps.targetTileId}\`, rather than sharing it again.`);
|
|
165
|
+
}
|
|
151
166
|
}
|
|
152
167
|
/** Turn an API error into something the operator can act on. */
|
|
153
168
|
export function describeShareFailure(err) {
|
|
@@ -174,6 +189,44 @@ export function describeShareFailure(err) {
|
|
|
174
189
|
* the config store rather than from daemon memory — the same credential the
|
|
175
190
|
* running daemon uses, and the only one the upload routes accept (Boundary B).
|
|
176
191
|
*/
|
|
192
|
+
/**
|
|
193
|
+
* The preconditions every daemon-credentialed command shares: logged in,
|
|
194
|
+
* attached, attached to the EXPECTED workspace, and holding a scoped token.
|
|
195
|
+
*
|
|
196
|
+
* Factored out so `runShare` and `runDeliver` cannot drift apart on which
|
|
197
|
+
* checks they perform or what they say when one fails.
|
|
198
|
+
*/
|
|
199
|
+
function resolveShareIdentity(deps) {
|
|
200
|
+
if (!loadAuth(deps.env, deps.io)) {
|
|
201
|
+
return { ok: false, reason: 'not-logged-in', message: 'Not logged in — run `yolo-bridge login` first.' };
|
|
202
|
+
}
|
|
203
|
+
const attachment = loadAttachment(deps.env, deps.io);
|
|
204
|
+
if (!attachment) {
|
|
205
|
+
return { ok: false, reason: 'not-attached', message: 'No active attachment — run `yolo-bridge attach` first.' };
|
|
206
|
+
}
|
|
207
|
+
if (deps.expectedWorkspaceId && attachment.workspaceId !== deps.expectedWorkspaceId) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: 'workspace-changed',
|
|
211
|
+
message: 'This machine is now attached to a different workspace than the one this request was '
|
|
212
|
+
+ 'authorised against. Nothing was sent. Re-run `yolo-bridge attach` or retry.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (!attachment.scopedToken) {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
reason: 'no-scoped-credential',
|
|
219
|
+
message: 'No workspace-scoped credential is stored for this attachment, so files cannot be shared '
|
|
220
|
+
+ 'from this machine. Run `yolo-bridge attach` to reconnect.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
ok: true,
|
|
225
|
+
cfg: { commonApiBaseUrl: deps.commonApiBaseUrl, accessToken: attachment.scopedToken, fetchImpl: deps.fetchImpl },
|
|
226
|
+
workspaceId: attachment.workspaceId,
|
|
227
|
+
attachmentId: attachment.attachmentId,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
177
230
|
export async function runShare(rawPath, deps) {
|
|
178
231
|
// Same precondition ordering as detach: `auth.json` is what makes this a
|
|
179
232
|
// set-up machine, and its absence has a far better remedy to offer than a
|
|
@@ -210,14 +263,37 @@ export async function runShare(rawPath, deps) {
|
|
|
210
263
|
fetchImpl: deps.fetchImpl,
|
|
211
264
|
};
|
|
212
265
|
try {
|
|
213
|
-
const { assetId } = await shareFile(rawPath, {
|
|
266
|
+
const { assetId, deliveredPath } = await shareFile(rawPath, {
|
|
214
267
|
cfg,
|
|
215
268
|
workspaceId: attachment.workspaceId,
|
|
216
269
|
attachmentId: attachment.attachmentId,
|
|
270
|
+
targetTileId: deps.targetTileId,
|
|
217
271
|
fetchImpl: deps.fetchImpl,
|
|
218
272
|
write: deps.write,
|
|
219
273
|
});
|
|
220
|
-
return { ok: true, assetId };
|
|
274
|
+
return { ok: true, assetId, ...(deliveredPath ? { deliveredPath } : {}) };
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
return { ok: false, reason: 'error', message: describeShareFailure(err) };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Deliver an ALREADY-SHARED asset into a tile's session, without re-uploading.
|
|
282
|
+
*
|
|
283
|
+
* WHY THIS EXISTS: `shareFile` tells the operator, after a delivery failure,
|
|
284
|
+
* that the file IS in the workspace and to retry the delivery rather than the
|
|
285
|
+
* upload. That instruction was false until this existed — every entry point
|
|
286
|
+
* began with a fresh presign, so the only available "retry" duplicated the
|
|
287
|
+
* asset. (codex P2, gpt-5.6-sol.) An error message must name a recovery the
|
|
288
|
+
* caller can actually perform.
|
|
289
|
+
*/
|
|
290
|
+
export async function runDeliver(assetId, targetTileId, deps) {
|
|
291
|
+
const preflight = await resolveShareIdentity(deps);
|
|
292
|
+
if (!preflight.ok)
|
|
293
|
+
return preflight;
|
|
294
|
+
try {
|
|
295
|
+
const delivered = await deliverShare(preflight.cfg, preflight.workspaceId, preflight.attachmentId, assetId, targetTileId);
|
|
296
|
+
return { ok: true, path: delivered.path };
|
|
221
297
|
}
|
|
222
298
|
catch (err) {
|
|
223
299
|
return { ok: false, reason: 'error', message: describeShareFailure(err) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-labs/yolobridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|