@frod.io/bridge 0.7.3 → 0.7.5
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 +343 -134
- 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.5. 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
|
|
@@ -4089,6 +4090,25 @@ var LineBuffer = class {
|
|
|
4089
4090
|
// src/session-store.ts
|
|
4090
4091
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
|
|
4091
4092
|
import { join as join4 } from "node:path";
|
|
4093
|
+
|
|
4094
|
+
// src/adapters/usage-delta.ts
|
|
4095
|
+
var KEYS = ["inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens"];
|
|
4096
|
+
function usageDelta(current, previous) {
|
|
4097
|
+
if (!previous || KEYS.some((key) => current[key] < previous[key])) return { ...current };
|
|
4098
|
+
return {
|
|
4099
|
+
inputTokens: current.inputTokens - previous.inputTokens,
|
|
4100
|
+
outputTokens: current.outputTokens - previous.outputTokens,
|
|
4101
|
+
cacheCreationTokens: current.cacheCreationTokens - previous.cacheCreationTokens,
|
|
4102
|
+
cacheReadTokens: current.cacheReadTokens - previous.cacheReadTokens
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
function isUsageCounters(value) {
|
|
4106
|
+
if (!value || typeof value !== "object") return false;
|
|
4107
|
+
const row = value;
|
|
4108
|
+
return KEYS.every((key) => typeof row[key] === "number" && Number.isFinite(row[key]) && row[key] >= 0);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
// src/session-store.ts
|
|
4092
4112
|
var PATH = join4(FROD_DIR, "sessions.json");
|
|
4093
4113
|
function read() {
|
|
4094
4114
|
if (!existsSync3(PATH)) return {};
|
|
@@ -4106,6 +4126,9 @@ function write(store) {
|
|
|
4106
4126
|
function getStored(platformSessionId) {
|
|
4107
4127
|
return read()[platformSessionId] ?? null;
|
|
4108
4128
|
}
|
|
4129
|
+
function getStoredByHostSessionId(hostSessionId) {
|
|
4130
|
+
return Object.values(read()).filter((session) => session.hostSessionId === hostSessionId).sort((a, b) => Date.parse(b.lastPromptAt ?? b.startedAt) - Date.parse(a.lastPromptAt ?? a.startedAt))[0] ?? null;
|
|
4131
|
+
}
|
|
4109
4132
|
function putStored(platformSessionId, s) {
|
|
4110
4133
|
const store = read();
|
|
4111
4134
|
store[platformSessionId] = s;
|
|
@@ -4120,6 +4143,14 @@ function touchStored(platformSessionId) {
|
|
|
4120
4143
|
write(store);
|
|
4121
4144
|
}
|
|
4122
4145
|
}
|
|
4146
|
+
function putStoredUsage(platformSessionId, adapter, counters) {
|
|
4147
|
+
const store = read();
|
|
4148
|
+
const session = store[platformSessionId];
|
|
4149
|
+
if (!session || !isUsageCounters(counters)) return;
|
|
4150
|
+
session.cumulativeUsage = { adapter, counters: { ...counters } };
|
|
4151
|
+
session.lastPromptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4152
|
+
write(store);
|
|
4153
|
+
}
|
|
4123
4154
|
|
|
4124
4155
|
// src/log.ts
|
|
4125
4156
|
var REDACT = [
|
|
@@ -4482,24 +4513,23 @@ var ClaudeAdapter = class {
|
|
|
4482
4513
|
writeChildren([...this.live.values()].map((l) => l.child?.pid).filter((p) => typeof p === "number"));
|
|
4483
4514
|
}
|
|
4484
4515
|
/** `claude --version`, and whether the CLI says it is signed in. Never throws. */
|
|
4485
|
-
static check(claudePath) {
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
let
|
|
4516
|
+
static check(claudePath, commands = {}) {
|
|
4517
|
+
const versionCommand = commands.version ?? ((path) => execFileSync2(path, ["--version"], { env: cleanEnv(process.env), stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString());
|
|
4518
|
+
const authStatusCommand = commands.authStatus ?? ((path) => execFileSync2(path, ["auth", "status"], { env: cleanEnv(process.env), stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 }).toString());
|
|
4519
|
+
let version;
|
|
4489
4520
|
try {
|
|
4490
|
-
version =
|
|
4521
|
+
version = versionCommand(claudePath).trim();
|
|
4522
|
+
if (!/\d+\.\d+\.\d+/.test(version)) throw new Error("answered without a valid version");
|
|
4491
4523
|
} catch (err) {
|
|
4492
|
-
detail
|
|
4524
|
+
return { version: null, loggedIn: null, detail: `--version failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
4493
4525
|
}
|
|
4494
4526
|
try {
|
|
4495
|
-
const
|
|
4496
|
-
const
|
|
4497
|
-
loggedIn
|
|
4498
|
-
detail += `auth: ${parsed.authMethod ?? "?"}${parsed.subscriptionType ? ` (${parsed.subscriptionType})` : ""}. `;
|
|
4527
|
+
const parsed = JSON.parse(authStatusCommand(claudePath));
|
|
4528
|
+
const loggedIn = parsed.loggedIn === true ? true : parsed.loggedIn === false ? false : null;
|
|
4529
|
+
return { version, loggedIn, detail: `auth: ${parsed.authMethod ?? "?"}${parsed.subscriptionType ? ` (${parsed.subscriptionType})` : ""}` };
|
|
4499
4530
|
} catch (err) {
|
|
4500
|
-
detail
|
|
4531
|
+
return { version, loggedIn: null, detail: `auth status failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
4501
4532
|
}
|
|
4502
|
-
return { version, loggedIn, detail: detail.trim() };
|
|
4503
4533
|
}
|
|
4504
4534
|
async start(spec, o) {
|
|
4505
4535
|
if (!existsSync4(o.sessionDir)) mkdirSync2(o.sessionDir, { recursive: true, mode: 448 });
|
|
@@ -4860,6 +4890,8 @@ var CodexTranslator = class {
|
|
|
4860
4890
|
}
|
|
4861
4891
|
/** The newest `thread/tokenUsage/updated` of this turn, or null if none arrived. */
|
|
4862
4892
|
usage = null;
|
|
4893
|
+
/** The running thread counters immediately before this turn's first model call. */
|
|
4894
|
+
billingBase = null;
|
|
4863
4895
|
/** Item ids whose text was already streamed as deltas — not repeated on completion. */
|
|
4864
4896
|
streamed = /* @__PURE__ */ new Set();
|
|
4865
4897
|
/** Tool items announced with `item/started`, so a completion without a start still opens a card. */
|
|
@@ -4949,19 +4981,42 @@ var CodexTranslator = class {
|
|
|
4949
4981
|
}
|
|
4950
4982
|
onTokenUsage(p) {
|
|
4951
4983
|
const u = p.tokenUsage;
|
|
4952
|
-
if (u && typeof u === "object" && u.last && u.total)
|
|
4984
|
+
if (u && typeof u === "object" && u.last && u.total) {
|
|
4985
|
+
if (!this.billingBase) {
|
|
4986
|
+
const total = countersOf(u.total);
|
|
4987
|
+
const last = countersOf(u.last);
|
|
4988
|
+
if (total && last && countersCover(total, last)) this.billingBase = usageDelta(total, last);
|
|
4989
|
+
}
|
|
4990
|
+
this.usage = u;
|
|
4991
|
+
}
|
|
4953
4992
|
return true;
|
|
4954
4993
|
}
|
|
4955
4994
|
};
|
|
4956
|
-
function
|
|
4995
|
+
function countersOf(breakdown) {
|
|
4996
|
+
const inputTokens = num(breakdown.inputTokens);
|
|
4997
|
+
const outputTokens = num(breakdown.outputTokens);
|
|
4998
|
+
if (inputTokens === null || outputTokens === null) return null;
|
|
4999
|
+
return {
|
|
5000
|
+
inputTokens,
|
|
5001
|
+
outputTokens,
|
|
5002
|
+
cacheCreationTokens: num(breakdown.cacheWriteInputTokens) ?? 0,
|
|
5003
|
+
cacheReadTokens: num(breakdown.cachedInputTokens) ?? 0
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
function countersCover(total, part) {
|
|
5007
|
+
return total.inputTokens >= part.inputTokens && total.outputTokens >= part.outputTokens && total.cacheCreationTokens >= part.cacheCreationTokens && total.cacheReadTokens >= part.cacheReadTokens;
|
|
5008
|
+
}
|
|
5009
|
+
function toUsage(usage, model, billingBase = null) {
|
|
4957
5010
|
if (!usage || !model) return null;
|
|
4958
5011
|
const { last, total } = usage;
|
|
4959
|
-
|
|
5012
|
+
const cumulative = countersOf(total);
|
|
5013
|
+
if (!cumulative) return null;
|
|
5014
|
+
const turn = usageDelta(cumulative, billingBase);
|
|
4960
5015
|
return {
|
|
4961
|
-
input_tokens: Math.max(0,
|
|
4962
|
-
output_tokens:
|
|
4963
|
-
cache_creation_tokens:
|
|
4964
|
-
cache_read_tokens:
|
|
5016
|
+
input_tokens: Math.max(0, turn.inputTokens - turn.cacheReadTokens),
|
|
5017
|
+
output_tokens: turn.outputTokens,
|
|
5018
|
+
cache_creation_tokens: turn.cacheCreationTokens,
|
|
5019
|
+
cache_read_tokens: turn.cacheReadTokens,
|
|
4965
5020
|
model,
|
|
4966
5021
|
context_tokens: num(last.inputTokens),
|
|
4967
5022
|
context_window: num(usage.modelContextWindow)
|
|
@@ -5045,19 +5100,30 @@ var CodexAdapter = class _CodexAdapter {
|
|
|
5045
5100
|
return "The Codex adapter needs the `codex` CLI on this computer and a signed-in account (`codex login`).";
|
|
5046
5101
|
}
|
|
5047
5102
|
/** The version and whether somebody is signed in — the same shape ClaudeAdapter.check returns. */
|
|
5048
|
-
static check(codexPath) {
|
|
5049
|
-
|
|
5103
|
+
static check(codexPath, commands = {}) {
|
|
5104
|
+
const versionCommand = commands.version ?? ((path) => execFileSync3(path, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 2e4 }).toString());
|
|
5105
|
+
const loginStatusCommand = commands.loginStatus ?? ((path) => {
|
|
5106
|
+
const res = spawnSync(path, ["login", "status"], { encoding: "utf8", timeout: 3e4 });
|
|
5107
|
+
if (res.error) throw res.error;
|
|
5108
|
+
return `${res.stdout ?? ""}${res.stderr ?? ""}`;
|
|
5109
|
+
});
|
|
5110
|
+
let version;
|
|
5050
5111
|
try {
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5112
|
+
const raw = versionCommand(codexPath).trim();
|
|
5113
|
+
version = /(\d+\.\d+\.\d+[0-9A-Za-z.-]*)/.exec(raw)?.[1] ?? "";
|
|
5114
|
+
if (!version) throw new Error("answered without a valid version");
|
|
5115
|
+
} catch (err) {
|
|
5116
|
+
return { version: null, loggedIn: null, detail: `${codexPath} did not run \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
5054
5117
|
}
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
return { version, loggedIn: false, detail
|
|
5118
|
+
try {
|
|
5119
|
+
const out = loginStatusCommand(codexPath).trim();
|
|
5120
|
+
const detail = out.split("\n").find((line) => line.trim().length > 0)?.trim() || "could not read the sign-in state \u2014 run `codex login`";
|
|
5121
|
+
if (/\b(?:not logged in|not signed in|not authenticated|unauthenticated)\b/i.test(out)) return { version, loggedIn: false, detail };
|
|
5122
|
+
if (/\blogged in\b/i.test(out)) return { version, loggedIn: true, detail };
|
|
5123
|
+
return { version, loggedIn: null, detail };
|
|
5124
|
+
} catch (err) {
|
|
5125
|
+
return { version, loggedIn: null, detail: `could not read the sign-in state: ${err instanceof Error ? err.message : String(err)}` };
|
|
5059
5126
|
}
|
|
5060
|
-
return { version, loggedIn: /logged in/i.test(out), detail: out.split("\n")[0] ?? "" };
|
|
5061
5127
|
}
|
|
5062
5128
|
/**
|
|
5063
5129
|
* A session's credential tier. `codex login status` says how the person
|
|
@@ -5221,7 +5287,8 @@ var CodexAdapter = class _CodexAdapter {
|
|
|
5221
5287
|
const status2 = typeof turn.status === "string" ? turn.status : "completed";
|
|
5222
5288
|
const err = turn.error;
|
|
5223
5289
|
const stopReason = status2 === "interrupted" ? "cancelled" : status2 === "failed" ? "error" : "end_turn";
|
|
5224
|
-
const
|
|
5290
|
+
const translator = live.turn?.translator;
|
|
5291
|
+
const usage = toUsage(translator?.usage ?? null, live.model, translator?.billingBase ?? null);
|
|
5225
5292
|
live.turnId = null;
|
|
5226
5293
|
this.settleTurn(live, {
|
|
5227
5294
|
stopReason,
|
|
@@ -5560,19 +5627,29 @@ function stopReasonFor(acp) {
|
|
|
5560
5627
|
}
|
|
5561
5628
|
}
|
|
5562
5629
|
var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
5563
|
-
function
|
|
5630
|
+
function cumulativeUsageOf(meta) {
|
|
5564
5631
|
const total = meta?.usage;
|
|
5632
|
+
if (!total) return null;
|
|
5633
|
+
const inputTokens = num2(total.inputTokens);
|
|
5634
|
+
const outputTokens = num2(total.outputTokens);
|
|
5635
|
+
if (inputTokens === null || outputTokens === null) return null;
|
|
5636
|
+
return {
|
|
5637
|
+
inputTokens,
|
|
5638
|
+
outputTokens,
|
|
5639
|
+
cacheCreationTokens: num2(total.cacheCreationTokens) ?? 0,
|
|
5640
|
+
cacheReadTokens: num2(total.cachedReadTokens) ?? 0
|
|
5641
|
+
};
|
|
5642
|
+
}
|
|
5643
|
+
function toUsage2(meta, fallbackModel, contextWindow, previous = null) {
|
|
5565
5644
|
const model = str2(meta?.modelId) ?? fallbackModel;
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
const
|
|
5569
|
-
if (input === null || output === null) return null;
|
|
5570
|
-
const cacheRead = num2(total.cachedReadTokens) ?? 0;
|
|
5645
|
+
const cumulative = cumulativeUsageOf(meta);
|
|
5646
|
+
if (!cumulative || !model) return null;
|
|
5647
|
+
const turn = usageDelta(cumulative, previous);
|
|
5571
5648
|
return {
|
|
5572
|
-
input_tokens: Math.max(0,
|
|
5573
|
-
output_tokens:
|
|
5574
|
-
cache_creation_tokens:
|
|
5575
|
-
cache_read_tokens:
|
|
5649
|
+
input_tokens: Math.max(0, turn.inputTokens - turn.cacheReadTokens),
|
|
5650
|
+
output_tokens: turn.outputTokens,
|
|
5651
|
+
cache_creation_tokens: turn.cacheCreationTokens,
|
|
5652
|
+
cache_read_tokens: turn.cacheReadTokens,
|
|
5576
5653
|
model,
|
|
5577
5654
|
context_tokens: num2(meta?.inputTokens),
|
|
5578
5655
|
context_window: contextWindow
|
|
@@ -5754,23 +5831,32 @@ var GrokAdapter = class _GrokAdapter {
|
|
|
5754
5831
|
* says "You are not authenticated." and opens no browser, so this is safe
|
|
5755
5832
|
* to run unattended at every start.
|
|
5756
5833
|
*/
|
|
5757
|
-
static check(grokPath) {
|
|
5834
|
+
static check(grokPath, commands = {}) {
|
|
5758
5835
|
const env = { ...process.env, GROK_DISABLE_AUTOUPDATER: "1" };
|
|
5759
|
-
|
|
5836
|
+
const versionCommand = commands.version ?? ((path) => execFileSync4(path, ["--version"], { env, stdio: ["ignore", "pipe", "ignore"], timeout: 2e4 }).toString());
|
|
5837
|
+
const modelsCommand = commands.models ?? ((path) => {
|
|
5838
|
+
const res = spawnSync2(path, ["models"], { env, encoding: "utf8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
|
|
5839
|
+
if (res.error) throw res.error;
|
|
5840
|
+
return `${res.stdout ?? ""}
|
|
5841
|
+
${res.stderr ?? ""}`;
|
|
5842
|
+
});
|
|
5843
|
+
let version;
|
|
5760
5844
|
try {
|
|
5761
|
-
const
|
|
5762
|
-
version = /(\d+\.\d+\.\d+[0-9A-Za-z.-]*)/.exec(
|
|
5763
|
-
|
|
5764
|
-
|
|
5845
|
+
const out = versionCommand(grokPath);
|
|
5846
|
+
version = /(\d+\.\d+\.\d+[0-9A-Za-z.-]*)/.exec(out)?.[1] ?? "";
|
|
5847
|
+
if (!version) throw new Error("answered without a valid version");
|
|
5848
|
+
} catch (err) {
|
|
5849
|
+
return { version: null, loggedIn: null, detail: `${grokPath} did not run \u2014 ${err instanceof Error ? err.message : String(err)}` };
|
|
5850
|
+
}
|
|
5851
|
+
try {
|
|
5852
|
+
const out = modelsCommand(grokPath).trim();
|
|
5853
|
+
const first = out.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
|
|
5854
|
+
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`" };
|
|
5855
|
+
if (/\blogged in\b/i.test(out)) return { version, loggedIn: true, detail: first };
|
|
5856
|
+
return { version, loggedIn: null, detail: first || "could not read the sign-in state \u2014 run `grok login`" };
|
|
5857
|
+
} catch (err) {
|
|
5858
|
+
return { version, loggedIn: null, detail: `could not read the sign-in state: ${err instanceof Error ? err.message : String(err)}` };
|
|
5765
5859
|
}
|
|
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
5860
|
}
|
|
5775
5861
|
/** A grok.com sign-in is the subscription; anything else that runs is a key (`XAI_API_KEY`). */
|
|
5776
5862
|
credentialTier() {
|
|
@@ -5789,6 +5875,8 @@ ${res.stderr ?? ""}`.trim();
|
|
|
5789
5875
|
turn: null,
|
|
5790
5876
|
model: spec.model?.name ?? null,
|
|
5791
5877
|
contextWindow: null,
|
|
5878
|
+
cumulativeUsage: null,
|
|
5879
|
+
needsUsageBaseline: false,
|
|
5792
5880
|
onExit: o.onExit,
|
|
5793
5881
|
askPermission: o.askPermission,
|
|
5794
5882
|
reportAutoAllowed: o.reportAutoAllowed,
|
|
@@ -5819,6 +5907,10 @@ ${res.stderr ?? ""}`.trim();
|
|
|
5819
5907
|
const mcpServers = mcpServersFor(spec);
|
|
5820
5908
|
const params = { cwd: o.sessionDir, mcpServers, _meta: sessionMeta(spec) };
|
|
5821
5909
|
const resumeId = spec.resume_session_id ?? null;
|
|
5910
|
+
const resumedStore = resumeId ? getStoredByHostSessionId(resumeId) : null;
|
|
5911
|
+
if (resumedStore?.cumulativeUsage?.adapter === "grok" && isUsageCounters(resumedStore.cumulativeUsage.counters)) {
|
|
5912
|
+
live.cumulativeUsage = { ...resumedStore.cumulativeUsage.counters };
|
|
5913
|
+
}
|
|
5822
5914
|
let resumed = false;
|
|
5823
5915
|
let started;
|
|
5824
5916
|
if (resumeId) {
|
|
@@ -5830,12 +5922,25 @@ ${res.stderr ?? ""}`.trim();
|
|
|
5830
5922
|
}
|
|
5831
5923
|
}
|
|
5832
5924
|
if (!started) started = await this.request(live, "session/new", params);
|
|
5925
|
+
if (!resumed) live.cumulativeUsage = null;
|
|
5926
|
+
live.needsUsageBaseline = resumed && !live.cumulativeUsage;
|
|
5833
5927
|
const sessionId = typeof started?.sessionId === "string" ? started.sessionId : null;
|
|
5834
5928
|
if (!sessionId) throw new Error("Grok Build started but did not name the session \u2014 it cannot be addressed.");
|
|
5835
5929
|
live.acpSessionId = sessionId;
|
|
5836
5930
|
const models = started?.models ?? init?._meta?.modelState ?? null;
|
|
5837
5931
|
live.model = live.model ?? (typeof models?.currentModelId === "string" ? models.currentModelId : null);
|
|
5838
5932
|
live.contextWindow = contextWindowOf(models, live.model);
|
|
5933
|
+
try {
|
|
5934
|
+
putStored(spec.session_id, {
|
|
5935
|
+
hostSessionId: sessionId,
|
|
5936
|
+
cwd: o.sessionDir,
|
|
5937
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5938
|
+
lastPromptAt: null,
|
|
5939
|
+
...resumed && live.cumulativeUsage ? { cumulativeUsage: { adapter: "grok", counters: { ...live.cumulativeUsage } } } : {}
|
|
5940
|
+
});
|
|
5941
|
+
} catch (err) {
|
|
5942
|
+
log.warn(`[${spec.session_id.slice(0, 8)}] grok: could not open the usage-counter record (${err instanceof Error ? err.message : String(err)})`);
|
|
5943
|
+
}
|
|
5839
5944
|
log.event("grok.started", {
|
|
5840
5945
|
session: spec.session_id,
|
|
5841
5946
|
acp_session: sessionId,
|
|
@@ -5860,7 +5965,21 @@ ${res.stderr ?? ""}`.trim();
|
|
|
5860
5965
|
const meta = r._meta ?? null;
|
|
5861
5966
|
if (typeof meta?.modelId === "string" && meta.modelId) live.model = meta.modelId;
|
|
5862
5967
|
const { stopReason, error } = stopReasonFor(r.stopReason);
|
|
5863
|
-
|
|
5968
|
+
const cumulative = cumulativeUsageOf(meta);
|
|
5969
|
+
const usage = live.needsUsageBaseline ? null : toUsage2(meta, live.model, live.contextWindow, live.cumulativeUsage);
|
|
5970
|
+
if (cumulative) {
|
|
5971
|
+
if (live.needsUsageBaseline) {
|
|
5972
|
+
log.warn(`[${live.spec.session_id.slice(0, 8)}] grok: resumed without an earlier usage snapshot \u2014 this turn is unpriced and seeds the next increment`);
|
|
5973
|
+
live.needsUsageBaseline = false;
|
|
5974
|
+
}
|
|
5975
|
+
live.cumulativeUsage = cumulative;
|
|
5976
|
+
try {
|
|
5977
|
+
putStoredUsage(live.spec.session_id, "grok", cumulative);
|
|
5978
|
+
} catch (err) {
|
|
5979
|
+
log.warn(`[${live.spec.session_id.slice(0, 8)}] grok: could not remember usage counters (${err instanceof Error ? err.message : String(err)})`);
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
this.settleTurn(live, { stopReason, usage, ...error ? { error } : {} });
|
|
5864
5983
|
}).catch((err) => {
|
|
5865
5984
|
this.settleTurn(live, { stopReason: "error", usage: null, error: err.message });
|
|
5866
5985
|
});
|
|
@@ -7770,6 +7889,8 @@ function askChild(input) {
|
|
|
7770
7889
|
} catch {
|
|
7771
7890
|
}
|
|
7772
7891
|
};
|
|
7892
|
+
child.stdin?.on("error", () => {
|
|
7893
|
+
});
|
|
7773
7894
|
child.on("error", (err) => finish({ problem: `could not start: ${err.message}` }));
|
|
7774
7895
|
child.on("exit", (code) => {
|
|
7775
7896
|
if (!done) finish({ problem: `ended (exit ${code ?? "signal"}) before answering` });
|
|
@@ -7829,8 +7950,56 @@ function samplesInWords(samples) {
|
|
|
7829
7950
|
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";
|
|
7830
7951
|
}
|
|
7831
7952
|
|
|
7953
|
+
// package.json
|
|
7954
|
+
var package_default = {
|
|
7955
|
+
name: "@frod.io/bridge",
|
|
7956
|
+
version: "0.7.5",
|
|
7957
|
+
type: "module",
|
|
7958
|
+
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
|
+
license: "UNLICENSED",
|
|
7960
|
+
homepage: "https://frod.io",
|
|
7961
|
+
repository: {
|
|
7962
|
+
type: "git",
|
|
7963
|
+
url: "https://github.com/awafeek/agentic-engineering.git",
|
|
7964
|
+
directory: "apps/bridge"
|
|
7965
|
+
},
|
|
7966
|
+
bin: {
|
|
7967
|
+
"frod-bridge": "dist/cli.js"
|
|
7968
|
+
},
|
|
7969
|
+
files: [
|
|
7970
|
+
"dist",
|
|
7971
|
+
"README.md"
|
|
7972
|
+
],
|
|
7973
|
+
engines: {
|
|
7974
|
+
node: ">=20"
|
|
7975
|
+
},
|
|
7976
|
+
publishConfig: {
|
|
7977
|
+
access: "public"
|
|
7978
|
+
},
|
|
7979
|
+
scripts: {
|
|
7980
|
+
build: "node scripts/bundle.mjs && tsc --noEmit",
|
|
7981
|
+
bundle: "node scripts/bundle.mjs",
|
|
7982
|
+
lint: "eslint src",
|
|
7983
|
+
test: "vitest run --passWithNoTests",
|
|
7984
|
+
"type-check": "tsc --noEmit",
|
|
7985
|
+
"pack:check": "npm pack --dry-run"
|
|
7986
|
+
},
|
|
7987
|
+
devDependencies: {
|
|
7988
|
+
"@eslint/js": "^9.39.3",
|
|
7989
|
+
"@types/node": "^22.0.0",
|
|
7990
|
+
"@types/ws": "^8.18.1",
|
|
7991
|
+
esbuild: "^0.27.3",
|
|
7992
|
+
eslint: "^9.0.0",
|
|
7993
|
+
tsx: "^4.19.0",
|
|
7994
|
+
typescript: "^5.7.0",
|
|
7995
|
+
"typescript-eslint": "^8.56.1",
|
|
7996
|
+
vitest: "^4.0.18",
|
|
7997
|
+
ws: "^8.21.3"
|
|
7998
|
+
}
|
|
7999
|
+
};
|
|
8000
|
+
|
|
7832
8001
|
// src/cli.ts
|
|
7833
|
-
var VERSION =
|
|
8002
|
+
var VERSION = package_default.version;
|
|
7834
8003
|
function arg(flag, argv) {
|
|
7835
8004
|
const i = argv.indexOf(flag);
|
|
7836
8005
|
return i >= 0 ? argv[i + 1] : void 0;
|
|
@@ -7842,7 +8011,7 @@ function findClaude() {
|
|
|
7842
8011
|
return execFileSync7("/usr/bin/which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
7843
8012
|
} catch {
|
|
7844
8013
|
}
|
|
7845
|
-
|
|
8014
|
+
return null;
|
|
7846
8015
|
}
|
|
7847
8016
|
function findCodex() {
|
|
7848
8017
|
const candidates = [process.env.FROD_CODEX_PATH, join11(homedir7(), ".local", "bin", "codex"), "/opt/homebrew/bin/codex", "/usr/local/bin/codex"].filter((c) => !!c);
|
|
@@ -7864,33 +8033,54 @@ function findGrok() {
|
|
|
7864
8033
|
return null;
|
|
7865
8034
|
}
|
|
7866
8035
|
}
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
8036
|
+
var RUNTIME_ORDER = ["claude", "codex", "grok"];
|
|
8037
|
+
var RUNTIME_LABELS = { claude: "Claude Code", codex: "Codex", grok: "Grok Build" };
|
|
8038
|
+
var INSTALL_HINTS = { claude: "Install Claude Code.", codex: "Install Codex.", grok: "Install Grok Build." };
|
|
8039
|
+
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`).";
|
|
8040
|
+
function checkedRuntime(name, path) {
|
|
8041
|
+
if (!path) return { path: null, version: null, loggedIn: null, detail: `not installed on this computer` };
|
|
8042
|
+
try {
|
|
8043
|
+
const check = name === "claude" ? ClaudeAdapter.check(path) : name === "codex" ? CodexAdapter.check(path) : GrokAdapter.check(path);
|
|
8044
|
+
return { path, ...check };
|
|
8045
|
+
} catch (err) {
|
|
8046
|
+
return { path, version: null, loggedIn: null, detail: `could not run: ${err instanceof Error ? err.message : String(err)}` };
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
var DEFAULT_RUNTIME_PROBES = {
|
|
8050
|
+
claude: (preferredPath) => checkedRuntime("claude", preferredPath ?? findClaude()),
|
|
8051
|
+
codex: (preferredPath) => checkedRuntime("codex", preferredPath ?? findCodex()),
|
|
8052
|
+
grok: (preferredPath) => checkedRuntime("grok", preferredPath ?? findGrok())
|
|
8053
|
+
};
|
|
8054
|
+
function probeAllRuntimes(probes, preferred) {
|
|
8055
|
+
return {
|
|
8056
|
+
claude: probes.claude(preferred?.claudePath),
|
|
8057
|
+
codex: probes.codex(preferred?.codexPath),
|
|
8058
|
+
grok: probes.grok(preferred?.grokPath)
|
|
7870
8059
|
};
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
}
|
|
7893
|
-
|
|
8060
|
+
}
|
|
8061
|
+
function isReady(probe) {
|
|
8062
|
+
return probe.path !== null && probe.version !== null && probe.loggedIn === true;
|
|
8063
|
+
}
|
|
8064
|
+
function readyNames(probes) {
|
|
8065
|
+
return RUNTIME_ORDER.filter((name) => isReady(probes[name]));
|
|
8066
|
+
}
|
|
8067
|
+
function runtimePreflightRefusal(probes) {
|
|
8068
|
+
const hints = RUNTIME_ORDER.filter((name) => probes[name].path === null).map((name) => INSTALL_HINTS[name]);
|
|
8069
|
+
return `${RUNTIME_PREFLIGHT_REFUSAL}${hints.length > 0 ? ` ${hints.join(" ")}` : ""}`;
|
|
8070
|
+
}
|
|
8071
|
+
function versionForReadySet(probes) {
|
|
8072
|
+
return runtimeVersionString2(
|
|
8073
|
+
isReady(probes.claude) ? probes.claude.version : null,
|
|
8074
|
+
isReady(probes.codex) ? probes.codex.version : null,
|
|
8075
|
+
isReady(probes.grok) ? probes.grok.version : null
|
|
8076
|
+
);
|
|
8077
|
+
}
|
|
8078
|
+
function buildAdapters(probes, permissions) {
|
|
8079
|
+
const adapters = {};
|
|
8080
|
+
if (isReady(probes.claude)) adapters.claude = new ClaudeAdapter({ claudePath: probes.claude.path, permissions });
|
|
8081
|
+
if (isReady(probes.codex)) adapters.codex = new CodexAdapter({ codexPath: probes.codex.path });
|
|
8082
|
+
if (isReady(probes.grok)) adapters.grok = new GrokAdapter({ grokPath: probes.grok.path });
|
|
8083
|
+
return { adapters, names: readyNames(probes) };
|
|
7894
8084
|
}
|
|
7895
8085
|
function runtimeVersionString2(claudeVersion, codexVersion, grokVersion = null) {
|
|
7896
8086
|
return runtimeVersionString(VERSION, claudeVersion, codexVersion, grokVersion);
|
|
@@ -7904,37 +8094,42 @@ function tokenFor(cfg) {
|
|
|
7904
8094
|
if (!token) throw new Error("The host token is missing from the keychain. Ask for a new join code and join again.");
|
|
7905
8095
|
return token;
|
|
7906
8096
|
}
|
|
7907
|
-
async function cmdJoin(argv) {
|
|
8097
|
+
async function cmdJoin(argv, overrides = {}) {
|
|
8098
|
+
const deps = {
|
|
8099
|
+
readConfig,
|
|
8100
|
+
probes: DEFAULT_RUNTIME_PROBES,
|
|
8101
|
+
join: join3,
|
|
8102
|
+
storeToken,
|
|
8103
|
+
ensureWorkdir: (path) => {
|
|
8104
|
+
if (!existsSync12(path)) mkdirSync9(path, { recursive: true, mode: 448 });
|
|
8105
|
+
},
|
|
8106
|
+
writeConfig,
|
|
8107
|
+
reportGitAccess,
|
|
8108
|
+
install: cmdInstall,
|
|
8109
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
8110
|
+
...overrides
|
|
8111
|
+
};
|
|
7908
8112
|
const code = argv[0];
|
|
7909
8113
|
const apiUrl = arg("--api", argv) ?? process.env.FROD_API_URL;
|
|
7910
8114
|
if (!code || !apiUrl) throw new Error(`Usage: join <code> --api https://api.agentic.frod.io [--name "Wafeek's Mac"] [--workdir <dir>] [--max-sessions N]`);
|
|
7911
|
-
const existing = readConfig();
|
|
8115
|
+
const existing = deps.readConfig();
|
|
7912
8116
|
const refusal = existing ? secondJoinWords(existing) : null;
|
|
7913
8117
|
if (refusal) throw new Error(refusal);
|
|
7914
8118
|
const safeApi = assertSafeEndpoint(apiUrl);
|
|
7915
8119
|
const name = arg("--name", argv) ?? defaultName();
|
|
7916
8120
|
const workdir = resolve6(arg("--workdir", argv) ?? join11(homedir7(), "frod-work"));
|
|
7917
8121
|
const maxSessions = Math.max(1, Math.min(20, Number(arg("--max-sessions", argv) ?? 2)));
|
|
7918
|
-
const
|
|
7919
|
-
const
|
|
7920
|
-
|
|
7921
|
-
|
|
8122
|
+
const probes = probeAllRuntimes(deps.probes);
|
|
8123
|
+
for (const runtime of RUNTIME_ORDER) {
|
|
8124
|
+
const probe = probes[runtime];
|
|
8125
|
+
log.info(`${RUNTIME_LABELS[runtime]}: ${probe.version ?? (probe.path ? "unknown version" : "not installed")} \u2014 ${probe.detail}`);
|
|
8126
|
+
}
|
|
8127
|
+
const adapters = readyNames(probes);
|
|
8128
|
+
if (adapters.length === 0) throw new Error(runtimePreflightRefusal(probes));
|
|
7922
8129
|
log.info(`Joining ${safeApi} as "${name}"\u2026`);
|
|
7923
|
-
const
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
const c = CodexAdapter.check(cp);
|
|
7927
|
-
return c.loggedIn ? c.version : null;
|
|
7928
|
-
})();
|
|
7929
|
-
const grokAtJoin = (() => {
|
|
7930
|
-
const gp = findGrok();
|
|
7931
|
-
if (!gp) return null;
|
|
7932
|
-
const g = GrokAdapter.check(gp);
|
|
7933
|
-
return g.loggedIn ? g.version : null;
|
|
7934
|
-
})();
|
|
7935
|
-
const joined = await join3(safeApi, code, name, { os: platform6(), version: runtimeVersionString2(check.version, codexAtJoin, grokAtJoin) });
|
|
7936
|
-
const store = storeToken(joined.hostId, joined.token);
|
|
7937
|
-
if (!existsSync12(workdir)) mkdirSync9(workdir, { recursive: true, mode: 448 });
|
|
8130
|
+
const joined = await deps.join(safeApi, code, name, { os: platform6(), version: versionForReadySet(probes) });
|
|
8131
|
+
const store = deps.storeToken(joined.hostId, joined.token);
|
|
8132
|
+
deps.ensureWorkdir(workdir);
|
|
7938
8133
|
const cfg = {
|
|
7939
8134
|
apiUrl: safeApi,
|
|
7940
8135
|
hostId: joined.hostId,
|
|
@@ -7944,26 +8139,28 @@ async function cmdJoin(argv) {
|
|
|
7944
8139
|
name,
|
|
7945
8140
|
workdir,
|
|
7946
8141
|
maxSessions,
|
|
7947
|
-
adapters
|
|
7948
|
-
claudePath,
|
|
8142
|
+
adapters,
|
|
8143
|
+
claudePath: isReady(probes.claude) ? probes.claude.path : null,
|
|
8144
|
+
codexPath: isReady(probes.codex) ? probes.codex.path : void 0,
|
|
8145
|
+
grokPath: isReady(probes.grok) ? probes.grok.path : void 0,
|
|
7949
8146
|
tokenStore: store,
|
|
7950
8147
|
keychainAccount: joined.hostId,
|
|
7951
|
-
joinedAt:
|
|
8148
|
+
joinedAt: deps.now(),
|
|
7952
8149
|
dormant: null,
|
|
7953
8150
|
bindings: joined.bindings ?? []
|
|
7954
8151
|
};
|
|
7955
|
-
writeConfig(cfg);
|
|
8152
|
+
deps.writeConfig(cfg);
|
|
7956
8153
|
log.info(`Joined. This computer is now "${name}" \u2014 ${joined.person?.name ?? "your"} computer in ${joined.workspaceName ?? joined.workspaceId} \u2014 hosting ${hostedAgentsWords(cfg)}.`);
|
|
7957
8154
|
for (const b of cfg.bindings ?? []) log.info(` ${b.agentName}: ${bindingWords(b)}`);
|
|
7958
8155
|
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)}.`);
|
|
7959
8156
|
log.info(`Settings: ${CONFIG_PATH}. Sessions work under ${workdir}/sessions. Up to ${maxSessions} at a time, across every agent this computer hosts.`);
|
|
7960
8157
|
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.`);
|
|
7961
|
-
await reportGitAccess(arg("--repo", argv));
|
|
8158
|
+
await deps.reportGitAccess(arg("--repo", argv));
|
|
7962
8159
|
if (argv.includes("--no-install")) {
|
|
7963
8160
|
log.info("Next: `install` to run at login, or `run` to run now.");
|
|
7964
8161
|
return;
|
|
7965
8162
|
}
|
|
7966
|
-
await
|
|
8163
|
+
await deps.install();
|
|
7967
8164
|
}
|
|
7968
8165
|
async function reportGitAccess(repoUrl) {
|
|
7969
8166
|
let version = "not found";
|
|
@@ -7999,24 +8196,30 @@ async function cmdRun() {
|
|
|
7999
8196
|
ClaudeAdapter.reapOrphans();
|
|
8000
8197
|
const pruned = pruneRunDirs(cfg.workdir);
|
|
8001
8198
|
if (pruned > 0) log.event("repo.runs_pruned", { count: pruned });
|
|
8199
|
+
const probes = probeAllRuntimes(DEFAULT_RUNTIME_PROBES, cfg);
|
|
8200
|
+
const ready = readyNames(probes);
|
|
8201
|
+
if (ready.length === 0) throw new Error(runtimePreflightRefusal(probes));
|
|
8002
8202
|
const permissions = new PermissionServer();
|
|
8003
8203
|
await permissions.start();
|
|
8004
|
-
const { adapters, names
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8204
|
+
const { adapters, names } = buildAdapters(probes, permissions);
|
|
8205
|
+
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(". ")}.`);
|
|
8206
|
+
const runtimeConfigBefore = JSON.stringify({ adapters: cfg.adapters ?? [], claudePath: cfg.claudePath, codexPath: cfg.codexPath, grokPath: cfg.grokPath });
|
|
8207
|
+
cfg.adapters = names;
|
|
8208
|
+
cfg.claudePath = isReady(probes.claude) ? probes.claude.path : null;
|
|
8209
|
+
cfg.codexPath = isReady(probes.codex) ? probes.codex.path : void 0;
|
|
8210
|
+
cfg.grokPath = isReady(probes.grok) ? probes.grok.path : void 0;
|
|
8211
|
+
if (runtimeConfigBefore !== JSON.stringify({ adapters: cfg.adapters, claudePath: cfg.claudePath, codexPath: cfg.codexPath, grokPath: cfg.grokPath })) {
|
|
8009
8212
|
writeConfig(cfg);
|
|
8010
8213
|
}
|
|
8011
|
-
log.event("bridge.start", { version: VERSION, name: cfg.name, host_id: cfg.hostId, agents: (cfg.bindings ?? []).map((b) => b.agentName), api: cfg.apiUrl, claude:
|
|
8012
|
-
const codexPath =
|
|
8013
|
-
const grokPath =
|
|
8214
|
+
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 });
|
|
8215
|
+
const codexPath = isReady(probes.codex) ? probes.codex.path : null;
|
|
8216
|
+
const grokPath = isReady(probes.grok) ? probes.grok.path : null;
|
|
8014
8217
|
let sampling = false;
|
|
8015
8218
|
const sampleAndSend = async () => {
|
|
8016
8219
|
if (sampling) return;
|
|
8017
8220
|
sampling = true;
|
|
8018
8221
|
try {
|
|
8019
|
-
const { at, samples } = await sampleCapacity({ claude:
|
|
8222
|
+
const { at, samples } = await sampleCapacity({ claude: isReady(probes.claude), codexPath, grokPath });
|
|
8020
8223
|
conn.sendHost("host/capacity", { at, samples });
|
|
8021
8224
|
log.event("capacity.sampled", { at, words: samplesInWords(samples) });
|
|
8022
8225
|
} catch (err) {
|
|
@@ -8061,7 +8264,7 @@ async function cmdRun() {
|
|
|
8061
8264
|
config: cfg,
|
|
8062
8265
|
token,
|
|
8063
8266
|
adapters,
|
|
8064
|
-
version:
|
|
8267
|
+
version: versionForReadySet(probes),
|
|
8065
8268
|
onFinal: (reason2) => {
|
|
8066
8269
|
writeConfig({ ...cfg, dormant: { reason: reason2, at: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
8067
8270
|
log.warn(dormantWords({ reason: reason2, at: (/* @__PURE__ */ new Date()).toISOString() }, hostedAgentsWords(cfg)));
|
|
@@ -8118,17 +8321,14 @@ async function cmdStatus(argv) {
|
|
|
8118
8321
|
if (cfg.dormant) log.warn(dormantWords(cfg.dormant, hostedAgentsWords(cfg)));
|
|
8119
8322
|
log.info(`Token: ${cfg.tokenStore}; settings ${CONFIG_PATH}; sessions under ${cfg.workdir}/sessions; up to ${cfg.maxSessions} at a time across every agent here.`);
|
|
8120
8323
|
log.info(`Working folders an agent may be given here: ${allowedRootsOf(cfg).join(", ")} (add more under "allowedRoots" in ${CONFIG_PATH}).`);
|
|
8121
|
-
const
|
|
8122
|
-
|
|
8324
|
+
const probes = probeAllRuntimes(DEFAULT_RUNTIME_PROBES, cfg);
|
|
8325
|
+
for (const runtime of RUNTIME_ORDER) {
|
|
8326
|
+
const probe = probes[runtime];
|
|
8327
|
+
log.info(`${RUNTIME_LABELS[runtime]}: ${probe.version ?? (probe.path ? "unknown version" : "not installed")} \u2014 ${probe.detail}`);
|
|
8328
|
+
}
|
|
8123
8329
|
const loc = runLocation();
|
|
8124
8330
|
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"}.`);
|
|
8125
8331
|
if (platform6() === "darwin") log.info(`Runs at login: ${launchdLoaded() ? "yes (launchd)" : "no \u2014 run install"}`);
|
|
8126
|
-
const codexPath = cfg.codexPath ?? findCodex();
|
|
8127
|
-
const codexCheck = codexPath ? CodexAdapter.check(codexPath) : null;
|
|
8128
|
-
log.info(`Codex: ${codexCheck ? `${codexCheck.version ?? "unknown"} \u2014 ${codexCheck.detail}` : CodexAdapter.notYet}`);
|
|
8129
|
-
const grokPath = cfg.grokPath ?? findGrok();
|
|
8130
|
-
const grokCheck = grokPath ? GrokAdapter.check(grokPath) : null;
|
|
8131
|
-
log.info(`Grok Build: ${grokCheck ? `${grokCheck.version ?? "unknown"} \u2014 ${grokCheck.detail}` : GrokAdapter.notYet}`);
|
|
8132
8332
|
log.info("Which agent runs on which runtime is the platform's to say \u2014 attach or detach agents in Mission Control.");
|
|
8133
8333
|
await reportGitAccess(arg("--repo", argv));
|
|
8134
8334
|
const token = readToken(cfg.keychainAccount, cfg.tokenStore);
|
|
@@ -8263,10 +8463,19 @@ async function main() {
|
|
|
8263
8463
|
process.exit(cmd ? 2 : 0);
|
|
8264
8464
|
}
|
|
8265
8465
|
}
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
process.
|
|
8269
|
-
}
|
|
8466
|
+
var isEntrypoint = false;
|
|
8467
|
+
try {
|
|
8468
|
+
isEntrypoint = !!process.argv[1] && realpathSync4(process.argv[1]) === realpathSync4(fileURLToPath2(import.meta.url));
|
|
8469
|
+
} catch {
|
|
8470
|
+
}
|
|
8471
|
+
if (isEntrypoint) {
|
|
8472
|
+
main().catch((err) => {
|
|
8473
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
8474
|
+
process.exit(1);
|
|
8475
|
+
});
|
|
8476
|
+
}
|
|
8270
8477
|
export {
|
|
8478
|
+
RUNTIME_PREFLIGHT_REFUSAL,
|
|
8479
|
+
cmdJoin,
|
|
8271
8480
|
runtimeVersionString2 as runtimeVersionString
|
|
8272
8481
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frod.io/bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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
|
}
|