@frod.io/bridge 0.7.5 → 0.7.6
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/README.md +52 -6
- package/dist/cli.js +418 -102
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,9 +145,10 @@ says the same). When the desk is clear, it swaps `current` atomically, logs
|
|
|
145
145
|
`bridge.updated {from, to}`, and restarts through the supervisor: it exits
|
|
146
146
|
with code 75, a non-zero exit that launchd's `KeepAlive SuccessfulExit=false`
|
|
147
147
|
and systemd's `Restart=on-failure` both bring straight back up — a clean exit
|
|
148
|
-
(code 0) is deliberately a final goodbye, not a restart signal. Windows
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
(code 0) is deliberately a final goodbye, not a restart signal. On Windows the
|
|
149
|
+
logon task carries the same rule (`RestartOnFailure`, three tries a minute
|
|
150
|
+
apart), and the bridge also relaunches the new version itself, detached and
|
|
151
|
+
with no console window; if that fails, the next logon starts it.
|
|
151
152
|
|
|
152
153
|
The keychain item is never touched by an update. You'll see `Updated to <v>`
|
|
153
154
|
in `~/.frod/bridge.log` and the new version on the computer card. Two
|
|
@@ -258,12 +259,57 @@ Never-ask policy as the others. Each adapter is one file.
|
|
|
258
259
|
unit is written correctly, but `systemctl --user` doesn't exist in a
|
|
259
260
|
container, so the manual command is printed instead. Not yet proven on a
|
|
260
261
|
real Linux machine.
|
|
261
|
-
- **Windows** — a
|
|
262
|
-
(`schtasks /Create
|
|
263
|
-
|
|
262
|
+
- **Windows** — a logon task *Frod.io bridge*, registered from a task
|
|
263
|
+
definition (`schtasks /Create /XML`, written to `~/.frod/bridge-task.xml`)
|
|
264
|
+
and pointing at `current` through `~/.frod/bridge-hidden.vbs`, a launcher
|
|
265
|
+
that starts the bridge with **no window** and appends its output to
|
|
266
|
+
`~/.frod/bridge.log`. The definition also says what the command line cannot:
|
|
267
|
+
hidden, no 72-hour execution limit, indifferent to battery, restart after a
|
|
268
|
+
crash. A real Windows *service* would run as SYSTEM while the runtime CLIs
|
|
269
|
+
are signed in as the person, so a logon task — the equivalent of the macOS
|
|
270
|
+
user agent — is deliberate. If the definition is refused, the plain
|
|
271
|
+
`schtasks /Create /TR` form is still tried (it works, and it shows a console
|
|
272
|
+
window), and the exact commands are printed. Not yet proven on a real
|
|
273
|
+
Windows machine.
|
|
274
|
+
|
|
275
|
+
Finding the runtimes on Windows needs no hand configuration either: `claude`,
|
|
276
|
+
`codex` and `grok` are looked for in npm's global folder (`%APPDATA%\npm`),
|
|
277
|
+
`~/.local/bin`, the per-tool folders and `%LOCALAPPDATA%\Programs`, with
|
|
278
|
+
`.exe`, `.cmd` and `.bat` tried in that order, then `where.exe`. An npm
|
|
279
|
+
`.cmd` shim cannot be spawned without a shell (CVE-2024-27980), so the shim is
|
|
280
|
+
read and the JavaScript entry it names is started with the bridge's own node —
|
|
281
|
+
which also keeps Claude Code's system prompt out of a cmd.exe command line and
|
|
282
|
+
its ~8 KB limit. `FROD_CLAUDE_PATH` / `FROD_CODEX_PATH` / `FROD_GROK_PATH`
|
|
283
|
+
still override everything, on every OS.
|
|
264
284
|
|
|
265
285
|
## Changelog
|
|
266
286
|
|
|
287
|
+
### 0.7.6
|
|
288
|
+
|
|
289
|
+
- **Windows, without hand configuration.** The runtimes are found where Windows
|
|
290
|
+
actually puts them (npm's global folder, `~/.local/bin`, the per-tool folders,
|
|
291
|
+
then `where.exe`) instead of only Unix paths and a `/usr/bin/which` that does
|
|
292
|
+
not exist there — no `FROD_*_PATH` to set by hand. An npm `.cmd` shim is read
|
|
293
|
+
and its JavaScript entry started with our own node, because Node refuses to
|
|
294
|
+
spawn a `.cmd` without a shell and a cmd.exe command line cannot carry an
|
|
295
|
+
agent's system prompt.
|
|
296
|
+
- **Windows, without a window.** The logon task is registered from a task
|
|
297
|
+
definition and runs a launcher that starts the bridge hidden and logs to
|
|
298
|
+
`~/.frod/bridge.log` — so there is no console window a person can close and
|
|
299
|
+
kill the bridge with. The definition also turns off the 72-hour execution
|
|
300
|
+
limit and the stop-on-battery default, and turns on restart-after-crash. The
|
|
301
|
+
old command-line form remains the fallback. Not yet proven on real Windows
|
|
302
|
+
hardware.
|
|
303
|
+
- **A computer's desk is sized by its memory**, not fixed at two sessions: the
|
|
304
|
+
ceiling is what memory allows after a 4 GB reserve at 200 MB a session, and
|
|
305
|
+
the default is 30 or that ceiling, whichever is smaller. A computer joined
|
|
306
|
+
before this is raised once, and a number a person chose is never overruled.
|
|
307
|
+
- **Capacity reads Claude Code's Keychain item by account**, as Claude Code
|
|
308
|
+
writes it, so a Mac that is signed in is no longer reported signed out
|
|
309
|
+
because of a stray item; a failed read now says why.
|
|
310
|
+
- Per-turn usage on Codex and Grok Build is billed as the increment, not the
|
|
311
|
+
running session total.
|
|
312
|
+
|
|
267
313
|
### 0.7.4 — unreleased
|
|
268
314
|
|
|
269
315
|
- `join` is runtime-neutral: it admits a computer when at least one of Claude
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @frod.io/bridge 0.7.
|
|
2
|
+
// @frod.io/bridge 0.7.6. One file; do not edit: the source is apps/bridge/src in the platform repository.
|
|
3
3
|
import { createRequire as __createRequire } from 'node:module';
|
|
4
4
|
const require = __createRequire(import.meta.url);
|
|
5
5
|
var __create = Object.create;
|
|
@@ -3705,10 +3705,10 @@ var require_websocket_server = __commonJS({
|
|
|
3705
3705
|
});
|
|
3706
3706
|
|
|
3707
3707
|
// src/cli.ts
|
|
3708
|
-
import { execFileSync as
|
|
3709
|
-
import { existsSync as
|
|
3710
|
-
import { homedir as
|
|
3711
|
-
import { dirname as dirname5, join as
|
|
3708
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
3709
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, realpathSync as realpathSync4 } from "node:fs";
|
|
3710
|
+
import { homedir as homedir8, hostname as hostname2, platform as platform6 } from "node:os";
|
|
3711
|
+
import { dirname as dirname5, join as join12, resolve as resolve6 } from "node:path";
|
|
3712
3712
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3713
3713
|
import { spawn as spawn6 } from "node:child_process";
|
|
3714
3714
|
|
|
@@ -3904,7 +3904,7 @@ function leave(apiUrl, token) {
|
|
|
3904
3904
|
|
|
3905
3905
|
// src/adapters/claude.ts
|
|
3906
3906
|
import { spawn, execFileSync as execFileSync2 } from "node:child_process";
|
|
3907
|
-
import { mkdirSync as mkdirSync2, existsSync as
|
|
3907
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3908
3908
|
import { join as join5 } from "node:path";
|
|
3909
3909
|
|
|
3910
3910
|
// src/adapters/claude-stream.ts
|
|
@@ -4184,6 +4184,44 @@ var log = {
|
|
|
4184
4184
|
}
|
|
4185
4185
|
};
|
|
4186
4186
|
|
|
4187
|
+
// src/win-launch.ts
|
|
4188
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4189
|
+
import { win32 as winPath } from "node:path";
|
|
4190
|
+
function defaultLaunchIo() {
|
|
4191
|
+
return {
|
|
4192
|
+
platform: process.platform,
|
|
4193
|
+
env: process.env,
|
|
4194
|
+
node: process.execPath,
|
|
4195
|
+
exists: existsSync4,
|
|
4196
|
+
readFile: (path) => readFileSync4(path, "utf8")
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
var BATCH = /\.(cmd|bat)$/i;
|
|
4200
|
+
var SHIM_ENTRY = /%~?dp0%?[\\/]+([^"\s]+\.(?:c|m)?js)/i;
|
|
4201
|
+
function shimEntry(shimPath, io = defaultLaunchIo()) {
|
|
4202
|
+
let text;
|
|
4203
|
+
try {
|
|
4204
|
+
text = io.readFile(shimPath);
|
|
4205
|
+
} catch {
|
|
4206
|
+
return null;
|
|
4207
|
+
}
|
|
4208
|
+
const found = SHIM_ENTRY.exec(text)?.[1];
|
|
4209
|
+
if (!found) return null;
|
|
4210
|
+
const entry = winPath.isAbsolute(found) ? found : winPath.resolve(winPath.dirname(shimPath), found);
|
|
4211
|
+
return io.exists(entry) ? entry : null;
|
|
4212
|
+
}
|
|
4213
|
+
function launchOf(exe, args = [], io = defaultLaunchIo()) {
|
|
4214
|
+
if (io.platform !== "win32" || !BATCH.test(exe)) return { command: exe, args, via: "direct" };
|
|
4215
|
+
const entry = shimEntry(exe, io);
|
|
4216
|
+
if (entry) return { command: io.node, args: [entry, ...args], via: "node-shim" };
|
|
4217
|
+
return {
|
|
4218
|
+
command: io.env.ComSpec ?? "cmd.exe",
|
|
4219
|
+
args: ["/d", "/s", "/c", exe, ...args],
|
|
4220
|
+
via: "cmd",
|
|
4221
|
+
note: `${exe} is a batch shim whose entry could not be read, so it starts through cmd.exe \u2014 where a long system prompt (more than about 8 KB of arguments) fails. Point the FROD_CLAUDE_PATH / FROD_CODEX_PATH / FROD_GROK_PATH override at a .exe if there is one.`
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4187
4225
|
// src/permission-policy.ts
|
|
4188
4226
|
import { resolve, isAbsolute, sep, dirname } from "node:path";
|
|
4189
4227
|
import { realpathSync } from "node:fs";
|
|
@@ -4462,7 +4500,7 @@ function cleanEnv(base, extra = {}) {
|
|
|
4462
4500
|
var CHILDREN_PATH = join5(FROD_DIR, "children.json");
|
|
4463
4501
|
function readChildren() {
|
|
4464
4502
|
try {
|
|
4465
|
-
return
|
|
4503
|
+
return existsSync5(CHILDREN_PATH) ? JSON.parse(readFileSync5(CHILDREN_PATH, "utf8")) : [];
|
|
4466
4504
|
} catch {
|
|
4467
4505
|
return [];
|
|
4468
4506
|
}
|
|
@@ -4514,8 +4552,12 @@ var ClaudeAdapter = class {
|
|
|
4514
4552
|
}
|
|
4515
4553
|
/** `claude --version`, and whether the CLI says it is signed in. Never throws. */
|
|
4516
4554
|
static check(claudePath, commands = {}) {
|
|
4517
|
-
const
|
|
4518
|
-
|
|
4555
|
+
const run = (path, args) => {
|
|
4556
|
+
const l = launchOf(path, args);
|
|
4557
|
+
return execFileSync2(l.command, l.args, { env: cleanEnv(process.env), stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString();
|
|
4558
|
+
};
|
|
4559
|
+
const versionCommand = commands.version ?? ((path) => run(path, ["--version"]));
|
|
4560
|
+
const authStatusCommand = commands.authStatus ?? ((path) => run(path, ["auth", "status"]));
|
|
4519
4561
|
let version;
|
|
4520
4562
|
try {
|
|
4521
4563
|
version = versionCommand(claudePath).trim();
|
|
@@ -4532,7 +4574,7 @@ var ClaudeAdapter = class {
|
|
|
4532
4574
|
}
|
|
4533
4575
|
}
|
|
4534
4576
|
async start(spec, o) {
|
|
4535
|
-
if (!
|
|
4577
|
+
if (!existsSync5(o.sessionDir)) mkdirSync2(o.sessionDir, { recursive: true, mode: 448 });
|
|
4536
4578
|
const stored = spec.resume_session_id ? getStored(spec.session_id) : null;
|
|
4537
4579
|
const resumeId = spec.resume_session_id && stored?.hostSessionId === spec.resume_session_id ? spec.resume_session_id : null;
|
|
4538
4580
|
const live = {
|
|
@@ -4629,7 +4671,9 @@ var ClaudeAdapter = class {
|
|
|
4629
4671
|
if (bytes > ARGV_LIMIT_BYTES) {
|
|
4630
4672
|
throw new Error(`The agent's instructions are too long to start Claude Code on this computer (${Math.round(bytes / 1024)} KB of arguments; the limit here is about ${Math.round(ARGV_LIMIT_BYTES / 1024)} KB). Shorten its skills or system instructions.`);
|
|
4631
4673
|
}
|
|
4632
|
-
const
|
|
4674
|
+
const launch = launchOf(this.opts.claudePath, args);
|
|
4675
|
+
if (launch.note) log.warn(launch.note);
|
|
4676
|
+
const child = spawn(launch.command, launch.args, {
|
|
4633
4677
|
cwd: live.sessionDir,
|
|
4634
4678
|
env: cleanEnv(process.env, { ...this.opts.env, ...live.secretEnv }),
|
|
4635
4679
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5101,9 +5145,13 @@ var CodexAdapter = class _CodexAdapter {
|
|
|
5101
5145
|
}
|
|
5102
5146
|
/** The version and whether somebody is signed in — the same shape ClaudeAdapter.check returns. */
|
|
5103
5147
|
static check(codexPath, commands = {}) {
|
|
5104
|
-
const versionCommand = commands.version ?? ((path) =>
|
|
5148
|
+
const versionCommand = commands.version ?? ((path) => {
|
|
5149
|
+
const l = launchOf(path, ["--version"]);
|
|
5150
|
+
return execFileSync3(l.command, l.args, { stdio: ["ignore", "pipe", "ignore"], timeout: 2e4 }).toString();
|
|
5151
|
+
});
|
|
5105
5152
|
const loginStatusCommand = commands.loginStatus ?? ((path) => {
|
|
5106
|
-
const
|
|
5153
|
+
const l = launchOf(path, ["login", "status"]);
|
|
5154
|
+
const res = spawnSync(l.command, l.args, { encoding: "utf8", timeout: 3e4 });
|
|
5107
5155
|
if (res.error) throw res.error;
|
|
5108
5156
|
return `${res.stdout ?? ""}${res.stderr ?? ""}`;
|
|
5109
5157
|
});
|
|
@@ -5232,7 +5280,9 @@ var CodexAdapter = class _CodexAdapter {
|
|
|
5232
5280
|
// The process and the wire
|
|
5233
5281
|
// -------------------------------------------------------------------------
|
|
5234
5282
|
spawnChild(live) {
|
|
5235
|
-
const
|
|
5283
|
+
const launch = launchOf(this.opts.codexPath, ["app-server"]);
|
|
5284
|
+
if (launch.note) log.warn(launch.note);
|
|
5285
|
+
const child = spawn2(launch.command, launch.args, {
|
|
5236
5286
|
cwd: live.sessionDir,
|
|
5237
5287
|
stdio: ["pipe", "pipe", "pipe"],
|
|
5238
5288
|
env: { ...process.env }
|
|
@@ -5506,7 +5556,7 @@ function describeError(err) {
|
|
|
5506
5556
|
|
|
5507
5557
|
// src/adapters/grok.ts
|
|
5508
5558
|
import { spawn as spawn3, spawnSync as spawnSync2, execFileSync as execFileSync4 } from "node:child_process";
|
|
5509
|
-
import { existsSync as
|
|
5559
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
|
|
5510
5560
|
|
|
5511
5561
|
// src/adapters/grok-spec.ts
|
|
5512
5562
|
import { isAbsolute as isAbsolute2, resolve as resolve2, sep as sep2, join as join6 } from "node:path";
|
|
@@ -5833,9 +5883,13 @@ var GrokAdapter = class _GrokAdapter {
|
|
|
5833
5883
|
*/
|
|
5834
5884
|
static check(grokPath, commands = {}) {
|
|
5835
5885
|
const env = { ...process.env, GROK_DISABLE_AUTOUPDATER: "1" };
|
|
5836
|
-
const versionCommand = commands.version ?? ((path) =>
|
|
5886
|
+
const versionCommand = commands.version ?? ((path) => {
|
|
5887
|
+
const l = launchOf(path, ["--version"]);
|
|
5888
|
+
return execFileSync4(l.command, l.args, { env, stdio: ["ignore", "pipe", "ignore"], timeout: 2e4 }).toString();
|
|
5889
|
+
});
|
|
5837
5890
|
const modelsCommand = commands.models ?? ((path) => {
|
|
5838
|
-
const
|
|
5891
|
+
const l = launchOf(path, ["models"]);
|
|
5892
|
+
const res = spawnSync2(l.command, l.args, { env, encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5839
5893
|
if (res.error) throw res.error;
|
|
5840
5894
|
return `${res.stdout ?? ""}
|
|
5841
5895
|
${res.stderr ?? ""}`;
|
|
@@ -5886,7 +5940,7 @@ ${res.stderr ?? ""}`;
|
|
|
5886
5940
|
stderrTail: []
|
|
5887
5941
|
};
|
|
5888
5942
|
this.live.set(spec.session_id, live);
|
|
5889
|
-
if (!
|
|
5943
|
+
if (!existsSync6(o.sessionDir)) mkdirSync4(o.sessionDir, { recursive: true, mode: 448 });
|
|
5890
5944
|
this.spawnChild(live);
|
|
5891
5945
|
let init;
|
|
5892
5946
|
try {
|
|
@@ -6019,7 +6073,9 @@ ${res.stderr ?? ""}`;
|
|
|
6019
6073
|
// The process and the wire
|
|
6020
6074
|
// -------------------------------------------------------------------------
|
|
6021
6075
|
spawnChild(live) {
|
|
6022
|
-
const
|
|
6076
|
+
const launch = launchOf(this.opts.grokPath, grokArgs(live.spec));
|
|
6077
|
+
if (launch.note) log.warn(launch.note);
|
|
6078
|
+
const child = spawn3(launch.command, launch.args, {
|
|
6023
6079
|
cwd: live.sessionDir,
|
|
6024
6080
|
stdio: ["pipe", "pipe", "pipe"],
|
|
6025
6081
|
env: grokEnv(process.env)
|
|
@@ -6228,7 +6284,7 @@ import { hostname, platform as platform2 } from "node:os";
|
|
|
6228
6284
|
|
|
6229
6285
|
// src/repo.ts
|
|
6230
6286
|
import { spawn as spawn4 } from "node:child_process";
|
|
6231
|
-
import { existsSync as
|
|
6287
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync, rmSync, statSync } from "node:fs";
|
|
6232
6288
|
import { join as join7, resolve as resolve3 } from "node:path";
|
|
6233
6289
|
var PLATFORM_COMMIT_ENV = {
|
|
6234
6290
|
GIT_AUTHOR_NAME: "AgentOrchestrator",
|
|
@@ -6329,7 +6385,7 @@ async function canReach(gitUrl, timeoutMs = 3e4) {
|
|
|
6329
6385
|
return { ok: false, detail: firstLine(r.stderr) || `git exited ${r.code}`, ms: r.ms };
|
|
6330
6386
|
}
|
|
6331
6387
|
async function originOf(dir) {
|
|
6332
|
-
if (!
|
|
6388
|
+
if (!existsSync7(join7(dir, ".git"))) return null;
|
|
6333
6389
|
const r = await git(["-C", dir, "remote", "get-url", "origin"], void 0, { timeoutMs: 1e4 });
|
|
6334
6390
|
return r.code === 0 ? r.stdout.trim() : null;
|
|
6335
6391
|
}
|
|
@@ -6354,7 +6410,7 @@ async function prepareCheckout(repo, workdir, computerName, log2, opts = {}) {
|
|
|
6354
6410
|
throw new Error(`${dir} already holds a different repository (${existing}); not touching it`);
|
|
6355
6411
|
}
|
|
6356
6412
|
if (!existing) {
|
|
6357
|
-
if (
|
|
6413
|
+
if (existsSync7(dir)) rmSync(dir, { recursive: true, force: true });
|
|
6358
6414
|
mkdirSync5(resolve3(dir, ".."), { recursive: true, mode: 448 });
|
|
6359
6415
|
const branchToClone = onOrigin ? repo.branch : repo.default_branch;
|
|
6360
6416
|
const clone = await git(["clone", "--quiet", "--branch", branchToClone, "--single-branch", "--", repo.git_url, dir], void 0, { timeoutMs: 10 * 6e4 });
|
|
@@ -6430,7 +6486,7 @@ function pruneRunDirs(workdir, maxAgeMs = 7 * 24 * 60 * 6e4) {
|
|
|
6430
6486
|
let removed = 0;
|
|
6431
6487
|
for (const sub of ["runs"]) {
|
|
6432
6488
|
const root = join7(workdir, sub);
|
|
6433
|
-
if (!
|
|
6489
|
+
if (!existsSync7(root)) continue;
|
|
6434
6490
|
for (const name of readdirSync(root)) {
|
|
6435
6491
|
const p = resolve3(root, name);
|
|
6436
6492
|
try {
|
|
@@ -6446,7 +6502,7 @@ function pruneRunDirs(workdir, maxAgeMs = 7 * 24 * 60 * 6e4) {
|
|
|
6446
6502
|
}
|
|
6447
6503
|
|
|
6448
6504
|
// src/repo-door.ts
|
|
6449
|
-
import { existsSync as
|
|
6505
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
6450
6506
|
var DoorCheckouts = class {
|
|
6451
6507
|
bySession = /* @__PURE__ */ new Map();
|
|
6452
6508
|
get(sessionId, repositoryId) {
|
|
@@ -6525,7 +6581,7 @@ async function handleRepoRequest(req, sessionId, ctx, checkouts) {
|
|
|
6525
6581
|
}
|
|
6526
6582
|
const known = checkouts.get(sessionId, req.repository.id);
|
|
6527
6583
|
const at = known?.dir ?? dir;
|
|
6528
|
-
if (!
|
|
6584
|
+
if (!existsSync8(at)) throw new Error(`${req.repository.name} is not checked out on ${ctx.computerName} for this session \u2014 call checkout_repository first`);
|
|
6529
6585
|
if (req.action === "status") {
|
|
6530
6586
|
const [branch, head, changed] = await Promise.all([
|
|
6531
6587
|
git(["-C", at, "symbolic-ref", "--short", "-q", "HEAD"], void 0, { timeoutMs: 1e4 }),
|
|
@@ -6543,7 +6599,7 @@ async function handleRepoRequest(req, sessionId, ctx, checkouts) {
|
|
|
6543
6599
|
}
|
|
6544
6600
|
|
|
6545
6601
|
// src/workdir.ts
|
|
6546
|
-
import { existsSync as
|
|
6602
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, realpathSync as realpathSync2 } from "node:fs";
|
|
6547
6603
|
import { homedir as homedir4 } from "node:os";
|
|
6548
6604
|
import { dirname as dirname2, isAbsolute as isAbsolute3, relative, resolve as resolve4, sep as sep3 } from "node:path";
|
|
6549
6605
|
function expandHome(p, home = homedir4()) {
|
|
@@ -6581,12 +6637,12 @@ function resolveWorkdir(requested, rule, opts = {}) {
|
|
|
6581
6637
|
const realAnchor = realpathSync2(anchor);
|
|
6582
6638
|
const realTarget = resolve4(realAnchor, relative(anchor, abs));
|
|
6583
6639
|
if (!realRoots.some((root) => under(realTarget, root))) throw new Error(`${refusalWords(rule, home)} (the folder links to a place outside them)`);
|
|
6584
|
-
if (opts.create !== false && !
|
|
6585
|
-
return
|
|
6640
|
+
if (opts.create !== false && !existsSync9(abs)) mkdirSync6(abs, { recursive: true, mode: 448 });
|
|
6641
|
+
return existsSync9(abs) ? realpathSync2(abs) : realTarget;
|
|
6586
6642
|
}
|
|
6587
6643
|
function deepestExisting(abs) {
|
|
6588
6644
|
let cur = abs;
|
|
6589
|
-
while (!
|
|
6645
|
+
while (!existsSync9(cur)) {
|
|
6590
6646
|
const parent = dirname2(cur);
|
|
6591
6647
|
if (parent === cur) return cur;
|
|
6592
6648
|
cur = parent;
|
|
@@ -7076,12 +7132,12 @@ function plainWords(message) {
|
|
|
7076
7132
|
|
|
7077
7133
|
// src/install.ts
|
|
7078
7134
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
7079
|
-
import { existsSync as
|
|
7135
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "node:fs";
|
|
7080
7136
|
import { homedir as homedir5, platform as platform4, userInfo } from "node:os";
|
|
7081
|
-
import { dirname as dirname4, join as join9 } from "node:path";
|
|
7137
|
+
import { dirname as dirname4, join as join9, win32 as winPath2 } from "node:path";
|
|
7082
7138
|
|
|
7083
7139
|
// src/updater.ts
|
|
7084
|
-
import { existsSync as
|
|
7140
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, rmSync as rmSync2, readdirSync as readdirSync2, renameSync, symlinkSync, readFileSync as readFileSync6, lstatSync, realpathSync as realpathSync3, readlinkSync, cpSync, writeFileSync as writeFileSync5, statSync as statSync2 } from "node:fs";
|
|
7085
7141
|
import { join as join8, dirname as dirname3, basename, resolve as resolve5, sep as sep4 } from "node:path";
|
|
7086
7142
|
import { platform as platform3 } from "node:os";
|
|
7087
7143
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
@@ -7192,7 +7248,7 @@ function currentVersion(currentLink = CURRENT_LINK) {
|
|
|
7192
7248
|
}
|
|
7193
7249
|
function packageVersionAt(root) {
|
|
7194
7250
|
try {
|
|
7195
|
-
const pkg = JSON.parse(
|
|
7251
|
+
const pkg = JSON.parse(readFileSync6(join8(root, "package.json"), "utf8"));
|
|
7196
7252
|
return pkg.name && pkg.version ? { name: pkg.name, version: pkg.version } : null;
|
|
7197
7253
|
} catch {
|
|
7198
7254
|
return null;
|
|
@@ -7204,14 +7260,14 @@ function stagePackage(loc, expectedVersion, versionsDir = VERSIONS_DIR) {
|
|
|
7204
7260
|
if (pkg.version !== expectedVersion) return { ok: false, reason: `the package at ${loc.packageRoot} is ${pkg.version}, this bridge is ${expectedVersion}` };
|
|
7205
7261
|
const dest = join8(versionsDir, pkg.version);
|
|
7206
7262
|
if (loc.installedVersion === pkg.version && safeReal(loc.packageRoot) === safeReal(dest)) return { ok: true, dir: dest, copied: false };
|
|
7207
|
-
if (!
|
|
7263
|
+
if (!existsSync10(join8(loc.packageRoot, "dist", "cli.js"))) {
|
|
7208
7264
|
return { ok: false, reason: `${loc.packageRoot} has no dist/cli.js \u2014 run \`npm run build -w @frod.io/bridge\` in the checkout, or run the published package: npx @frod.io/bridge@latest install` };
|
|
7209
7265
|
}
|
|
7210
|
-
if (
|
|
7266
|
+
if (existsSync10(join8(dest, "dist", "cli.js"))) return { ok: true, dir: dest, copied: false };
|
|
7211
7267
|
const partial = `${dest}.partial`;
|
|
7212
7268
|
rmSync2(partial, { recursive: true, force: true });
|
|
7213
7269
|
mkdirSync7(partial, { recursive: true, mode: 493 });
|
|
7214
|
-
for (const f of ["package.json", "README.md"]) if (
|
|
7270
|
+
for (const f of ["package.json", "README.md"]) if (existsSync10(join8(loc.packageRoot, f))) cpSync(join8(loc.packageRoot, f), join8(partial, f));
|
|
7215
7271
|
cpSync(join8(loc.packageRoot, "dist"), join8(partial, "dist"), { recursive: true });
|
|
7216
7272
|
rmSync2(dest, { recursive: true, force: true });
|
|
7217
7273
|
renameSync(partial, dest);
|
|
@@ -7271,7 +7327,7 @@ async function stageRelease(release, opts = {}) {
|
|
|
7271
7327
|
const pkg = packageVersionAt(root);
|
|
7272
7328
|
if (!pkg || pkg.name !== "@frod.io/bridge") return refuse("the tarball is not the @frod.io/bridge package");
|
|
7273
7329
|
if (pkg.version !== v) return refuse(`the tarball says it is ${pkg.version}, not ${v}`);
|
|
7274
|
-
if (!
|
|
7330
|
+
if (!existsSync10(join8(root, "dist", "cli.js"))) return refuse("the tarball has no dist/cli.js");
|
|
7275
7331
|
try {
|
|
7276
7332
|
rmSync2(dest, { recursive: true, force: true });
|
|
7277
7333
|
renameSync(root, dest);
|
|
@@ -7283,7 +7339,7 @@ async function stageRelease(release, opts = {}) {
|
|
|
7283
7339
|
}
|
|
7284
7340
|
function swapCurrent(version, versionsDir = VERSIONS_DIR, currentLink = CURRENT_LINK) {
|
|
7285
7341
|
const target = join8(versionsDir, version);
|
|
7286
|
-
if (!
|
|
7342
|
+
if (!existsSync10(join8(target, "dist", "cli.js"))) throw new Error(`${target} has no dist/cli.js \u2014 not swapping`);
|
|
7287
7343
|
mkdirSync7(dirname3(currentLink), { recursive: true });
|
|
7288
7344
|
const tmp = `${currentLink}.tmp-${process.pid}`;
|
|
7289
7345
|
rmSync2(tmp, { recursive: true, force: true });
|
|
@@ -7298,7 +7354,7 @@ function swapCurrent(version, versionsDir = VERSIONS_DIR, currentLink = CURRENT_
|
|
|
7298
7354
|
}
|
|
7299
7355
|
function pruneVersions(opts = {}) {
|
|
7300
7356
|
const versionsDir = opts.versionsDir ?? VERSIONS_DIR;
|
|
7301
|
-
if (!
|
|
7357
|
+
if (!existsSync10(versionsDir)) return [];
|
|
7302
7358
|
const keepSet = new Set([opts.running ?? "", currentVersion(opts.currentLink ?? CURRENT_LINK) ?? ""].filter(Boolean));
|
|
7303
7359
|
const entries = readdirSync2(versionsDir);
|
|
7304
7360
|
const versions = entries.filter((e) => SEMVER.test(e)).sort((a, b) => -(compareSemver(a, b) ?? 0));
|
|
@@ -7460,17 +7516,22 @@ function xml(s) {
|
|
|
7460
7516
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7461
7517
|
}
|
|
7462
7518
|
function defaultTarget() {
|
|
7463
|
-
return { node: process.execPath, cli: join9(CURRENT_LINK, "dist", "cli.js"), workdir: BRIDGE_DIR, home: homedir5(), frodDir: FROD_DIR, logPath: LOG_PATH };
|
|
7519
|
+
return { node: process.execPath, cli: join9(CURRENT_LINK, "dist", "cli.js"), workdir: BRIDGE_DIR, home: homedir5(), frodDir: FROD_DIR, logPath: LOG_PATH, user: windowsUser() };
|
|
7520
|
+
}
|
|
7521
|
+
function windowsUser(env = process.env) {
|
|
7522
|
+
const name = env.USERNAME ?? userInfo().username;
|
|
7523
|
+
const domain = env.USERDOMAIN;
|
|
7524
|
+
return domain ? `${domain}\\${name}` : name;
|
|
7464
7525
|
}
|
|
7465
7526
|
var realIo = {
|
|
7466
7527
|
exec: (cmd, args) => {
|
|
7467
7528
|
execFileSync6(cmd, args, { stdio: "pipe" });
|
|
7468
7529
|
},
|
|
7469
|
-
writeFile: (path, content, mode) => {
|
|
7470
|
-
writeFileSync6(path, content, { encoding
|
|
7530
|
+
writeFile: (path, content, mode, encoding = "utf8") => {
|
|
7531
|
+
writeFileSync6(path, content, { encoding, mode });
|
|
7471
7532
|
},
|
|
7472
7533
|
mkdir: (dir) => {
|
|
7473
|
-
if (!
|
|
7534
|
+
if (!existsSync11(dir)) mkdirSync8(dir, { recursive: true });
|
|
7474
7535
|
}
|
|
7475
7536
|
};
|
|
7476
7537
|
function renderLaunchdPlist(t) {
|
|
@@ -7544,7 +7605,7 @@ function uninstallMacOS() {
|
|
|
7544
7605
|
execFileSync6("launchctl", ["bootout", `gui/${uid}/${LAUNCHD_LABEL}`], { stdio: "ignore" });
|
|
7545
7606
|
} catch {
|
|
7546
7607
|
}
|
|
7547
|
-
if (
|
|
7608
|
+
if (existsSync11(plistPath())) {
|
|
7548
7609
|
unlinkSync2(plistPath());
|
|
7549
7610
|
return { removed: true };
|
|
7550
7611
|
}
|
|
@@ -7621,12 +7682,96 @@ function uninstallLinux(t = defaultTarget(), io = realIo) {
|
|
|
7621
7682
|
} catch (err) {
|
|
7622
7683
|
detail = `systemctl: ${err instanceof Error ? err.message : String(err)}`;
|
|
7623
7684
|
}
|
|
7624
|
-
if (
|
|
7685
|
+
if (existsSync11(path)) {
|
|
7625
7686
|
unlinkSync2(path);
|
|
7626
7687
|
return { removed: true, detail };
|
|
7627
7688
|
}
|
|
7628
7689
|
return { removed: false, detail };
|
|
7629
7690
|
}
|
|
7691
|
+
function hiddenLauncherPath(t) {
|
|
7692
|
+
return winPath2.join(t.frodDir, "bridge-hidden.vbs");
|
|
7693
|
+
}
|
|
7694
|
+
function windowsTaskXmlPath(t) {
|
|
7695
|
+
return winPath2.join(t.frodDir, "bridge-task.xml");
|
|
7696
|
+
}
|
|
7697
|
+
function vbString(s) {
|
|
7698
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
7699
|
+
}
|
|
7700
|
+
function renderHiddenLauncher(t) {
|
|
7701
|
+
const command = `cmd /c "${'"'}${t.node}${'"'} ${'"'}${t.cli}${'"'} run >> ${'"'}${t.logPath}${'"'} 2>&1"`;
|
|
7702
|
+
return [
|
|
7703
|
+
`' Frod.io bridge \u2014 started at logon by the scheduled task "` + WINDOWS_TASK + '".',
|
|
7704
|
+
"' No window: the bridge is not something a person can close by accident.",
|
|
7705
|
+
"Dim command",
|
|
7706
|
+
`command = ${vbString(command)}`,
|
|
7707
|
+
'CreateObject("WScript.Shell").Run command, 0, False',
|
|
7708
|
+
""
|
|
7709
|
+
].join("\r\n");
|
|
7710
|
+
}
|
|
7711
|
+
function system32(name, env) {
|
|
7712
|
+
return winPath2.join(env.SystemRoot ?? "C:\\Windows", "System32", name);
|
|
7713
|
+
}
|
|
7714
|
+
function windowsAction(t, env = process.env) {
|
|
7715
|
+
return { command: system32("wscript.exe", env), args: `//B //NoLogo "${hiddenLauncherPath(t)}"` };
|
|
7716
|
+
}
|
|
7717
|
+
function renderWindowsTaskXml(t, env = process.env) {
|
|
7718
|
+
const action = windowsAction(t, env);
|
|
7719
|
+
const user = t.user ?? "BUILTIN\\Users";
|
|
7720
|
+
return `\uFEFF<?xml version="1.0" encoding="UTF-16"?>
|
|
7721
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
7722
|
+
<RegistrationInfo>
|
|
7723
|
+
<Description>${xml("Frod.io bridge \u2014 hosts your agents' sessions on this computer. Starts at logon, runs with no window.")}</Description>
|
|
7724
|
+
<URI>\\${xml(WINDOWS_TASK)}</URI>
|
|
7725
|
+
</RegistrationInfo>
|
|
7726
|
+
<Triggers>
|
|
7727
|
+
<LogonTrigger>
|
|
7728
|
+
<Enabled>true</Enabled>
|
|
7729
|
+
<UserId>${xml(user)}</UserId>
|
|
7730
|
+
</LogonTrigger>
|
|
7731
|
+
</Triggers>
|
|
7732
|
+
<Principals>
|
|
7733
|
+
<Principal id="Author">
|
|
7734
|
+
<UserId>${xml(user)}</UserId>
|
|
7735
|
+
<LogonType>InteractiveToken</LogonType>
|
|
7736
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
7737
|
+
</Principal>
|
|
7738
|
+
</Principals>
|
|
7739
|
+
<Settings>
|
|
7740
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
7741
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
7742
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
7743
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
7744
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
7745
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
7746
|
+
<IdleSettings>
|
|
7747
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
7748
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
7749
|
+
</IdleSettings>
|
|
7750
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
7751
|
+
<Enabled>true</Enabled>
|
|
7752
|
+
<Hidden>true</Hidden>
|
|
7753
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
7754
|
+
<WakeToRun>false</WakeToRun>
|
|
7755
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
7756
|
+
<Priority>7</Priority>
|
|
7757
|
+
<RestartOnFailure>
|
|
7758
|
+
<Interval>PT1M</Interval>
|
|
7759
|
+
<Count>3</Count>
|
|
7760
|
+
</RestartOnFailure>
|
|
7761
|
+
</Settings>
|
|
7762
|
+
<Actions Context="Author">
|
|
7763
|
+
<Exec>
|
|
7764
|
+
<Command>${xml(action.command)}</Command>
|
|
7765
|
+
<Arguments>${xml(action.args)}</Arguments>
|
|
7766
|
+
<WorkingDirectory>${xml(t.workdir)}</WorkingDirectory>
|
|
7767
|
+
</Exec>
|
|
7768
|
+
</Actions>
|
|
7769
|
+
</Task>
|
|
7770
|
+
`;
|
|
7771
|
+
}
|
|
7772
|
+
function renderSchtasksXmlArgs(t) {
|
|
7773
|
+
return ["/Create", "/F", "/TN", WINDOWS_TASK, "/XML", windowsTaskXmlPath(t)];
|
|
7774
|
+
}
|
|
7630
7775
|
function renderSchtasksArgs(t) {
|
|
7631
7776
|
const tr = `"${t.node}" "${t.cli}" run`;
|
|
7632
7777
|
return ["/Create", "/F", "/TN", WINDOWS_TASK, "/SC", "ONLOGON", "/RL", "LIMITED", "/TR", tr];
|
|
@@ -7634,26 +7779,60 @@ function renderSchtasksArgs(t) {
|
|
|
7634
7779
|
function windowsManualPath(t) {
|
|
7635
7780
|
return [
|
|
7636
7781
|
"To run it at logon, in a command prompt:",
|
|
7637
|
-
` schtasks
|
|
7782
|
+
` schtasks /Create /F /TN "${WINDOWS_TASK}" /XML "${windowsTaskXmlPath(t)}"`,
|
|
7638
7783
|
` schtasks /Run /TN "${WINDOWS_TASK}"`,
|
|
7639
|
-
`
|
|
7640
|
-
`
|
|
7784
|
+
`The task definition and the launcher it runs are written to ${windowsTaskXmlPath(t)} and ${hiddenLauncherPath(t)} whatever happens, so you can register them by hand or open Task Scheduler and import the XML.`,
|
|
7785
|
+
`Without that definition the plain form still works \u2014 schtasks ${renderSchtasksArgs(t).map((a) => /\s/.test(a) && !a.startsWith('"') ? `"${a}"` : a).join(" ")} \u2014 but it shows a console window that ends the bridge when closed, it has no restart on failure, and Windows stops it after 72 hours or when the machine goes on battery.`,
|
|
7786
|
+
`Logs: ${t.logPath}, written by the launcher. A self-update relaunches the bridge itself; if that fails, the next logon starts it.`
|
|
7641
7787
|
].join("\n");
|
|
7642
7788
|
}
|
|
7643
|
-
function installWindows(t = defaultTarget(), io = realIo) {
|
|
7789
|
+
function installWindows(t = defaultTarget(), io = realIo, env = process.env) {
|
|
7644
7790
|
const manual = windowsManualPath(t);
|
|
7791
|
+
io.mkdir(t.frodDir);
|
|
7792
|
+
let wrote = true;
|
|
7793
|
+
try {
|
|
7794
|
+
io.writeFile(hiddenLauncherPath(t), renderHiddenLauncher(t), 420);
|
|
7795
|
+
io.writeFile(windowsTaskXmlPath(t), renderWindowsTaskXml(t, env), 420, "utf16le");
|
|
7796
|
+
} catch {
|
|
7797
|
+
wrote = false;
|
|
7798
|
+
}
|
|
7799
|
+
if (wrote) {
|
|
7800
|
+
try {
|
|
7801
|
+
io.exec("schtasks", renderSchtasksXmlArgs(t));
|
|
7802
|
+
try {
|
|
7803
|
+
io.exec("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
7804
|
+
} catch {
|
|
7805
|
+
}
|
|
7806
|
+
return { loaded: true, hidden: true, detail: `scheduled task "${WINDOWS_TASK}" created (at logon, no window, restarts after a crash)`, manual };
|
|
7807
|
+
} catch {
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7645
7810
|
try {
|
|
7646
7811
|
io.exec("schtasks", renderSchtasksArgs(t));
|
|
7647
7812
|
try {
|
|
7648
7813
|
io.exec("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
|
|
7649
7814
|
} catch {
|
|
7650
7815
|
}
|
|
7651
|
-
return { loaded: true, detail: `scheduled task "${WINDOWS_TASK}" created (at logon)`, manual };
|
|
7816
|
+
return { loaded: true, hidden: false, detail: `scheduled task "${WINDOWS_TASK}" created (at logon) \u2014 the task definition was refused, so it runs in a console window`, manual };
|
|
7652
7817
|
} catch (err) {
|
|
7653
|
-
return { loaded: false, detail: `schtasks refused: ${err instanceof Error ? err.message : String(err)}`, manual };
|
|
7818
|
+
return { loaded: false, hidden: false, detail: `schtasks refused: ${err instanceof Error ? err.message : String(err)}`, manual };
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
function windowsTaskLoaded(io = realIo) {
|
|
7822
|
+
try {
|
|
7823
|
+
io.exec("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
|
|
7824
|
+
return true;
|
|
7825
|
+
} catch {
|
|
7826
|
+
return false;
|
|
7654
7827
|
}
|
|
7655
7828
|
}
|
|
7656
|
-
function uninstallWindows(io = realIo) {
|
|
7829
|
+
function uninstallWindows(io = realIo, t = defaultTarget()) {
|
|
7830
|
+
for (const path of [hiddenLauncherPath(t), windowsTaskXmlPath(t)]) {
|
|
7831
|
+
try {
|
|
7832
|
+
if (existsSync11(path)) unlinkSync2(path);
|
|
7833
|
+
} catch {
|
|
7834
|
+
}
|
|
7835
|
+
}
|
|
7657
7836
|
try {
|
|
7658
7837
|
io.exec("schtasks", ["/Delete", "/F", "/TN", WINDOWS_TASK]);
|
|
7659
7838
|
return { removed: true, detail: "task deleted" };
|
|
@@ -7665,11 +7844,84 @@ function installStub(t = defaultTarget()) {
|
|
|
7665
7844
|
return `Automatic install is not available on ${platform4()}. Run: node "${t.cli}" run`;
|
|
7666
7845
|
}
|
|
7667
7846
|
|
|
7847
|
+
// src/runtime-paths.ts
|
|
7848
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
7849
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
7850
|
+
import { homedir as homedir6, platform as osPlatform } from "node:os";
|
|
7851
|
+
import { join as join10, win32 as winPath3 } from "node:path";
|
|
7852
|
+
var ENV_OVERRIDE = {
|
|
7853
|
+
claude: "FROD_CLAUDE_PATH",
|
|
7854
|
+
codex: "FROD_CODEX_PATH",
|
|
7855
|
+
grok: "FROD_GROK_PATH"
|
|
7856
|
+
};
|
|
7857
|
+
var WINDOWS_EXTENSIONS = [".exe", ".cmd", ".bat"];
|
|
7858
|
+
function posixCandidates(name, home) {
|
|
7859
|
+
const dirs = [join10(home, ".local", "bin"), "/opt/homebrew/bin", "/usr/local/bin"];
|
|
7860
|
+
if (name === "grok") dirs.splice(1, 0, join10(home, ".grok", "bin"));
|
|
7861
|
+
return dirs.map((d) => join10(d, name));
|
|
7862
|
+
}
|
|
7863
|
+
function windowsCandidates(name, home, env) {
|
|
7864
|
+
const appData = env.APPDATA ?? winPath3.join(home, "AppData", "Roaming");
|
|
7865
|
+
const localAppData = env.LOCALAPPDATA ?? winPath3.join(home, "AppData", "Local");
|
|
7866
|
+
const dirs = [
|
|
7867
|
+
winPath3.join(appData, "npm"),
|
|
7868
|
+
winPath3.join(localAppData, "npm"),
|
|
7869
|
+
winPath3.join(home, ".local", "bin"),
|
|
7870
|
+
winPath3.join(home, `.${name}`, "bin"),
|
|
7871
|
+
winPath3.join(localAppData, "Programs", name),
|
|
7872
|
+
winPath3.join(localAppData, name, "bin")
|
|
7873
|
+
];
|
|
7874
|
+
const out = [];
|
|
7875
|
+
for (const dir of dirs) for (const ext of WINDOWS_EXTENSIONS) out.push(winPath3.join(dir, `${name}${ext}`));
|
|
7876
|
+
return out;
|
|
7877
|
+
}
|
|
7878
|
+
function findOnWindowsPath(command, env = process.env) {
|
|
7879
|
+
const whereExe = winPath3.join(env.SystemRoot ?? "C:\\Windows", "System32", "where.exe");
|
|
7880
|
+
for (const tool of [whereExe, "where.exe"]) {
|
|
7881
|
+
try {
|
|
7882
|
+
const lines = execFileSync7(tool, [command], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }).toString().split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
7883
|
+
if (lines.length === 0) return null;
|
|
7884
|
+
for (const ext of WINDOWS_EXTENSIONS) {
|
|
7885
|
+
const hit = lines.find((l) => l.toLowerCase().endsWith(ext));
|
|
7886
|
+
if (hit) return hit;
|
|
7887
|
+
}
|
|
7888
|
+
return lines[0];
|
|
7889
|
+
} catch {
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
return null;
|
|
7893
|
+
}
|
|
7894
|
+
function findOnPosixPath(command) {
|
|
7895
|
+
try {
|
|
7896
|
+
const found = execFileSync7("/usr/bin/which", [command], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
7897
|
+
return found || null;
|
|
7898
|
+
} catch {
|
|
7899
|
+
return null;
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
function defaultLookup() {
|
|
7903
|
+
const platform7 = osPlatform();
|
|
7904
|
+
return {
|
|
7905
|
+
platform: platform7,
|
|
7906
|
+
env: process.env,
|
|
7907
|
+
home: homedir6(),
|
|
7908
|
+
exists: existsSync12,
|
|
7909
|
+
onPath: (command) => platform7 === "win32" ? findOnWindowsPath(command, process.env) : findOnPosixPath(command)
|
|
7910
|
+
};
|
|
7911
|
+
}
|
|
7912
|
+
function findRuntime(name, lookup = defaultLookup()) {
|
|
7913
|
+
const override = lookup.env[ENV_OVERRIDE[name]];
|
|
7914
|
+
if (override && lookup.exists(override)) return override;
|
|
7915
|
+
const candidates = lookup.platform === "win32" ? windowsCandidates(name, lookup.home, lookup.env) : posixCandidates(name, lookup.home);
|
|
7916
|
+
for (const c of candidates) if (lookup.exists(c)) return c;
|
|
7917
|
+
return lookup.onPath(name);
|
|
7918
|
+
}
|
|
7919
|
+
|
|
7668
7920
|
// src/capacity.ts
|
|
7669
7921
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
7670
|
-
import { existsSync as
|
|
7671
|
-
import { homedir as
|
|
7672
|
-
import { join as
|
|
7922
|
+
import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync7 } from "node:fs";
|
|
7923
|
+
import { homedir as homedir7, platform as platform5, tmpdir as tmpdir2, userInfo as userInfo2 } from "node:os";
|
|
7924
|
+
import { join as join11 } from "node:path";
|
|
7673
7925
|
import { createInterface } from "node:readline";
|
|
7674
7926
|
var SAMPLE_INTERVAL_MS = 30 * 6e4;
|
|
7675
7927
|
var READER_TIMEOUT_MS = 25e3;
|
|
@@ -7679,9 +7931,27 @@ var CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
|
7679
7931
|
var CLAUDE_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
7680
7932
|
var CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
|
|
7681
7933
|
var APPROVAL_WORDS = "Needs a one-time approval on this computer: allow the bridge to read Claude Code's sign-in in the Keychain";
|
|
7934
|
+
var SECURITY_ITEM_NOT_FOUND = 44;
|
|
7935
|
+
function securityRunner(timeoutMs) {
|
|
7936
|
+
return (args) => new Promise((resolve7) => {
|
|
7937
|
+
execFile("security", args, { timeout: timeoutMs, maxBuffer: 1 << 20 }, (err, stdout) => {
|
|
7938
|
+
if (err) {
|
|
7939
|
+
const e = err;
|
|
7940
|
+
resolve7({ ok: false, code: typeof e.code === "number" ? e.code : null, killed: e.killed === true || e.signal === "SIGTERM" });
|
|
7941
|
+
return;
|
|
7942
|
+
}
|
|
7943
|
+
resolve7({ ok: true, stdout: String(stdout) });
|
|
7944
|
+
});
|
|
7945
|
+
});
|
|
7946
|
+
}
|
|
7947
|
+
async function keychainAccountOf(run) {
|
|
7948
|
+
const meta = await run(["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE]);
|
|
7949
|
+
if (!meta.ok) return null;
|
|
7950
|
+
return /"acct"<blob>="([^"]*)"/.exec(meta.stdout)?.[1] ?? null;
|
|
7951
|
+
}
|
|
7682
7952
|
function readClaudeCredential(deps = {}) {
|
|
7683
7953
|
const os = deps.platform ?? platform5();
|
|
7684
|
-
const home = deps.home ??
|
|
7954
|
+
const home = deps.home ?? homedir7();
|
|
7685
7955
|
const parse = (raw) => {
|
|
7686
7956
|
try {
|
|
7687
7957
|
const j = JSON.parse(raw);
|
|
@@ -7693,25 +7963,34 @@ function readClaudeCredential(deps = {}) {
|
|
|
7693
7963
|
}
|
|
7694
7964
|
};
|
|
7695
7965
|
if (os === "darwin") {
|
|
7696
|
-
return
|
|
7697
|
-
execFile("security", ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"], { timeout: deps.timeoutMs ?? KEYCHAIN_TIMEOUT_MS, maxBuffer: 1 << 20 }, (err, stdout) => {
|
|
7698
|
-
if (err) {
|
|
7699
|
-
const killed = err.killed || err.signal === "SIGTERM";
|
|
7700
|
-
resolve7({ problem: killed ? APPROVAL_WORDS : "Claude Code is not signed in on this computer" });
|
|
7701
|
-
return;
|
|
7702
|
-
}
|
|
7703
|
-
resolve7(parse(String(stdout).trim()));
|
|
7704
|
-
});
|
|
7705
|
-
});
|
|
7966
|
+
return readClaudeKeychain(deps.security ?? securityRunner(deps.timeoutMs ?? KEYCHAIN_TIMEOUT_MS), deps.account ?? userInfo2().username, parse);
|
|
7706
7967
|
}
|
|
7707
|
-
const file =
|
|
7708
|
-
if (!
|
|
7968
|
+
const file = join11(home, ".claude", ".credentials.json");
|
|
7969
|
+
if (!existsSync13(file)) return Promise.resolve({ problem: "Claude Code is not signed in on this computer" });
|
|
7709
7970
|
try {
|
|
7710
|
-
return Promise.resolve(parse(
|
|
7971
|
+
return Promise.resolve(parse(readFileSync7(file, "utf8")));
|
|
7711
7972
|
} catch {
|
|
7712
7973
|
return Promise.resolve({ problem: "Claude Code's sign-in could not be read on this computer" });
|
|
7713
7974
|
}
|
|
7714
7975
|
}
|
|
7976
|
+
async function readClaudeKeychain(run, account, parse) {
|
|
7977
|
+
let answer = await run(["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-a", account, "-w"]);
|
|
7978
|
+
let scoped = true;
|
|
7979
|
+
if (!answer.ok && !answer.killed && answer.code === SECURITY_ITEM_NOT_FOUND) {
|
|
7980
|
+
answer = await run(["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"]);
|
|
7981
|
+
scoped = false;
|
|
7982
|
+
}
|
|
7983
|
+
if (!answer.ok) {
|
|
7984
|
+
if (answer.killed) return { problem: APPROVAL_WORDS };
|
|
7985
|
+
if (answer.code === SECURITY_ITEM_NOT_FOUND) return { problem: "Claude Code is not signed in on this computer" };
|
|
7986
|
+
return { problem: `Claude Code's sign-in could not be read from the Keychain (security exit ${answer.code ?? "unknown"}) \u2014 it may need a one-time approval on this computer` };
|
|
7987
|
+
}
|
|
7988
|
+
const cred = parse(answer.stdout.trim());
|
|
7989
|
+
if (!("problem" in cred) || scoped) return cred;
|
|
7990
|
+
const other = await keychainAccountOf(run);
|
|
7991
|
+
if (!other || other === account) return cred;
|
|
7992
|
+
return { problem: `The Keychain's Claude Code entry on this computer belongs to "${other}", not to ${account}, and carries no sign-in \u2014 remove that entry if it is not yours` };
|
|
7993
|
+
}
|
|
7715
7994
|
function claudeTierWords(profile) {
|
|
7716
7995
|
const org = profile?.organization;
|
|
7717
7996
|
const tier = org?.rate_limit_tier ?? "";
|
|
@@ -7846,7 +8125,7 @@ function parseGrokBilling(result) {
|
|
|
7846
8125
|
}
|
|
7847
8126
|
async function readGrokCapacity(grokPath, deps = {}) {
|
|
7848
8127
|
const ask = deps.ask ?? askChild;
|
|
7849
|
-
const cwd = deps.cwd ?? mkdtempSync(
|
|
8128
|
+
const cwd = deps.cwd ?? mkdtempSync(join11(tmpdir2(), "frod-capacity-"));
|
|
7850
8129
|
const r = await ask({
|
|
7851
8130
|
command: grokPath,
|
|
7852
8131
|
args: ["agent", "--no-leader", "stdio"],
|
|
@@ -7950,10 +8229,56 @@ function samplesInWords(samples) {
|
|
|
7950
8229
|
return samples.map((s) => `${label[s.provider]} ${s.usedPct === null ? `(${s.words ?? "no number"})` : `${Math.round(s.usedPct)}% of ${win[s.window]}`}`).join(" \xB7 ") || "nothing to report";
|
|
7951
8230
|
}
|
|
7952
8231
|
|
|
8232
|
+
// src/session-cap.ts
|
|
8233
|
+
import { totalmem } from "node:os";
|
|
8234
|
+
var SESSION_RAM_MB = 200;
|
|
8235
|
+
var RESERVED_RAM_MB = 4 * 1024;
|
|
8236
|
+
var HARD_MAX_SESSIONS = 100;
|
|
8237
|
+
var DEFAULT_MAX_SESSIONS = 30;
|
|
8238
|
+
var LEGACY_DEFAULT_MAX_SESSIONS = 2;
|
|
8239
|
+
function sessionCeiling(totalMemBytes = totalmem()) {
|
|
8240
|
+
const usableMb = Math.floor(totalMemBytes / (1024 * 1024)) - RESERVED_RAM_MB;
|
|
8241
|
+
return Math.max(1, Math.min(HARD_MAX_SESSIONS, Math.floor(usableMb / SESSION_RAM_MB)));
|
|
8242
|
+
}
|
|
8243
|
+
function defaultMaxSessions(totalMemBytes) {
|
|
8244
|
+
return Math.min(DEFAULT_MAX_SESSIONS, sessionCeiling(totalMemBytes));
|
|
8245
|
+
}
|
|
8246
|
+
function resolveMaxSessions(requested, totalMemBytes) {
|
|
8247
|
+
const ceiling = sessionCeiling(totalMemBytes);
|
|
8248
|
+
if (requested === null || requested === void 0 || !Number.isFinite(requested)) {
|
|
8249
|
+
return { value: defaultMaxSessions(totalMemBytes), ceiling, clamped: false };
|
|
8250
|
+
}
|
|
8251
|
+
const asked = Math.max(1, Math.floor(requested));
|
|
8252
|
+
return { value: Math.min(asked, ceiling), ceiling, clamped: asked > ceiling };
|
|
8253
|
+
}
|
|
8254
|
+
function clampWords(asked, cap) {
|
|
8255
|
+
if (!cap.clamped) return null;
|
|
8256
|
+
return `${asked} sessions were asked for; this computer's memory carries ${cap.ceiling} at ${SESSION_RAM_MB} MB a session. Using ${cap.value}.`;
|
|
8257
|
+
}
|
|
8258
|
+
function applySessionCap(cfg, totalMemBytes) {
|
|
8259
|
+
const words = [];
|
|
8260
|
+
let write2 = false;
|
|
8261
|
+
if (cfg.maxSessionsChosen === void 0 && cfg.maxSessions <= LEGACY_DEFAULT_MAX_SESSIONS) {
|
|
8262
|
+
const raised = defaultMaxSessions(totalMemBytes);
|
|
8263
|
+
if (raised > cfg.maxSessions) {
|
|
8264
|
+
words.push(`This computer offered ${cfg.maxSessions} sessions at a time \u2014 the old default. Raising it to ${raised}, which its memory carries at ${SESSION_RAM_MB} MB a session. Set "maxSessions" in the settings file to choose your own; this happens once.`);
|
|
8265
|
+
cfg.maxSessions = raised;
|
|
8266
|
+
}
|
|
8267
|
+
cfg.maxSessionsChosen = false;
|
|
8268
|
+
write2 = true;
|
|
8269
|
+
}
|
|
8270
|
+
const cap = resolveMaxSessions(cfg.maxSessions, totalMemBytes);
|
|
8271
|
+
if (cap.clamped) {
|
|
8272
|
+
words.push(`The settings file asks for ${cfg.maxSessions} sessions; this computer's memory carries ${cap.ceiling} at ${SESSION_RAM_MB} MB a session. Using ${cap.value} \u2014 the file is left as it is.`);
|
|
8273
|
+
cfg.maxSessions = cap.value;
|
|
8274
|
+
}
|
|
8275
|
+
return { write: write2, words };
|
|
8276
|
+
}
|
|
8277
|
+
|
|
7953
8278
|
// package.json
|
|
7954
8279
|
var package_default = {
|
|
7955
8280
|
name: "@frod.io/bridge",
|
|
7956
|
-
version: "0.7.
|
|
8281
|
+
version: "0.7.6",
|
|
7957
8282
|
type: "module",
|
|
7958
8283
|
description: "Your computer is a desk: hosts a Frod.io agent's sessions with your own Claude Code, Codex or Grok Build. Installs and updates itself.",
|
|
7959
8284
|
license: "UNLICENSED",
|
|
@@ -8005,33 +8330,13 @@ function arg(flag, argv) {
|
|
|
8005
8330
|
return i >= 0 ? argv[i + 1] : void 0;
|
|
8006
8331
|
}
|
|
8007
8332
|
function findClaude() {
|
|
8008
|
-
|
|
8009
|
-
for (const c of candidates) if (existsSync12(c)) return c;
|
|
8010
|
-
try {
|
|
8011
|
-
return execFileSync7("/usr/bin/which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
8012
|
-
} catch {
|
|
8013
|
-
}
|
|
8014
|
-
return null;
|
|
8333
|
+
return findRuntime("claude");
|
|
8015
8334
|
}
|
|
8016
8335
|
function findCodex() {
|
|
8017
|
-
|
|
8018
|
-
for (const c of candidates) if (existsSync12(c)) return c;
|
|
8019
|
-
try {
|
|
8020
|
-
const found = execFileSync7("/usr/bin/which", ["codex"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
8021
|
-
return found || null;
|
|
8022
|
-
} catch {
|
|
8023
|
-
return null;
|
|
8024
|
-
}
|
|
8336
|
+
return findRuntime("codex");
|
|
8025
8337
|
}
|
|
8026
8338
|
function findGrok() {
|
|
8027
|
-
|
|
8028
|
-
for (const c of candidates) if (existsSync12(c)) return c;
|
|
8029
|
-
try {
|
|
8030
|
-
const found = execFileSync7("/usr/bin/which", ["grok"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
8031
|
-
return found || null;
|
|
8032
|
-
} catch {
|
|
8033
|
-
return null;
|
|
8034
|
-
}
|
|
8339
|
+
return findRuntime("grok");
|
|
8035
8340
|
}
|
|
8036
8341
|
var RUNTIME_ORDER = ["claude", "codex", "grok"];
|
|
8037
8342
|
var RUNTIME_LABELS = { claude: "Claude Code", codex: "Codex", grok: "Grok Build" };
|
|
@@ -8101,7 +8406,7 @@ async function cmdJoin(argv, overrides = {}) {
|
|
|
8101
8406
|
join: join3,
|
|
8102
8407
|
storeToken,
|
|
8103
8408
|
ensureWorkdir: (path) => {
|
|
8104
|
-
if (!
|
|
8409
|
+
if (!existsSync14(path)) mkdirSync9(path, { recursive: true, mode: 448 });
|
|
8105
8410
|
},
|
|
8106
8411
|
writeConfig,
|
|
8107
8412
|
reportGitAccess,
|
|
@@ -8117,8 +8422,10 @@ async function cmdJoin(argv, overrides = {}) {
|
|
|
8117
8422
|
if (refusal) throw new Error(refusal);
|
|
8118
8423
|
const safeApi = assertSafeEndpoint(apiUrl);
|
|
8119
8424
|
const name = arg("--name", argv) ?? defaultName();
|
|
8120
|
-
const workdir = resolve6(arg("--workdir", argv) ??
|
|
8121
|
-
const
|
|
8425
|
+
const workdir = resolve6(arg("--workdir", argv) ?? join12(homedir8(), "frod-work"));
|
|
8426
|
+
const askedSessions = arg("--max-sessions", argv);
|
|
8427
|
+
const cap = resolveMaxSessions(askedSessions === void 0 ? null : Number(askedSessions));
|
|
8428
|
+
const maxSessions = cap.value;
|
|
8122
8429
|
const probes = probeAllRuntimes(deps.probes);
|
|
8123
8430
|
for (const runtime of RUNTIME_ORDER) {
|
|
8124
8431
|
const probe = probes[runtime];
|
|
@@ -8139,6 +8446,7 @@ async function cmdJoin(argv, overrides = {}) {
|
|
|
8139
8446
|
name,
|
|
8140
8447
|
workdir,
|
|
8141
8448
|
maxSessions,
|
|
8449
|
+
maxSessionsChosen: askedSessions !== void 0,
|
|
8142
8450
|
adapters,
|
|
8143
8451
|
claudePath: isReady(probes.claude) ? probes.claude.path : null,
|
|
8144
8452
|
codexPath: isReady(probes.codex) ? probes.codex.path : void 0,
|
|
@@ -8153,7 +8461,9 @@ async function cmdJoin(argv, overrides = {}) {
|
|
|
8153
8461
|
log.info(`Joined. This computer is now "${name}" \u2014 ${joined.person?.name ?? "your"} computer in ${joined.workspaceName ?? joined.workspaceId} \u2014 hosting ${hostedAgentsWords(cfg)}.`);
|
|
8154
8462
|
for (const b of cfg.bindings ?? []) log.info(` ${b.agentName}: ${bindingWords(b)}`);
|
|
8155
8463
|
log.info(`Token kept in ${store === "keychain" ? "the OS keychain" : `a 0600 file under ${dirname5(CONFIG_PATH)} (no keychain available on this computer \u2014 stated, not hidden)`}; it expires ${joined.expiresAt.slice(0, 10)}.`);
|
|
8156
|
-
|
|
8464
|
+
const clamped = askedSessions === void 0 ? null : clampWords(Math.max(1, Math.floor(Number(askedSessions))), cap);
|
|
8465
|
+
if (clamped) log.info(clamped);
|
|
8466
|
+
log.info(`Settings: ${CONFIG_PATH}. Sessions work under ${workdir}/sessions. Up to ${maxSessions} at a time, across every agent this computer hosts \u2014 this computer's memory carries ${cap.ceiling} at ${SESSION_RAM_MB} MB a session; change "maxSessions" in the settings file.`);
|
|
8157
8467
|
log.info(`To host more agents from here: open the agent's page in Mission Control \u2192 "Join from your computer" \u2192 "Attach an existing computer". No second join.`);
|
|
8158
8468
|
await deps.reportGitAccess(arg("--repo", argv));
|
|
8159
8469
|
if (argv.includes("--no-install")) {
|
|
@@ -8165,12 +8475,12 @@ async function cmdJoin(argv, overrides = {}) {
|
|
|
8165
8475
|
async function reportGitAccess(repoUrl) {
|
|
8166
8476
|
let version = "not found";
|
|
8167
8477
|
try {
|
|
8168
|
-
version =
|
|
8478
|
+
version = execFileSync8("git", ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString().trim();
|
|
8169
8479
|
} catch {
|
|
8170
8480
|
}
|
|
8171
8481
|
let helper = "none configured";
|
|
8172
8482
|
try {
|
|
8173
|
-
helper =
|
|
8483
|
+
helper = execFileSync8("git", ["config", "--global", "--get-all", "credential.helper"], { stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString().trim().split("\n").filter(Boolean).join(", ") || helper;
|
|
8174
8484
|
} catch {
|
|
8175
8485
|
}
|
|
8176
8486
|
const url = repoUrl ?? "";
|
|
@@ -8193,6 +8503,9 @@ async function cmdRun() {
|
|
|
8193
8503
|
}
|
|
8194
8504
|
const token = tokenFor(cfg);
|
|
8195
8505
|
ensureFrodDir();
|
|
8506
|
+
const sizing = applySessionCap(cfg);
|
|
8507
|
+
if (sizing.write) writeConfig(cfg);
|
|
8508
|
+
for (const words of sizing.words) log.info(words);
|
|
8196
8509
|
ClaudeAdapter.reapOrphans();
|
|
8197
8510
|
const pruned = pruneRunDirs(cfg.workdir);
|
|
8198
8511
|
if (pruned > 0) log.event("repo.runs_pruned", { count: pruned });
|
|
@@ -8250,7 +8563,7 @@ async function cmdRun() {
|
|
|
8250
8563
|
});
|
|
8251
8564
|
if (platform6() === "win32") {
|
|
8252
8565
|
try {
|
|
8253
|
-
const child = spawn6(process.execPath, [
|
|
8566
|
+
const child = spawn6(process.execPath, [join12(CURRENT_LINK, "dist", "cli.js"), "run"], { detached: true, stdio: "ignore", windowsHide: true, env: process.env });
|
|
8254
8567
|
child.unref();
|
|
8255
8568
|
log.info(`Windows: started ${to} detached (pid ${child.pid}).`);
|
|
8256
8569
|
} catch (err) {
|
|
@@ -8319,7 +8632,8 @@ async function cmdStatus(argv) {
|
|
|
8319
8632
|
}
|
|
8320
8633
|
log.info(`This computer: "${cfg.name}" \u2014 ${cfg.person?.name ? `${cfg.person.name}'s computer` : "a computer"} in ${cfg.workspaceName ?? cfg.workspaceId} via ${cfg.apiUrl}`);
|
|
8321
8634
|
if (cfg.dormant) log.warn(dormantWords(cfg.dormant, hostedAgentsWords(cfg)));
|
|
8322
|
-
|
|
8635
|
+
const statusCap = resolveMaxSessions(cfg.maxSessions);
|
|
8636
|
+
log.info(`Token: ${cfg.tokenStore}; settings ${CONFIG_PATH}; sessions under ${cfg.workdir}/sessions; up to ${statusCap.value} at a time across every agent here (this computer's memory carries ${statusCap.ceiling} at ${SESSION_RAM_MB} MB a session${statusCap.clamped ? `, so the ${cfg.maxSessions} in the settings file is not honoured` : ""}).`);
|
|
8323
8637
|
log.info(`Working folders an agent may be given here: ${allowedRootsOf(cfg).join(", ")} (add more under "allowedRoots" in ${CONFIG_PATH}).`);
|
|
8324
8638
|
const probes = probeAllRuntimes(DEFAULT_RUNTIME_PROBES, cfg);
|
|
8325
8639
|
for (const runtime of RUNTIME_ORDER) {
|
|
@@ -8329,6 +8643,7 @@ async function cmdStatus(argv) {
|
|
|
8329
8643
|
const loc = runLocation();
|
|
8330
8644
|
log.info(`Software: ${VERSION}, running from ${loc.installedVersion ? `${VERSIONS_DIR}/${loc.installedVersion}` : `${loc.packageRoot} (not an installed version \u2014 it will not update itself; run \`npx @frod.io/bridge@latest install\`)`}; current \u2192 ${currentVersion() ?? "none"}.`);
|
|
8331
8645
|
if (platform6() === "darwin") log.info(`Runs at login: ${launchdLoaded() ? "yes (launchd)" : "no \u2014 run install"}`);
|
|
8646
|
+
if (platform6() === "win32") log.info(`Runs at logon: ${windowsTaskLoaded() ? "yes (scheduled task)" : "no \u2014 run install"}`);
|
|
8332
8647
|
log.info("Which agent runs on which runtime is the platform's to say \u2014 attach or detach agents in Mission Control.");
|
|
8333
8648
|
await reportGitAccess(arg("--repo", argv));
|
|
8334
8649
|
const token = readToken(cfg.keychainAccount, cfg.tokenStore);
|
|
@@ -8412,6 +8727,7 @@ async function cmdInstall() {
|
|
|
8412
8727
|
if (platform6() === "win32") {
|
|
8413
8728
|
const r = installWindows(target);
|
|
8414
8729
|
log.info(r.loaded ? r.detail : `NOT scheduled (${r.detail}).`);
|
|
8730
|
+
if (r.loaded && !r.hidden) log.info(r.manual);
|
|
8415
8731
|
if (!r.loaded) {
|
|
8416
8732
|
log.info(r.manual);
|
|
8417
8733
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frod.io/bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Your computer is a desk: hosts a Frod.io agent's sessions with your own Claude Code, Codex or Grok Build. Installs and updates itself.",
|
|
6
6
|
"license": "UNLICENSED",
|