@frod.io/bridge 0.7.2 → 0.7.4
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 +61 -16
- package/dist/cli.js +235 -124
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Frod.io bridge — your computer is a desk
|
|
2
2
|
|
|
3
|
-
You already have
|
|
4
|
-
|
|
3
|
+
You already have at least one supported coding runtime on your computer —
|
|
4
|
+
Claude Code, Codex, or Grok Build — with its own sign-in, settings and tools.
|
|
5
|
+
The bridge makes those ready runtimes available to **sessions of the platform**:
|
|
5
6
|
agents you choose run on your computer, take `@mentions` from Teams and
|
|
6
7
|
Telegram, read their Inbox, sit in the Office, run blueprint steps and file
|
|
7
8
|
into the map — governed and metered like every other agent — and nothing of
|
|
@@ -20,7 +21,11 @@ npx @frod.io/bridge@latest join <code> --api https://api.agentic.frod.io [--name
|
|
|
20
21
|
|
|
21
22
|
`npx` only bootstraps the command. `join` itself:
|
|
22
23
|
|
|
23
|
-
- checks
|
|
24
|
+
- checks Claude Code, Codex, and Grok Build once each, and offers exactly the
|
|
25
|
+
**READY set**: a runtime is READY only when it is installed and its sign-in
|
|
26
|
+
check explicitly says it is signed in. The computer may therefore join with
|
|
27
|
+
Codex only, Claude Code only, Grok Build only, or any mixed-ready set; it
|
|
28
|
+
does not need Claude Code when Codex or Grok Build is READY;
|
|
24
29
|
- exchanges the code for a token and keeps it in your OS keychain — macOS's
|
|
25
30
|
`security`, set up so the item trusts `/usr/bin/security` and never pops a
|
|
26
31
|
dialog; Linux's `secret-tool`; anywhere else a 0600 file, and it says so;
|
|
@@ -38,6 +43,22 @@ npx @frod.io/bridge@latest join <code> --api https://api.agentic.frod.io [--name
|
|
|
38
43
|
Pass `--no-install` to join without any of the install/run-at-login steps —
|
|
39
44
|
useful if you'd rather run `install` or `run` by hand afterwards.
|
|
40
45
|
|
|
46
|
+
An installed runtime whose sign-in result is false is not READY. Neither is a
|
|
47
|
+
runtime whose probe is unreadable, invalid, ambiguous, timed out, or unable to
|
|
48
|
+
run; an unreadable result never becomes READY by assumption. Other READY
|
|
49
|
+
runtimes still let the computer join, and only those READY runtimes are
|
|
50
|
+
offered. If none is READY, `join` stops before exchanging the code and always
|
|
51
|
+
starts its refusal with this exact text:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
No supported runtime is ready. Sign in and try again: Claude Code (`claude auth login`), Codex (`codex login`), and Grok Build (`grok login`).
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That core always names all three runtimes and all three sign-in commands. It
|
|
58
|
+
may then add `Install Claude Code.`, `Install Codex.`, or `Install Grok Build.`
|
|
59
|
+
in that order, but only for a runtime the probe established is absent. An
|
|
60
|
+
installed-but-signed-out or unreadable runtime receives no install hint.
|
|
61
|
+
|
|
41
62
|
**You join once.** A computer is *yours* on the platform, not an agent's.
|
|
42
63
|
Every further agent is attached in Mission Control — on that agent's page,
|
|
43
64
|
**Join from your computer → Attach an existing computer** — and the change
|
|
@@ -82,15 +103,15 @@ For every agent on your computer the workspace owner chooses:
|
|
|
82
103
|
from the token counts Claude Code reports (the ledger says *billed to
|
|
83
104
|
subscription (list rate)*), and its **working folder** (`<workdir>/sessions/<id>`,
|
|
84
105
|
or the run checkout for a repository step).
|
|
85
|
-
- That this computer is **connected or offline**, its name, OS
|
|
86
|
-
|
|
87
|
-
all its agents).
|
|
106
|
+
- That this computer is **connected or offline**, its name, OS, the versions of
|
|
107
|
+
its READY runtimes, and how many sessions it is running (up to
|
|
108
|
+
`--max-sessions`, across all its agents).
|
|
88
109
|
- Every permission decision, and — under *Never ask* — every call that would
|
|
89
110
|
have asked.
|
|
90
111
|
|
|
91
112
|
## What it never sees
|
|
92
113
|
|
|
93
|
-
- Your
|
|
114
|
+
- Your runtime sign-ins and local runtime settings, skills, plugins and MCP
|
|
94
115
|
servers — they load as they always do; the platform only *adds* its own
|
|
95
116
|
system prompt and its own tools on top.
|
|
96
117
|
- Any file outside the session folder unless the agent asks and a person
|
|
@@ -177,7 +198,7 @@ Disconnect**), which takes the computer off the floor at once.
|
|
|
177
198
|
| the OS keychain, item `io.frod.bridge` | the host token |
|
|
178
199
|
| `~/.frod/bridge/versions/<version>/` | one installed copy of the package |
|
|
179
200
|
| `~/.frod/bridge/current` | a symlink (junction on Windows) to the version that's running |
|
|
180
|
-
| `~/.frod/sessions.json` | which
|
|
201
|
+
| `~/.frod/sessions.json` | which local runtime session each platform session ran as (for resume) |
|
|
181
202
|
| `~/.frod/bridge.log` | the log — tokens are redacted before anything is written |
|
|
182
203
|
| `<workdir>/sessions/<id>` | a session's folder (the default) |
|
|
183
204
|
| the folder the owner chose | a session's folder when the agent's binding names a **working folder on this computer** (see below) |
|
|
@@ -216,14 +237,13 @@ is spelled.
|
|
|
216
237
|
|
|
217
238
|
The bridge talks to a tool through one small interface — `start`, `prompt`,
|
|
218
239
|
`cancel`, `stop` (`src/adapters/types.ts`). **`claude`**
|
|
219
|
-
(`src/adapters/claude.ts`)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
others. Each adapter is one file.
|
|
240
|
+
(`src/adapters/claude.ts`), **`codex`** (`src/adapters/codex.ts`), and
|
|
241
|
+
**`grok`** (`src/adapters/grok.ts`) are peers: each is offered only when its
|
|
242
|
+
runtime is installed and explicitly signed in. At least one must be READY;
|
|
243
|
+
none is mandatory by itself. Which agent runs on which offered adapter is
|
|
244
|
+
decided in Mission Control, not here. Grok Build runs in its `workspace`
|
|
245
|
+
sandbox, and the bridge answers its permission questions by the same Ask /
|
|
246
|
+
Never-ask policy as the others. Each adapter is one file.
|
|
227
247
|
|
|
228
248
|
## Running at login
|
|
229
249
|
|
|
@@ -242,6 +262,31 @@ others. Each adapter is one file.
|
|
|
242
262
|
(`schtasks /Create`), also pointing at `current`. If it refuses, the exact
|
|
243
263
|
command is printed. Not yet proven on a real Windows machine.
|
|
244
264
|
|
|
265
|
+
## Changelog
|
|
266
|
+
|
|
267
|
+
### 0.7.4 — unreleased
|
|
268
|
+
|
|
269
|
+
- `join` is runtime-neutral: it admits a computer when at least one of Claude
|
|
270
|
+
Code, Codex, or Grok Build is installed and explicitly signed in, and offers
|
|
271
|
+
exactly that READY set.
|
|
272
|
+
- All three runtime probes use the same tri-state sign-in contract: `true` is
|
|
273
|
+
ready, explicit signed-out is `false`, and unreadable or unestablished state
|
|
274
|
+
is unknown rather than ready.
|
|
275
|
+
- Configuration, announced adapters, version-bearing runtime segments, and
|
|
276
|
+
capacity sampling come from that READY set. The version string keeps the
|
|
277
|
+
compatibility-only `claude ?` sentinel when Claude Code is not READY; the
|
|
278
|
+
sentinel never offers Claude Code.
|
|
279
|
+
- This release is waiting for owner review at the merge queue. It has not been
|
|
280
|
+
merged, published, signed, tagged, announced, deployed, or rolled out; only
|
|
281
|
+
the owner may perform those release and rollout actions in the owner-present
|
|
282
|
+
rollout window.
|
|
283
|
+
|
|
284
|
+
### 0.7.3 — historical
|
|
285
|
+
|
|
286
|
+
- The previous bridge release retained the Claude-required join preflight.
|
|
287
|
+
Its behavior is historical and is superseded by the unreleased 0.7.4 rule
|
|
288
|
+
above.
|
|
289
|
+
|
|
245
290
|
## Publishing
|
|
246
291
|
|
|
247
292
|
`.github/workflows/deploy-bridge.yml` runs on every push to `main` that
|
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.4. 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;
|
|
@@ -3706,9 +3706,10 @@ var require_websocket_server = __commonJS({
|
|
|
3706
3706
|
|
|
3707
3707
|
// src/cli.ts
|
|
3708
3708
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
3709
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "node:fs";
|
|
3709
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9, realpathSync as realpathSync4 } from "node:fs";
|
|
3710
3710
|
import { homedir as homedir7, hostname as hostname2, platform as platform6 } from "node:os";
|
|
3711
3711
|
import { dirname as dirname5, join as join11, resolve as resolve6 } from "node:path";
|
|
3712
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3712
3713
|
import { spawn as spawn6 } from "node:child_process";
|
|
3713
3714
|
|
|
3714
3715
|
// src/config.ts
|
|
@@ -4482,24 +4483,23 @@ var ClaudeAdapter = class {
|
|
|
4482
4483
|
writeChildren([...this.live.values()].map((l) => l.child?.pid).filter((p) => typeof p === "number"));
|
|
4483
4484
|
}
|
|
4484
4485
|
/** `claude --version`, and whether the CLI says it is signed in. Never throws. */
|
|
4485
|
-
static check(claudePath) {
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
let
|
|
4486
|
+
static check(claudePath, commands = {}) {
|
|
4487
|
+
const versionCommand = commands.version ?? ((path) => execFileSync2(path, ["--version"], { env: cleanEnv(process.env), stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString());
|
|
4488
|
+
const authStatusCommand = commands.authStatus ?? ((path) => execFileSync2(path, ["auth", "status"], { env: cleanEnv(process.env), stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString());
|
|
4489
|
+
let version;
|
|
4489
4490
|
try {
|
|
4490
|
-
version =
|
|
4491
|
+
version = versionCommand(claudePath).trim();
|
|
4492
|
+
if (!/\d+\.\d+\.\d+/.test(version)) throw new Error("answered without a valid version");
|
|
4491
4493
|
} catch (err) {
|
|
4492
|
-
detail
|
|
4494
|
+
return { version: null, loggedIn: null, detail: `--version failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
4493
4495
|
}
|
|
4494
4496
|
try {
|
|
4495
|
-
const
|
|
4496
|
-
const
|
|
4497
|
-
loggedIn
|
|
4498
|
-
detail += `auth: ${parsed.authMethod ?? "?"}${parsed.subscriptionType ? ` (${parsed.subscriptionType})` : ""}. `;
|
|
4497
|
+
const parsed = JSON.parse(authStatusCommand(claudePath));
|
|
4498
|
+
const loggedIn = parsed.loggedIn === true ? true : parsed.loggedIn === false ? false : null;
|
|
4499
|
+
return { version, loggedIn, detail: `auth: ${parsed.authMethod ?? "?"}${parsed.subscriptionType ? ` (${parsed.subscriptionType})` : ""}` };
|
|
4499
4500
|
} catch (err) {
|
|
4500
|
-
detail
|
|
4501
|
+
return { version, loggedIn: null, detail: `auth status failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
4501
4502
|
}
|
|
4502
|
-
return { version, loggedIn, detail: detail.trim() };
|
|
4503
4503
|
}
|
|
4504
4504
|
async start(spec, o) {
|
|
4505
4505
|
if (!existsSync4(o.sessionDir)) mkdirSync2(o.sessionDir, { recursive: true, mode: 448 });
|
|
@@ -5045,19 +5045,30 @@ var CodexAdapter = class _CodexAdapter {
|
|
|
5045
5045
|
return "The Codex adapter needs the `codex` CLI on this computer and a signed-in account (`codex login`).";
|
|
5046
5046
|
}
|
|
5047
5047
|
/** The version and whether somebody is signed in — the same shape ClaudeAdapter.check returns. */
|
|
5048
|
-
static check(codexPath) {
|
|
5049
|
-
|
|
5048
|
+
static check(codexPath, commands = {}) {
|
|
5049
|
+
const versionCommand = commands.version ?? ((path) => execFileSync3(path, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 2e4 }).toString());
|
|
5050
|
+
const loginStatusCommand = commands.loginStatus ?? ((path) => {
|
|
5051
|
+
const res = spawnSync(path, ["login", "status"], { encoding: "utf8", timeout: 3e4 });
|
|
5052
|
+
if (res.error) throw res.error;
|
|
5053
|
+
return `${res.stdout ?? ""}${res.stderr ?? ""}`;
|
|
5054
|
+
});
|
|
5055
|
+
let version;
|
|
5050
5056
|
try {
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5057
|
+
const raw = versionCommand(codexPath).trim();
|
|
5058
|
+
version = /(\d+\.\d+\.\d+[0-9A-Za-z.-]*)/.exec(raw)?.[1] ?? "";
|
|
5059
|
+
if (!version) throw new Error("answered without a valid version");
|
|
5060
|
+
} catch (err) {
|
|
5061
|
+
return { version: null, loggedIn: null, detail: `${codexPath} did not run \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
5054
5062
|
}
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
return { version, loggedIn: false, detail
|
|
5063
|
+
try {
|
|
5064
|
+
const out = loginStatusCommand(codexPath).trim();
|
|
5065
|
+
const detail = out.split("\n").find((line) => line.trim().length > 0)?.trim() || "could not read the sign-in state \u2014 run `codex login`";
|
|
5066
|
+
if (/\b(?:not logged in|not signed in|not authenticated|unauthenticated)\b/i.test(out)) return { version, loggedIn: false, detail };
|
|
5067
|
+
if (/\blogged in\b/i.test(out)) return { version, loggedIn: true, detail };
|
|
5068
|
+
return { version, loggedIn: null, detail };
|
|
5069
|
+
} catch (err) {
|
|
5070
|
+
return { version, loggedIn: null, detail: `could not read the sign-in state: ${err instanceof Error ? err.message : String(err)}` };
|
|
5059
5071
|
}
|
|
5060
|
-
return { version, loggedIn: /logged in/i.test(out), detail: out.split("\n")[0] ?? "" };
|
|
5061
5072
|
}
|
|
5062
5073
|
/**
|
|
5063
5074
|
* A session's credential tier. `codex login status` says how the person
|
|
@@ -5754,23 +5765,32 @@ var GrokAdapter = class _GrokAdapter {
|
|
|
5754
5765
|
* says "You are not authenticated." and opens no browser, so this is safe
|
|
5755
5766
|
* to run unattended at every start.
|
|
5756
5767
|
*/
|
|
5757
|
-
static check(grokPath) {
|
|
5768
|
+
static check(grokPath, commands = {}) {
|
|
5758
5769
|
const env = { ...process.env, GROK_DISABLE_AUTOUPDATER: "1" };
|
|
5759
|
-
|
|
5770
|
+
const versionCommand = commands.version ?? ((path) => execFileSync4(path, ["--version"], { env, stdio: ["ignore", "pipe", "ignore"], timeout: 2e4 }).toString());
|
|
5771
|
+
const modelsCommand = commands.models ?? ((path) => {
|
|
5772
|
+
const res = spawnSync2(path, ["models"], { env, encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5773
|
+
if (res.error) throw res.error;
|
|
5774
|
+
return `${res.stdout ?? ""}
|
|
5775
|
+
${res.stderr ?? ""}`;
|
|
5776
|
+
});
|
|
5777
|
+
let version;
|
|
5760
5778
|
try {
|
|
5761
|
-
const
|
|
5762
|
-
version = /(\d+\.\d+\.\d+[0-9A-Za-z.-]*)/.exec(
|
|
5763
|
-
|
|
5764
|
-
|
|
5779
|
+
const out = versionCommand(grokPath);
|
|
5780
|
+
version = /(\d+\.\d+\.\d+[0-9A-Za-z.-]*)/.exec(out)?.[1] ?? "";
|
|
5781
|
+
if (!version) throw new Error("answered without a valid version");
|
|
5782
|
+
} catch (err) {
|
|
5783
|
+
return { version: null, loggedIn: null, detail: `${grokPath} did not run \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
5784
|
+
}
|
|
5785
|
+
try {
|
|
5786
|
+
const out = modelsCommand(grokPath).trim();
|
|
5787
|
+
const first = out.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
|
|
5788
|
+
if (/\b(?:not logged in|not signed in|not authenticated|unauthenticated)\b/i.test(out)) return { version, loggedIn: false, detail: "not signed in \u2014 run `grok login`" };
|
|
5789
|
+
if (/\blogged in\b/i.test(out)) return { version, loggedIn: true, detail: first };
|
|
5790
|
+
return { version, loggedIn: null, detail: first || "could not read the sign-in state \u2014 run `grok login`" };
|
|
5791
|
+
} catch (err) {
|
|
5792
|
+
return { version, loggedIn: null, detail: `could not read the sign-in state: ${err instanceof Error ? err.message : String(err)}` };
|
|
5765
5793
|
}
|
|
5766
|
-
const res = spawnSync2(grokPath, ["models"], { env, encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5767
|
-
const out = `${res.stdout ?? ""}
|
|
5768
|
-
${res.stderr ?? ""}`.trim();
|
|
5769
|
-
const first = out.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
5770
|
-
if (res.error || !out) return { version, loggedIn: false, detail: first || "could not read the sign-in state \u2014 run `grok login`" };
|
|
5771
|
-
if (/logged in/i.test(out)) return { version, loggedIn: true, detail: first };
|
|
5772
|
-
if (/not authenticated/i.test(out)) return { version, loggedIn: false, detail: "not signed in \u2014 run `grok login`" };
|
|
5773
|
-
return { version, loggedIn: false, detail: first };
|
|
5774
5794
|
}
|
|
5775
5795
|
/** A grok.com sign-in is the subscription; anything else that runs is a key (`XAI_API_KEY`). */
|
|
5776
5796
|
credentialTier() {
|
|
@@ -6481,6 +6501,9 @@ function workdirRequestFor(spec) {
|
|
|
6481
6501
|
}
|
|
6482
6502
|
|
|
6483
6503
|
// src/connection.ts
|
|
6504
|
+
var RECONCILE_INTERVAL_MS = 6e4;
|
|
6505
|
+
var RECONCILE_WAIT_MS = 1500;
|
|
6506
|
+
var STOP_BOUND_MS = 1e4;
|
|
6484
6507
|
var HostConnection = class {
|
|
6485
6508
|
constructor(o) {
|
|
6486
6509
|
this.o = o;
|
|
@@ -6498,6 +6521,7 @@ var HostConnection = class {
|
|
|
6498
6521
|
/** Which runtime started each session — a turn must reach the one holding it. */
|
|
6499
6522
|
sessionAdapter = /* @__PURE__ */ new Map();
|
|
6500
6523
|
attempt = 0;
|
|
6524
|
+
reconcileTimer = null;
|
|
6501
6525
|
connectedAt = null;
|
|
6502
6526
|
/** Requests this computer sent to the pod (night 2: permissions), by id. */
|
|
6503
6527
|
nextRequestId = 1;
|
|
@@ -6593,6 +6617,7 @@ var HostConnection = class {
|
|
|
6593
6617
|
const reason = reasonBuf.toString();
|
|
6594
6618
|
this.connectedAt = null;
|
|
6595
6619
|
this.ws = null;
|
|
6620
|
+
this.stopReconciling();
|
|
6596
6621
|
this.failPending(`the connection closed (${code})`);
|
|
6597
6622
|
void this.endAllSessions(`connection closed (${code}${reason ? ` ${reason}` : ""})`);
|
|
6598
6623
|
if (final && (code === 4403 || code === 4404 || final.reason.startsWith("Another computer") || final.reason.startsWith("This computer left") || final.reason.startsWith("This computer was disconnected") || /access was revoked/.test(final.reason))) {
|
|
@@ -6634,6 +6659,8 @@ var HostConnection = class {
|
|
|
6634
6659
|
const hosted = hostedAgentsWords({ bindings, agentName: this.o.config.agentName });
|
|
6635
6660
|
log.event("host.welcome", { host: params.hostName ?? this.o.config.name, host_id: params.hostId ?? null, person: params.person?.name ?? null, workspace: params.workspaceId, agents: bindings.map((b) => `${b.agentName}: ${bindingWords(b)}`), ping_ms: params.pingIntervalMs });
|
|
6636
6661
|
log.info(`Connected \u2014 ${this.o.config.name} hosts ${hosted}`);
|
|
6662
|
+
this.reconcile("welcome");
|
|
6663
|
+
this.startReconciling();
|
|
6637
6664
|
if (typeof params.hostId === "string") {
|
|
6638
6665
|
this.o.onWelcome?.({ hostId: params.hostId, hostName: String(params.hostName ?? this.o.config.name), person: params.person ?? null, bindings, bridge: params.bridge });
|
|
6639
6666
|
}
|
|
@@ -6673,7 +6700,13 @@ var HostConnection = class {
|
|
|
6673
6700
|
const spec = params;
|
|
6674
6701
|
if (this.draining || this.stopping) return fail(`${this.o.config.name} is restarting${this.draining ? " to update itself" : ""} \u2014 try again in a moment`);
|
|
6675
6702
|
if (this.sessions.size >= this.o.config.maxSessions && !this.sessions.has(sid)) {
|
|
6676
|
-
|
|
6703
|
+
const before = this.sessions.size;
|
|
6704
|
+
this.reconcile("desk full");
|
|
6705
|
+
await sleep(RECONCILE_WAIT_MS);
|
|
6706
|
+
log.event("session.reconciled", { session: sid, before, after: this.sessions.size, max: this.o.config.maxSessions });
|
|
6707
|
+
if (this.sessions.size >= this.o.config.maxSessions) {
|
|
6708
|
+
return fail(`${this.o.config.name} is busy (${this.sessions.size} of ${this.o.config.maxSessions} sessions)`);
|
|
6709
|
+
}
|
|
6677
6710
|
}
|
|
6678
6711
|
this.sessions.add(sid);
|
|
6679
6712
|
this.sessionsChanged();
|
|
@@ -6774,7 +6807,10 @@ var HostConnection = class {
|
|
|
6774
6807
|
return;
|
|
6775
6808
|
}
|
|
6776
6809
|
if (method === "session/stop") {
|
|
6777
|
-
|
|
6810
|
+
const reason = typeof params.reason === "string" ? params.reason : "stopped";
|
|
6811
|
+
if (reason === "unknown_session") log.event("session.forgotten_by_pod", { session: sid });
|
|
6812
|
+
await this.adapterOf(sid).stop(sid, reason);
|
|
6813
|
+
this.forget(sid);
|
|
6778
6814
|
return;
|
|
6779
6815
|
}
|
|
6780
6816
|
if (method === "session/permission_decided") {
|
|
@@ -6869,15 +6905,50 @@ var HostConnection = class {
|
|
|
6869
6905
|
}
|
|
6870
6906
|
async endAllSessions(reason) {
|
|
6871
6907
|
if (this.sessions.size === 0) return;
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
this.
|
|
6877
|
-
this.checkouts.delete(sid);
|
|
6908
|
+
const sids = [...this.sessions];
|
|
6909
|
+
log.warn(`Stopping ${sids.length} session(s): ${reason}`);
|
|
6910
|
+
for (const sid of sids) this.forget(sid, { keepAdapter: true });
|
|
6911
|
+
await Promise.all(sids.map(async (sid) => {
|
|
6912
|
+
const adapter = this.sessionAdapter.get(sid) ?? this.adapterOf(sid);
|
|
6878
6913
|
this.sessionAdapter.delete(sid);
|
|
6914
|
+
await Promise.race([
|
|
6915
|
+
adapter.stop(sid, "host_disconnected").catch(() => {
|
|
6916
|
+
}),
|
|
6917
|
+
sleep(STOP_BOUND_MS).then(() => log.warn(`[${sid.slice(0, 8)}] did not stop within ${STOP_BOUND_MS / 1e3} s \u2014 slot freed anyway`))
|
|
6918
|
+
]);
|
|
6919
|
+
}));
|
|
6920
|
+
}
|
|
6921
|
+
/** Free a session's slot and everything this computer remembered about it. Idempotent. */
|
|
6922
|
+
forget(sid, opts = {}) {
|
|
6923
|
+
const had = this.sessions.delete(sid);
|
|
6924
|
+
this.checkouts.delete(sid);
|
|
6925
|
+
this.doorCheckouts.forget(sid);
|
|
6926
|
+
this.sessionDirs.delete(sid);
|
|
6927
|
+
if (!opts.keepAdapter) this.sessionAdapter.delete(sid);
|
|
6928
|
+
if (had) this.sessionsChanged();
|
|
6929
|
+
}
|
|
6930
|
+
/**
|
|
6931
|
+
* No tails: ask the pod about every lane this computer holds. A known
|
|
6932
|
+
* session gets silence; a forgotten one gets `session/stop` back, which
|
|
6933
|
+
* frees its slot through the ordinary path. Sent only while connected.
|
|
6934
|
+
*/
|
|
6935
|
+
reconcile(why) {
|
|
6936
|
+
if (this.sessions.size === 0 || !this.ws || this.ws.readyState !== wrapper_default.OPEN) return;
|
|
6937
|
+
log.event("sessions.reconcile", { why, held: this.sessions.size, max: this.o.config.maxSessions });
|
|
6938
|
+
for (const sid of this.sessions) {
|
|
6939
|
+
this.send({ sid, msg: { jsonrpc: "2.0", method: "session/heartbeat", params: { sessionId: sid } } });
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
startReconciling() {
|
|
6943
|
+
this.stopReconciling();
|
|
6944
|
+
this.reconcileTimer = setInterval(() => this.reconcile("interval"), RECONCILE_INTERVAL_MS);
|
|
6945
|
+
this.reconcileTimer.unref?.();
|
|
6946
|
+
}
|
|
6947
|
+
stopReconciling() {
|
|
6948
|
+
if (this.reconcileTimer) {
|
|
6949
|
+
clearInterval(this.reconcileTimer);
|
|
6950
|
+
this.reconcileTimer = null;
|
|
6879
6951
|
}
|
|
6880
|
-
this.sessionsChanged();
|
|
6881
6952
|
}
|
|
6882
6953
|
send(frame) {
|
|
6883
6954
|
const ws = this.ws;
|
|
@@ -7779,7 +7850,7 @@ function samplesInWords(samples) {
|
|
|
7779
7850
|
}
|
|
7780
7851
|
|
|
7781
7852
|
// src/cli.ts
|
|
7782
|
-
var VERSION = "0.7.
|
|
7853
|
+
var VERSION = "0.7.4";
|
|
7783
7854
|
function arg(flag, argv) {
|
|
7784
7855
|
const i = argv.indexOf(flag);
|
|
7785
7856
|
return i >= 0 ? argv[i + 1] : void 0;
|
|
@@ -7791,7 +7862,7 @@ function findClaude() {
|
|
|
7791
7862
|
return execFileSync7("/usr/bin/which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
7792
7863
|
} catch {
|
|
7793
7864
|
}
|
|
7794
|
-
|
|
7865
|
+
return null;
|
|
7795
7866
|
}
|
|
7796
7867
|
function findCodex() {
|
|
7797
7868
|
const candidates = [process.env.FROD_CODEX_PATH, join11(homedir7(), ".local", "bin", "codex"), "/opt/homebrew/bin/codex", "/usr/local/bin/codex"].filter((c) => !!c);
|
|
@@ -7813,33 +7884,54 @@ function findGrok() {
|
|
|
7813
7884
|
return null;
|
|
7814
7885
|
}
|
|
7815
7886
|
}
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7887
|
+
var RUNTIME_ORDER = ["claude", "codex", "grok"];
|
|
7888
|
+
var RUNTIME_LABELS = { claude: "Claude Code", codex: "Codex", grok: "Grok Build" };
|
|
7889
|
+
var INSTALL_HINTS = { claude: "Install Claude Code.", codex: "Install Codex.", grok: "Install Grok Build." };
|
|
7890
|
+
var RUNTIME_PREFLIGHT_REFUSAL = "No supported runtime is ready. Sign in and try again: Claude Code (`claude auth login`), Codex (`codex login`), and Grok Build (`grok login`).";
|
|
7891
|
+
function checkedRuntime(name, path) {
|
|
7892
|
+
if (!path) return { path: null, version: null, loggedIn: null, detail: `not installed on this computer` };
|
|
7893
|
+
try {
|
|
7894
|
+
const check = name === "claude" ? ClaudeAdapter.check(path) : name === "codex" ? CodexAdapter.check(path) : GrokAdapter.check(path);
|
|
7895
|
+
return { path, ...check };
|
|
7896
|
+
} catch (err) {
|
|
7897
|
+
return { path, version: null, loggedIn: null, detail: `could not run: ${err instanceof Error ? err.message : String(err)}` };
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
var DEFAULT_RUNTIME_PROBES = {
|
|
7901
|
+
claude: (preferredPath) => checkedRuntime("claude", preferredPath ?? findClaude()),
|
|
7902
|
+
codex: (preferredPath) => checkedRuntime("codex", preferredPath ?? findCodex()),
|
|
7903
|
+
grok: (preferredPath) => checkedRuntime("grok", preferredPath ?? findGrok())
|
|
7904
|
+
};
|
|
7905
|
+
function probeAllRuntimes(probes, preferred) {
|
|
7906
|
+
return {
|
|
7907
|
+
claude: probes.claude(preferred?.claudePath),
|
|
7908
|
+
codex: probes.codex(preferred?.codexPath),
|
|
7909
|
+
grok: probes.grok(preferred?.grokPath)
|
|
7819
7910
|
};
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
}
|
|
7842
|
-
|
|
7911
|
+
}
|
|
7912
|
+
function isReady(probe) {
|
|
7913
|
+
return probe.path !== null && probe.version !== null && probe.loggedIn === true;
|
|
7914
|
+
}
|
|
7915
|
+
function readyNames(probes) {
|
|
7916
|
+
return RUNTIME_ORDER.filter((name) => isReady(probes[name]));
|
|
7917
|
+
}
|
|
7918
|
+
function runtimePreflightRefusal(probes) {
|
|
7919
|
+
const hints = RUNTIME_ORDER.filter((name) => probes[name].path === null).map((name) => INSTALL_HINTS[name]);
|
|
7920
|
+
return `${RUNTIME_PREFLIGHT_REFUSAL}${hints.length > 0 ? ` ${hints.join(" ")}` : ""}`;
|
|
7921
|
+
}
|
|
7922
|
+
function versionForReadySet(probes) {
|
|
7923
|
+
return runtimeVersionString2(
|
|
7924
|
+
isReady(probes.claude) ? probes.claude.version : null,
|
|
7925
|
+
isReady(probes.codex) ? probes.codex.version : null,
|
|
7926
|
+
isReady(probes.grok) ? probes.grok.version : null
|
|
7927
|
+
);
|
|
7928
|
+
}
|
|
7929
|
+
function buildAdapters(probes, permissions) {
|
|
7930
|
+
const adapters = {};
|
|
7931
|
+
if (isReady(probes.claude)) adapters.claude = new ClaudeAdapter({ claudePath: probes.claude.path, permissions });
|
|
7932
|
+
if (isReady(probes.codex)) adapters.codex = new CodexAdapter({ codexPath: probes.codex.path });
|
|
7933
|
+
if (isReady(probes.grok)) adapters.grok = new GrokAdapter({ grokPath: probes.grok.path });
|
|
7934
|
+
return { adapters, names: readyNames(probes) };
|
|
7843
7935
|
}
|
|
7844
7936
|
function runtimeVersionString2(claudeVersion, codexVersion, grokVersion = null) {
|
|
7845
7937
|
return runtimeVersionString(VERSION, claudeVersion, codexVersion, grokVersion);
|
|
@@ -7853,37 +7945,42 @@ function tokenFor(cfg) {
|
|
|
7853
7945
|
if (!token) throw new Error("The host token is missing from the keychain. Ask for a new join code and join again.");
|
|
7854
7946
|
return token;
|
|
7855
7947
|
}
|
|
7856
|
-
async function cmdJoin(argv) {
|
|
7948
|
+
async function cmdJoin(argv, overrides = {}) {
|
|
7949
|
+
const deps = {
|
|
7950
|
+
readConfig,
|
|
7951
|
+
probes: DEFAULT_RUNTIME_PROBES,
|
|
7952
|
+
join: join3,
|
|
7953
|
+
storeToken,
|
|
7954
|
+
ensureWorkdir: (path) => {
|
|
7955
|
+
if (!existsSync12(path)) mkdirSync9(path, { recursive: true, mode: 448 });
|
|
7956
|
+
},
|
|
7957
|
+
writeConfig,
|
|
7958
|
+
reportGitAccess,
|
|
7959
|
+
install: cmdInstall,
|
|
7960
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
7961
|
+
...overrides
|
|
7962
|
+
};
|
|
7857
7963
|
const code = argv[0];
|
|
7858
7964
|
const apiUrl = arg("--api", argv) ?? process.env.FROD_API_URL;
|
|
7859
7965
|
if (!code || !apiUrl) throw new Error(`Usage: join <code> --api https://api.agentic.frod.io [--name "Wafeek's Mac"] [--workdir <dir>] [--max-sessions N]`);
|
|
7860
|
-
const existing = readConfig();
|
|
7966
|
+
const existing = deps.readConfig();
|
|
7861
7967
|
const refusal = existing ? secondJoinWords(existing) : null;
|
|
7862
7968
|
if (refusal) throw new Error(refusal);
|
|
7863
7969
|
const safeApi = assertSafeEndpoint(apiUrl);
|
|
7864
7970
|
const name = arg("--name", argv) ?? defaultName();
|
|
7865
7971
|
const workdir = resolve6(arg("--workdir", argv) ?? join11(homedir7(), "frod-work"));
|
|
7866
7972
|
const maxSessions = Math.max(1, Math.min(20, Number(arg("--max-sessions", argv) ?? 2)));
|
|
7867
|
-
const
|
|
7868
|
-
const
|
|
7869
|
-
|
|
7870
|
-
|
|
7973
|
+
const probes = probeAllRuntimes(deps.probes);
|
|
7974
|
+
for (const runtime of RUNTIME_ORDER) {
|
|
7975
|
+
const probe = probes[runtime];
|
|
7976
|
+
log.info(`${RUNTIME_LABELS[runtime]}: ${probe.version ?? (probe.path ? "unknown version" : "not installed")} \u2014 ${probe.detail}`);
|
|
7977
|
+
}
|
|
7978
|
+
const adapters = readyNames(probes);
|
|
7979
|
+
if (adapters.length === 0) throw new Error(runtimePreflightRefusal(probes));
|
|
7871
7980
|
log.info(`Joining ${safeApi} as "${name}"\u2026`);
|
|
7872
|
-
const
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
const c = CodexAdapter.check(cp);
|
|
7876
|
-
return c.loggedIn ? c.version : null;
|
|
7877
|
-
})();
|
|
7878
|
-
const grokAtJoin = (() => {
|
|
7879
|
-
const gp = findGrok();
|
|
7880
|
-
if (!gp) return null;
|
|
7881
|
-
const g = GrokAdapter.check(gp);
|
|
7882
|
-
return g.loggedIn ? g.version : null;
|
|
7883
|
-
})();
|
|
7884
|
-
const joined = await join3(safeApi, code, name, { os: platform6(), version: runtimeVersionString2(check.version, codexAtJoin, grokAtJoin) });
|
|
7885
|
-
const store = storeToken(joined.hostId, joined.token);
|
|
7886
|
-
if (!existsSync12(workdir)) mkdirSync9(workdir, { recursive: true, mode: 448 });
|
|
7981
|
+
const joined = await deps.join(safeApi, code, name, { os: platform6(), version: versionForReadySet(probes) });
|
|
7982
|
+
const store = deps.storeToken(joined.hostId, joined.token);
|
|
7983
|
+
deps.ensureWorkdir(workdir);
|
|
7887
7984
|
const cfg = {
|
|
7888
7985
|
apiUrl: safeApi,
|
|
7889
7986
|
hostId: joined.hostId,
|
|
@@ -7893,26 +7990,28 @@ async function cmdJoin(argv) {
|
|
|
7893
7990
|
name,
|
|
7894
7991
|
workdir,
|
|
7895
7992
|
maxSessions,
|
|
7896
|
-
adapters
|
|
7897
|
-
claudePath,
|
|
7993
|
+
adapters,
|
|
7994
|
+
claudePath: isReady(probes.claude) ? probes.claude.path : null,
|
|
7995
|
+
codexPath: isReady(probes.codex) ? probes.codex.path : void 0,
|
|
7996
|
+
grokPath: isReady(probes.grok) ? probes.grok.path : void 0,
|
|
7898
7997
|
tokenStore: store,
|
|
7899
7998
|
keychainAccount: joined.hostId,
|
|
7900
|
-
joinedAt:
|
|
7999
|
+
joinedAt: deps.now(),
|
|
7901
8000
|
dormant: null,
|
|
7902
8001
|
bindings: joined.bindings ?? []
|
|
7903
8002
|
};
|
|
7904
|
-
writeConfig(cfg);
|
|
8003
|
+
deps.writeConfig(cfg);
|
|
7905
8004
|
log.info(`Joined. This computer is now "${name}" \u2014 ${joined.person?.name ?? "your"} computer in ${joined.workspaceName ?? joined.workspaceId} \u2014 hosting ${hostedAgentsWords(cfg)}.`);
|
|
7906
8005
|
for (const b of cfg.bindings ?? []) log.info(` ${b.agentName}: ${bindingWords(b)}`);
|
|
7907
8006
|
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)}.`);
|
|
7908
8007
|
log.info(`Settings: ${CONFIG_PATH}. Sessions work under ${workdir}/sessions. Up to ${maxSessions} at a time, across every agent this computer hosts.`);
|
|
7909
8008
|
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.`);
|
|
7910
|
-
await reportGitAccess(arg("--repo", argv));
|
|
8009
|
+
await deps.reportGitAccess(arg("--repo", argv));
|
|
7911
8010
|
if (argv.includes("--no-install")) {
|
|
7912
8011
|
log.info("Next: `install` to run at login, or `run` to run now.");
|
|
7913
8012
|
return;
|
|
7914
8013
|
}
|
|
7915
|
-
await
|
|
8014
|
+
await deps.install();
|
|
7916
8015
|
}
|
|
7917
8016
|
async function reportGitAccess(repoUrl) {
|
|
7918
8017
|
let version = "not found";
|
|
@@ -7948,24 +8047,30 @@ async function cmdRun() {
|
|
|
7948
8047
|
ClaudeAdapter.reapOrphans();
|
|
7949
8048
|
const pruned = pruneRunDirs(cfg.workdir);
|
|
7950
8049
|
if (pruned > 0) log.event("repo.runs_pruned", { count: pruned });
|
|
8050
|
+
const probes = probeAllRuntimes(DEFAULT_RUNTIME_PROBES, cfg);
|
|
8051
|
+
const ready = readyNames(probes);
|
|
8052
|
+
if (ready.length === 0) throw new Error(runtimePreflightRefusal(probes));
|
|
7951
8053
|
const permissions = new PermissionServer();
|
|
7952
8054
|
await permissions.start();
|
|
7953
|
-
const { adapters, names
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
8055
|
+
const { adapters, names } = buildAdapters(probes, permissions);
|
|
8056
|
+
log.info(`Runtimes this computer hosts: ${names.join(", ")}. ${RUNTIME_ORDER.map((name) => `${RUNTIME_LABELS[name]}: ${probes[name].version ?? (probes[name].path ? "unknown version" : "not installed")} \u2014 ${probes[name].detail}`).join(". ")}.`);
|
|
8057
|
+
const runtimeConfigBefore = JSON.stringify({ adapters: cfg.adapters ?? [], claudePath: cfg.claudePath, codexPath: cfg.codexPath, grokPath: cfg.grokPath });
|
|
8058
|
+
cfg.adapters = names;
|
|
8059
|
+
cfg.claudePath = isReady(probes.claude) ? probes.claude.path : null;
|
|
8060
|
+
cfg.codexPath = isReady(probes.codex) ? probes.codex.path : void 0;
|
|
8061
|
+
cfg.grokPath = isReady(probes.grok) ? probes.grok.path : void 0;
|
|
8062
|
+
if (runtimeConfigBefore !== JSON.stringify({ adapters: cfg.adapters, claudePath: cfg.claudePath, codexPath: cfg.codexPath, grokPath: cfg.grokPath })) {
|
|
7958
8063
|
writeConfig(cfg);
|
|
7959
8064
|
}
|
|
7960
|
-
log.event("bridge.start", { version: VERSION, name: cfg.name, host_id: cfg.hostId, agents: (cfg.bindings ?? []).map((b) => b.agentName), api: cfg.apiUrl, claude:
|
|
7961
|
-
const codexPath =
|
|
7962
|
-
const grokPath =
|
|
8065
|
+
log.event("bridge.start", { version: VERSION, name: cfg.name, host_id: cfg.hostId, agents: (cfg.bindings ?? []).map((b) => b.agentName), api: cfg.apiUrl, claude: probes.claude.version, logged_in: probes.claude.loggedIn, max_sessions: cfg.maxSessions, os: platform6(), runs_from: runLocation().installedVersion ? `versions/${runLocation().installedVersion}` : runLocation().packageRoot });
|
|
8066
|
+
const codexPath = isReady(probes.codex) ? probes.codex.path : null;
|
|
8067
|
+
const grokPath = isReady(probes.grok) ? probes.grok.path : null;
|
|
7963
8068
|
let sampling = false;
|
|
7964
8069
|
const sampleAndSend = async () => {
|
|
7965
8070
|
if (sampling) return;
|
|
7966
8071
|
sampling = true;
|
|
7967
8072
|
try {
|
|
7968
|
-
const { at, samples } = await sampleCapacity({ claude:
|
|
8073
|
+
const { at, samples } = await sampleCapacity({ claude: isReady(probes.claude), codexPath, grokPath });
|
|
7969
8074
|
conn.sendHost("host/capacity", { at, samples });
|
|
7970
8075
|
log.event("capacity.sampled", { at, words: samplesInWords(samples) });
|
|
7971
8076
|
} catch (err) {
|
|
@@ -8010,7 +8115,7 @@ async function cmdRun() {
|
|
|
8010
8115
|
config: cfg,
|
|
8011
8116
|
token,
|
|
8012
8117
|
adapters,
|
|
8013
|
-
version:
|
|
8118
|
+
version: versionForReadySet(probes),
|
|
8014
8119
|
onFinal: (reason2) => {
|
|
8015
8120
|
writeConfig({ ...cfg, dormant: { reason: reason2, at: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
8016
8121
|
log.warn(dormantWords({ reason: reason2, at: (/* @__PURE__ */ new Date()).toISOString() }, hostedAgentsWords(cfg)));
|
|
@@ -8067,17 +8172,14 @@ async function cmdStatus(argv) {
|
|
|
8067
8172
|
if (cfg.dormant) log.warn(dormantWords(cfg.dormant, hostedAgentsWords(cfg)));
|
|
8068
8173
|
log.info(`Token: ${cfg.tokenStore}; settings ${CONFIG_PATH}; sessions under ${cfg.workdir}/sessions; up to ${cfg.maxSessions} at a time across every agent here.`);
|
|
8069
8174
|
log.info(`Working folders an agent may be given here: ${allowedRootsOf(cfg).join(", ")} (add more under "allowedRoots" in ${CONFIG_PATH}).`);
|
|
8070
|
-
const
|
|
8071
|
-
|
|
8175
|
+
const probes = probeAllRuntimes(DEFAULT_RUNTIME_PROBES, cfg);
|
|
8176
|
+
for (const runtime of RUNTIME_ORDER) {
|
|
8177
|
+
const probe = probes[runtime];
|
|
8178
|
+
log.info(`${RUNTIME_LABELS[runtime]}: ${probe.version ?? (probe.path ? "unknown version" : "not installed")} \u2014 ${probe.detail}`);
|
|
8179
|
+
}
|
|
8072
8180
|
const loc = runLocation();
|
|
8073
8181
|
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"}.`);
|
|
8074
8182
|
if (platform6() === "darwin") log.info(`Runs at login: ${launchdLoaded() ? "yes (launchd)" : "no \u2014 run install"}`);
|
|
8075
|
-
const codexPath = cfg.codexPath ?? findCodex();
|
|
8076
|
-
const codexCheck = codexPath ? CodexAdapter.check(codexPath) : null;
|
|
8077
|
-
log.info(`Codex: ${codexCheck ? `${codexCheck.version ?? "unknown"} \u2014 ${codexCheck.detail}` : CodexAdapter.notYet}`);
|
|
8078
|
-
const grokPath = cfg.grokPath ?? findGrok();
|
|
8079
|
-
const grokCheck = grokPath ? GrokAdapter.check(grokPath) : null;
|
|
8080
|
-
log.info(`Grok Build: ${grokCheck ? `${grokCheck.version ?? "unknown"} \u2014 ${grokCheck.detail}` : GrokAdapter.notYet}`);
|
|
8081
8183
|
log.info("Which agent runs on which runtime is the platform's to say \u2014 attach or detach agents in Mission Control.");
|
|
8082
8184
|
await reportGitAccess(arg("--repo", argv));
|
|
8083
8185
|
const token = readToken(cfg.keychainAccount, cfg.tokenStore);
|
|
@@ -8212,10 +8314,19 @@ async function main() {
|
|
|
8212
8314
|
process.exit(cmd ? 2 : 0);
|
|
8213
8315
|
}
|
|
8214
8316
|
}
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
process.
|
|
8218
|
-
}
|
|
8317
|
+
var isEntrypoint = false;
|
|
8318
|
+
try {
|
|
8319
|
+
isEntrypoint = !!process.argv[1] && realpathSync4(process.argv[1]) === realpathSync4(fileURLToPath2(import.meta.url));
|
|
8320
|
+
} catch {
|
|
8321
|
+
}
|
|
8322
|
+
if (isEntrypoint) {
|
|
8323
|
+
main().catch((err) => {
|
|
8324
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
8325
|
+
process.exit(1);
|
|
8326
|
+
});
|
|
8327
|
+
}
|
|
8219
8328
|
export {
|
|
8329
|
+
RUNTIME_PREFLIGHT_REFUSAL,
|
|
8330
|
+
cmdJoin,
|
|
8220
8331
|
runtimeVersionString2 as runtimeVersionString
|
|
8221
8332
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frod.io/bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
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",
|
|
@@ -32,11 +32,14 @@
|
|
|
32
32
|
"pack:check": "npm pack --dry-run"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@eslint/js": "^9.39.3",
|
|
35
36
|
"@types/node": "^22.0.0",
|
|
36
37
|
"@types/ws": "^8.18.1",
|
|
37
38
|
"esbuild": "^0.27.3",
|
|
39
|
+
"eslint": "^9.0.0",
|
|
38
40
|
"tsx": "^4.19.0",
|
|
39
41
|
"typescript": "^5.7.0",
|
|
42
|
+
"typescript-eslint": "^8.56.1",
|
|
40
43
|
"vitest": "^4.0.18",
|
|
41
44
|
"ws": "^8.21.3"
|
|
42
45
|
}
|