@yolo-labs/yolobridge 0.29.0 → 0.31.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/cli.js +19 -7
- package/dist/resolve-agent-binary.js +344 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ import { runShare, runDeliver } from './share-cmd.js';
|
|
|
35
35
|
import { runAllow } from './approved-paths.js';
|
|
36
36
|
import { runDetach } from './detach-cmd.js';
|
|
37
37
|
import { getStatus, formatStatus } from './status-cmd.js';
|
|
38
|
+
import { resolveAgentBinary } from './resolve-agent-binary.js';
|
|
38
39
|
import { startLocalAgent, stopLocalAgent, DEFAULT_AGENT_BIN } from './local-agent.js';
|
|
39
40
|
import { runListWorkspaces, formatWorkspacesTable } from './workspaces-cmd.js';
|
|
40
41
|
import { startMcpProxy, mcpUrl, SECRET_ENV_VAR } from './mcp-proxy.js';
|
|
@@ -326,13 +327,6 @@ async function cmdAttach(args) {
|
|
|
326
327
|
cliVersion: readOwnVersion(),
|
|
327
328
|
});
|
|
328
329
|
let mcpProxyHandle;
|
|
329
|
-
/**
|
|
330
|
-
* Serves terminals on 127.0.0.1 for the tile's "open terminal".
|
|
331
|
-
*
|
|
332
|
-
* ⚠️ SEPARATE FROM THE AGENT PTY. `startLocalAgent` owns the one agent
|
|
333
|
-
* session; this owns any shells the operator opens from the workspace. They
|
|
334
|
-
* share a lifetime — both die with the attach — and nothing else.
|
|
335
|
-
*/
|
|
336
330
|
// argv fragment pointing the spawned agent at the local MCP proxy, or
|
|
337
331
|
// `[]` when MCP isn't wired in — see `agent-mcp-args.ts`. Nothing else is
|
|
338
332
|
// tracked for cleanup any more: as of 2026-08-26 `attach` writes NOTHING
|
|
@@ -472,6 +466,24 @@ async function cmdAttach(args) {
|
|
|
472
466
|
// Say where Ctrl+C goes BEFORE the agent takes over the screen.
|
|
473
467
|
// Without this the operator presses it expecting to quit, nothing
|
|
474
468
|
// happens, and there is no way to discover why.
|
|
469
|
+
// ⚠️ RESOLVE THE BINARY BEFORE SPAWNING IT, so a failure can be
|
|
470
|
+
// EXPLAINED. node-pty reports an unspawnable binary as the bare
|
|
471
|
+
// string `posix_spawnp failed.` — no binary name, no PATH, no
|
|
472
|
+
// remedy — which on macOS reached an operator as "failed to start
|
|
473
|
+
// the local agent (posix_spawnp failed.)" and told them nothing
|
|
474
|
+
// they could act on. Worse, the one fact they could see (`claude`
|
|
475
|
+
// works when typed) argued that nothing was wrong, because a shell
|
|
476
|
+
// alias or function resolves interactively and cannot be spawned.
|
|
477
|
+
// This does not change WHETHER the attach fails — only whether the
|
|
478
|
+
// operator can tell why.
|
|
479
|
+
const resolvedBinary = resolveAgentBinary(agentBin ?? DEFAULT_AGENT_BIN, process.env);
|
|
480
|
+
if (!resolvedBinary.ok) {
|
|
481
|
+
process.stdout.write(`yolo-bridge: cannot start the agent — ${resolvedBinary.message}\n`);
|
|
482
|
+
process.stdout.write('yolo-bridge: detaching...\n');
|
|
483
|
+
stopRequested = true;
|
|
484
|
+
await runDetach({ commonApiBaseUrl: apiUrl() }).catch(() => undefined);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
475
487
|
process.stdout.write('yolo-bridge: Ctrl+C goes to the agent · Ctrl-P Ctrl-Q to detach\n');
|
|
476
488
|
startLocalAgent({
|
|
477
489
|
agentBin,
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Can the agent binary actually be spawned, and if not, WHY?
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ THE FAILURE THIS EXISTS FOR. node-pty reports an unspawnable binary as
|
|
5
|
+
* the literal string `posix_spawnp failed.` — no binary name, no PATH, no
|
|
6
|
+
* errno, no remedy. On macOS that surfaced as:
|
|
7
|
+
*
|
|
8
|
+
* yolo-bridge: failed to start the local agent (posix_spawnp failed.), detaching...
|
|
9
|
+
*
|
|
10
|
+
* which is true, useless, and indistinguishable from half a dozen unrelated
|
|
11
|
+
* causes. The operator cannot tell whether the agent is missing, installed but
|
|
12
|
+
* not executable, or whether `attach` even looked in the right place — and the
|
|
13
|
+
* one thing they CAN see (`claude` works when they type it) argues that
|
|
14
|
+
* nothing is wrong.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ THE MACOS TRAP THIS NAMES EXPLICITLY. `claude` working in the operator's
|
|
17
|
+
* terminal does NOT mean there is a binary to spawn: a shell alias or function
|
|
18
|
+
* from `.zshrc` resolves interactively and does not exist as a file, so
|
|
19
|
+
* `posix_spawnp` cannot find it however correct the PATH looks. A message that
|
|
20
|
+
* only says "not found" invites the reply "but it IS installed", so this says
|
|
21
|
+
* where it looked and offers the escape hatch.
|
|
22
|
+
*
|
|
23
|
+
* Resolution mirrors `posix_spawnp`: a name containing a separator is a path,
|
|
24
|
+
* anything else is searched across PATH in order.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
const DEFAULT_FS = {
|
|
29
|
+
statSync: (p) => fs.statSync(p),
|
|
30
|
+
accessSync: (p, mode) => fs.accessSync(p, mode),
|
|
31
|
+
constants: { X_OK: fs.constants.X_OK },
|
|
32
|
+
readFirstLine(p) {
|
|
33
|
+
// Only the shebang is wanted, and the file may be a multi-megabyte binary,
|
|
34
|
+
// so this reads a small prefix rather than slurping it.
|
|
35
|
+
let fd;
|
|
36
|
+
try {
|
|
37
|
+
fd = fs.openSync(p, 'r');
|
|
38
|
+
const buf = Buffer.alloc(512);
|
|
39
|
+
const read = fs.readSync(fd, buf, 0, 512, 0);
|
|
40
|
+
const text = buf.subarray(0, read).toString('utf-8');
|
|
41
|
+
const nl = text.indexOf('\n');
|
|
42
|
+
return nl === -1 ? text : text.slice(0, nl);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
if (fd !== undefined) {
|
|
49
|
+
try {
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
}
|
|
52
|
+
catch { /* already gone */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The interpreter a `#!` line names, resolved the way the kernel would.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ THIS IS THE CASE EVERY OTHER CHECK PASSES. An executable script whose
|
|
61
|
+
* interpreter is missing fails with ENOENT — the kernel reports the absent
|
|
62
|
+
* INTERPRETER, and it is indistinguishable from the script itself not
|
|
63
|
+
* existing. So a binary can be on PATH, be a real file, and be +x, and still
|
|
64
|
+
* be unspawnable. That is precisely the macOS report this was written for:
|
|
65
|
+
* `type -a claude` listed two real installs while node-pty said only
|
|
66
|
+
* `posix_spawnp failed.`
|
|
67
|
+
*
|
|
68
|
+
* `#!/usr/bin/env node` delegates the search back to PATH, so the argument is
|
|
69
|
+
* resolved recursively rather than treated as a literal path.
|
|
70
|
+
*/
|
|
71
|
+
function checkInterpreter(scriptPath, env, io, depth = 0) {
|
|
72
|
+
if (depth > 4)
|
|
73
|
+
return { ok: true }; // pathological chain; let the spawn speak
|
|
74
|
+
const first = io.readFirstLine(scriptPath);
|
|
75
|
+
if (!first || !first.startsWith('#!'))
|
|
76
|
+
return { ok: true }; // a real binary, not a script
|
|
77
|
+
const parts = tokenizeShebang(first.slice(2).trim());
|
|
78
|
+
if (parts.length === 0)
|
|
79
|
+
return { ok: true };
|
|
80
|
+
const [interpreter, ...rest] = parts;
|
|
81
|
+
// ⚠️ THE INTERPRETER ITSELF FIRST, ALWAYS. `#!/missing/env node` must fail on
|
|
82
|
+
// `/missing/env` even when `node` is perfectly available: the kernel execs
|
|
83
|
+
// the interpreter, and delegating straight to env's argument would report a
|
|
84
|
+
// clean bill of health for a script that cannot spawn. (codex P2.)
|
|
85
|
+
const interpreterFound = resolveAgentBinary(interpreter, env, io, depth + 1);
|
|
86
|
+
if (!interpreterFound.ok) {
|
|
87
|
+
// ⚠️ DO NOT BLAME THE OUTER INTERPRETER FOR AN INNER FAULT. If the
|
|
88
|
+
// interpreter EXISTS but is itself a script with a missing interpreter
|
|
89
|
+
// (claude -> /usr/bin/node -> /missing/runtime), its own message already
|
|
90
|
+
// names the file that is actually absent. Overwriting it here would state
|
|
91
|
+
// that /usr/bin/node cannot be found — about a file that is right there —
|
|
92
|
+
// which is the same misdirection this whole check exists to end.
|
|
93
|
+
// (codex P2.)
|
|
94
|
+
if (interpreterFound.reason === 'bad-interpreter') {
|
|
95
|
+
return { ok: false, nested: interpreterFound.message };
|
|
96
|
+
}
|
|
97
|
+
return { ok: false, interpreter, via: scriptPath };
|
|
98
|
+
}
|
|
99
|
+
// `env` hands the search back to PATH, so the command it delegates to is a
|
|
100
|
+
// second thing that must exist.
|
|
101
|
+
if (path.basename(interpreter) === 'env' && rest.length > 0) {
|
|
102
|
+
const { command, env: envForCommand } = envInvocation(rest, env);
|
|
103
|
+
if (!command)
|
|
104
|
+
return { ok: true };
|
|
105
|
+
// ⚠️ `envForCommand`, not `env`: the lookup must use the environment env
|
|
106
|
+
// will have BUILT by the time it resolves the command.
|
|
107
|
+
const resolved = resolveAgentBinary(command, envForCommand, io, depth + 1);
|
|
108
|
+
if (resolved.ok)
|
|
109
|
+
return { ok: true };
|
|
110
|
+
if (resolved.reason === 'bad-interpreter')
|
|
111
|
+
return { ok: false, nested: resolved.message };
|
|
112
|
+
return { ok: false, interpreter: command, via: scriptPath };
|
|
113
|
+
}
|
|
114
|
+
return { ok: true };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Split a shebang tail the way a shell would, not on raw whitespace.
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ KNOWN LIMIT, ACCEPTED DELIBERATELY. This is shell-style tokenization, and
|
|
120
|
+
* the kernel's rules differ by platform: macOS splits a shebang tail on
|
|
121
|
+
* whitespace (so this matches it closely — and macOS is where the report that
|
|
122
|
+
* prompted this came from), while Linux hands the whole tail to the
|
|
123
|
+
* interpreter as ONE literal argument. So an exotic line like
|
|
124
|
+
* `#!/usr/bin/env NODE_ENV="a b" node` can be called spawnable here and still
|
|
125
|
+
* fail to exec on Linux.
|
|
126
|
+
*
|
|
127
|
+
* That residual gap is the RIGHT direction to leave open. Missing a broken
|
|
128
|
+
* script costs nothing new — the spawn then fails exactly as it does today,
|
|
129
|
+
* with node-pty's opaque message, which is the status quo this file improves
|
|
130
|
+
* on rather than a regression it introduces. Falsely REJECTING a working
|
|
131
|
+
* agent, by contrast, would block a working setup and be strictly worse than
|
|
132
|
+
* the silence being replaced. Every fix in review has closed a false-reject;
|
|
133
|
+
* this one would trade a small class of false-accepts for the risk of new
|
|
134
|
+
* false-rejects by emulating two kernels' exec semantics, which is not a
|
|
135
|
+
* trade a diagnostic helper should make.
|
|
136
|
+
*
|
|
137
|
+
* ⚠️ QUOTED VALUES CONTAIN SPACES. `env -S NODE_OPTIONS="--require /tmp/h.js"
|
|
138
|
+
* node` is a valid, spawnable shebang; splitting it on whitespace picks
|
|
139
|
+
* `/tmp/h.js"` as the command and declares a working agent broken. Rejecting
|
|
140
|
+
* something that works is a worse outcome than the silence this file replaces,
|
|
141
|
+
* so quotes and backslash escapes are honoured. (codex P2.)
|
|
142
|
+
*/
|
|
143
|
+
export function tokenizeShebang(tail) {
|
|
144
|
+
const out = [];
|
|
145
|
+
let cur = '';
|
|
146
|
+
let quote = null;
|
|
147
|
+
let started = false;
|
|
148
|
+
for (let i = 0; i < tail.length; i++) {
|
|
149
|
+
const c = tail[i];
|
|
150
|
+
if (c === '\\' && quote !== "'" && i + 1 < tail.length) {
|
|
151
|
+
cur += tail[++i];
|
|
152
|
+
started = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (quote) {
|
|
156
|
+
if (c === quote)
|
|
157
|
+
quote = null;
|
|
158
|
+
else
|
|
159
|
+
cur += c;
|
|
160
|
+
started = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (c === '"' || c === "'") {
|
|
164
|
+
quote = c;
|
|
165
|
+
started = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (/\s/.test(c)) {
|
|
169
|
+
if (started) {
|
|
170
|
+
out.push(cur);
|
|
171
|
+
cur = '';
|
|
172
|
+
started = false;
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
cur += c;
|
|
177
|
+
started = true;
|
|
178
|
+
}
|
|
179
|
+
if (started)
|
|
180
|
+
out.push(cur);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The command `env` will run, AND the environment it will run it in.
|
|
185
|
+
*
|
|
186
|
+
* ⚠️ BOTH HALVES MATTER. `env` constructs the environment first and resolves
|
|
187
|
+
* the command second, so `#!/usr/bin/env PATH=/opt/runtime/bin node` looks for
|
|
188
|
+
* `node` in `/opt/runtime/bin` — not wherever the daemon's own PATH points.
|
|
189
|
+
* Resolving against the daemon's PATH would reject a spawnable agent whose
|
|
190
|
+
* runtime lives only in the assigned path, and accept one that will fail the
|
|
191
|
+
* moment env replaces PATH. (codex P2.)
|
|
192
|
+
*
|
|
193
|
+
* ⚠️ The command is NOT "the first token without a leading dash". Every one of
|
|
194
|
+
* these is valid and its command is `node`, and that naive rule picks the
|
|
195
|
+
* wrong token in three:
|
|
196
|
+
*
|
|
197
|
+
* env node -> node
|
|
198
|
+
* env -S node --flag -> node (-S splits the rest)
|
|
199
|
+
* env -u NODE_OPTIONS node -> node (-u CONSUMES an operand)
|
|
200
|
+
* env NODE_ENV=production node -> node (NAME=VALUE assignment)
|
|
201
|
+
* env PATH=/opt/runtime/bin node -> node, looked up in /opt/runtime/bin
|
|
202
|
+
*/
|
|
203
|
+
export function envInvocation(args, base) {
|
|
204
|
+
const TAKES_OPERAND = new Set(['-u', '--unset', '-C', '--chdir']);
|
|
205
|
+
let env = { ...base };
|
|
206
|
+
let i = 0;
|
|
207
|
+
while (i < args.length) {
|
|
208
|
+
const a = args[i];
|
|
209
|
+
if (a === '--') {
|
|
210
|
+
i += 1;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
// `-i` starts from an EMPTY environment, which means no PATH at all — the
|
|
214
|
+
// delegated lookup then has nothing to search, and saying so is more use
|
|
215
|
+
// than guessing.
|
|
216
|
+
if (a === '-i' || a === '--ignore-environment' || a === '-') {
|
|
217
|
+
env = {};
|
|
218
|
+
i += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (a === '-u' || a === '--unset') {
|
|
222
|
+
if (args[i + 1])
|
|
223
|
+
delete env[args[i + 1]];
|
|
224
|
+
i += 2;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (a.startsWith('-u') && a.length > 2) {
|
|
228
|
+
delete env[a.slice(2)];
|
|
229
|
+
i += 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (TAKES_OPERAND.has(a)) {
|
|
233
|
+
i += 2;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (a.startsWith('-')) {
|
|
237
|
+
i += 1;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/.exec(a);
|
|
241
|
+
if (assignment) {
|
|
242
|
+
env[assignment[1]] = assignment[2];
|
|
243
|
+
i += 1;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
return { command: a, env };
|
|
247
|
+
}
|
|
248
|
+
return { command: args[i], env };
|
|
249
|
+
}
|
|
250
|
+
/** A found binary, unless its shebang interpreter is the thing that is missing. */
|
|
251
|
+
function withInterpreter(found, env, io, depth) {
|
|
252
|
+
const interp = checkInterpreter(found, env, io, depth);
|
|
253
|
+
if (interp.ok)
|
|
254
|
+
return { ok: true, path: found };
|
|
255
|
+
if ('nested' in interp)
|
|
256
|
+
return { ok: false, reason: 'bad-interpreter', message: interp.nested };
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
reason: 'bad-interpreter',
|
|
260
|
+
message: `\`${found}\` exists and is executable, but it is a script whose interpreter\n`
|
|
261
|
+
+ ` \`${interp.interpreter}\` cannot be found. The spawn fails with a "not found" error that\n`
|
|
262
|
+
+ ` names the SCRIPT rather than the missing interpreter, which is why this looks like\n`
|
|
263
|
+
+ ` the agent is missing when it plainly is not.\n`
|
|
264
|
+
+ ` Reinstall the agent against a current runtime, or point yolo-bridge at another copy\n`
|
|
265
|
+
+ ` with \`--agent /full/path\` (or YOLOBRIDGE_AGENT_BIN).`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function isExecutableFile(p, io) {
|
|
269
|
+
try {
|
|
270
|
+
if (!io.statSync(p).isFile())
|
|
271
|
+
return 'no';
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return 'no';
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
io.accessSync(p, io.constants.X_OK);
|
|
278
|
+
return 'yes';
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return 'not-executable';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Where `bin` would be spawned from, or a sentence explaining why it cannot be.
|
|
286
|
+
*
|
|
287
|
+
* The message is the whole point: it is what the operator reads instead of
|
|
288
|
+
* `posix_spawnp failed.`, so it names the binary, says where the search
|
|
289
|
+
* looked, and gives the two ways out.
|
|
290
|
+
*/
|
|
291
|
+
export function resolveAgentBinary(bin, env = process.env, io = DEFAULT_FS, depth = 0) {
|
|
292
|
+
const escapeHatch = `Point yolo-bridge at it explicitly with \`--agent /full/path/to/${bin}\` `
|
|
293
|
+
+ `(or set YOLOBRIDGE_AGENT_BIN).`;
|
|
294
|
+
// An explicit path is taken at its word — no PATH search, same as execve.
|
|
295
|
+
if (bin.includes(path.sep) || bin.includes('/')) {
|
|
296
|
+
const abs = path.resolve(bin);
|
|
297
|
+
const state = isExecutableFile(abs, io);
|
|
298
|
+
if (state === 'yes')
|
|
299
|
+
return withInterpreter(abs, env, io, depth);
|
|
300
|
+
if (state === 'not-executable') {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
reason: 'not-executable',
|
|
304
|
+
message: `The agent at ${abs} exists but is not executable. \`chmod +x ${abs}\` and re-attach.`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return { ok: false, reason: 'not-found', message: `No agent binary at ${abs}. ${escapeHatch}` };
|
|
308
|
+
}
|
|
309
|
+
const rawPath = env.PATH ?? '';
|
|
310
|
+
if (!rawPath.trim()) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
reason: 'no-path',
|
|
314
|
+
message: `Cannot look for \`${bin}\`: PATH is empty in this process. ${escapeHatch}`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const dirs = rawPath.split(path.delimiter).filter(Boolean);
|
|
318
|
+
let sawNonExecutable;
|
|
319
|
+
for (const dir of dirs) {
|
|
320
|
+
const candidate = path.join(dir, bin);
|
|
321
|
+
const state = isExecutableFile(candidate, io);
|
|
322
|
+
if (state === 'yes')
|
|
323
|
+
return withInterpreter(candidate, env, io, depth);
|
|
324
|
+
if (state === 'not-executable' && !sawNonExecutable)
|
|
325
|
+
sawNonExecutable = candidate;
|
|
326
|
+
}
|
|
327
|
+
if (sawNonExecutable) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
reason: 'not-executable',
|
|
331
|
+
message: `Found \`${bin}\` at ${sawNonExecutable}, but it is not executable. `
|
|
332
|
+
+ `\`chmod +x ${sawNonExecutable}\` and re-attach.`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
reason: 'not-found',
|
|
338
|
+
message: `\`${bin}\` is not on this process's PATH (searched ${dirs.length} `
|
|
339
|
+
+ `${dirs.length === 1 ? 'directory' : 'directories'}).\n`
|
|
340
|
+
+ ` ⚠️ If \`${bin}\` works when you type it, it may be a shell alias or function rather than\n`
|
|
341
|
+
+ ` a real binary — those cannot be spawned. Check with: type -a ${bin}\n`
|
|
342
|
+
+ ` ${escapeHatch}`,
|
|
343
|
+
};
|
|
344
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolo-labs/yolobridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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",
|