@bridge4dev/runner 0.46.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Mirror of the `#region agent-runtimes-mirror` block in
3
+ * `packages/shared/src/constants/agent-runtimes.ts` — the DevBridge side is the
4
+ * source of truth, exactly like `protocol.ts` mirrors the API's wire types and
5
+ * `recipe-schema.ts` mirrors the shared recipe schema.
6
+ *
7
+ * Copied rather than imported on purpose: this package is published to npm on
8
+ * its own and installed by users who have no DevBridge workspace, so a
9
+ * `@devbridge/shared` import would make the published tarball unresolvable.
10
+ *
11
+ * Unlike the older mirrors, this one is CHECKED: `agent-registry.test.ts` reads
12
+ * both files and compares the region between the `#region` markers character by
13
+ * character. Edit the shared file first, then paste the region here — nothing
14
+ * but the region, and nothing of the region left out.
15
+ *
16
+ * Why the runner needs it at all: it is the runner, not the server, that knows
17
+ * which agents exist and how to ask them their version. A command from the
18
+ * server carries an agent name from a closed list and a version, never a package
19
+ * name, a URL or anything else executable (§6 of the plan).
20
+ */
21
+ /**
22
+ * How an agent is put on a machine — a field, not a branch in the code.
23
+ *
24
+ * - `npm-global` — `npm install -g <package>@<version>`;
25
+ * - `script` — the vendor's own installer, downloaded from a URL baked in
26
+ * here and run as a file (never `curl | bash`), with the version as its
27
+ * argument.
28
+ */
29
+ export type AgentInstallKind = 'npm-global' | 'script';
30
+ /**
31
+ * Who owns the copy on the machine, as the agent's own diagnostics report it.
32
+ *
33
+ * This is the answer to «may we touch it». Each install kind manages exactly
34
+ * one value (`script` ↔ `native`, `npm-global` ↔ `npm`); anything else —
35
+ * Homebrew, a hand-placed binary, Claude installed through the npm package
36
+ * rather than the native installer, or a machine that would not say — is
37
+ * someone else's arrangement, and the product shows the command instead of a
38
+ * button.
39
+ */
40
+ export type AgentManagedBy = 'native' | 'npm' | 'brew' | 'standalone' | 'unknown';
41
+ /** How the raw text of a probe becomes a version or an install method. */
42
+ export interface AgentProbeSpec {
43
+ /** Arguments after the binary name. Never a shell string. */
44
+ readonly argv: readonly string[];
45
+ /**
46
+ * Pattern for the first capture group, as SOURCE TEXT rather than a RegExp.
47
+ *
48
+ * A `RegExp` literal cannot be copied into the runner's mirror as data, and a
49
+ * shared mutable object (`lastIndex`) is a poor thing to export. Callers do
50
+ * `new RegExp(spec.pattern, spec.flags)` — never with `g`.
51
+ */
52
+ readonly pattern: string;
53
+ /** Extra flags for the pattern. `m` where the answer is one line of many. */
54
+ readonly flags?: string;
55
+ /**
56
+ * Present when the probe answers with JSON: the path to the string to match,
57
+ * key by key. Written as a path because the keys themselves contain dots
58
+ * (`checks['runtime.provenance']`).
59
+ */
60
+ readonly jsonPath?: readonly string[];
61
+ }
62
+ /** Where the newest published version is announced. */
63
+ export interface AgentLatestSpec {
64
+ /** Primary source: `registry.npmjs.org/-/package/<package>/dist-tags`. */
65
+ readonly npmPackage: string;
66
+ /** Which dist-tag counts as «the current one». */
67
+ readonly npmTag: string;
68
+ /**
69
+ * The vendor's own endpoint, used only when the registry could not be asked.
70
+ * `text` — the body IS the version; `json` — read `field`, then take the
71
+ * first `1.2.3` in it (`rust-v0.153.4`).
72
+ */
73
+ readonly fallback: {
74
+ readonly url: string;
75
+ readonly format: 'text' | 'json';
76
+ readonly field?: string;
77
+ };
78
+ }
79
+ export interface AgentRuntime {
80
+ /** Registry key. Lowercase, stable, appears in no database column. */
81
+ readonly id: string;
82
+ /** `DevAgent` in Prisma and in every DTO the dashboard reads. */
83
+ readonly dbValue: string;
84
+ /** The spelling the runner, the relay and `capabilities.agents` use. */
85
+ readonly wireKey: string;
86
+ /** What a person is shown: «Claude Code». */
87
+ readonly label: string;
88
+ /** The same name where a row is narrow: «Claude». */
89
+ readonly shortLabel: string;
90
+ /** The executable, looked up on the daemon user's PATH. */
91
+ readonly bin: string;
92
+ /**
93
+ * Does this agent report what a turn cost? Claude does, Codex does not — and
94
+ * a budget that silently never fills is worse than no budget.
95
+ */
96
+ readonly reportsCost: boolean;
97
+ readonly install: {
98
+ readonly kind: 'npm-global';
99
+ readonly package: string;
100
+ readonly managedBy: AgentManagedBy;
101
+ } | {
102
+ readonly kind: 'script';
103
+ readonly url: string;
104
+ readonly managedBy: AgentManagedBy;
105
+ };
106
+ /** `<bin> --version` and how to read its answer. */
107
+ readonly versionProbe: AgentProbeSpec;
108
+ /** How the agent's own diagnostics name its install method. Best effort. */
109
+ readonly managedByProbe: AgentProbeSpec;
110
+ readonly latest: AgentLatestSpec;
111
+ /**
112
+ * Below this the product warns («часть возможностей недоступна») but still
113
+ * starts the session — Р6. `null` means «no threshold measured yet», which is
114
+ * not the same as «any version will do»: Claude's number comes from the probe
115
+ * of stage C1.
116
+ */
117
+ readonly minSupported: string | null;
118
+ }
119
+ /** Registry order. It is the order agents appear in every list. */
120
+ export declare const AGENT_RUNTIME_IDS: readonly ["claude", "codex"];
121
+ export type AgentRuntimeId = (typeof AGENT_RUNTIME_IDS)[number];
122
+ export declare const AGENT_RUNTIMES: {
123
+ readonly claude: {
124
+ readonly id: "claude";
125
+ readonly dbValue: "CLAUDE";
126
+ readonly wireKey: "claude";
127
+ readonly label: "Claude Code";
128
+ readonly shortLabel: "Claude";
129
+ readonly bin: "claude";
130
+ readonly reportsCost: true;
131
+ readonly install: {
132
+ readonly kind: "script";
133
+ readonly url: "https://claude.ai/install.sh";
134
+ readonly managedBy: "native";
135
+ };
136
+ readonly versionProbe: {
137
+ readonly argv: readonly ["--version"];
138
+ readonly pattern: "^(\\d+\\.\\d+\\.\\d+)\\s+\\(Claude Code\\)";
139
+ };
140
+ readonly managedByProbe: {
141
+ readonly argv: readonly ["doctor"];
142
+ readonly pattern: "^Config install method:\\s*(\\S+)";
143
+ readonly flags: "m";
144
+ };
145
+ readonly latest: {
146
+ readonly npmPackage: "@anthropic-ai/claude-code";
147
+ readonly npmTag: "latest";
148
+ readonly fallback: {
149
+ readonly url: "https://downloads.claude.ai/claude-code-releases/latest";
150
+ readonly format: "text";
151
+ };
152
+ };
153
+ readonly minSupported: null;
154
+ };
155
+ readonly codex: {
156
+ readonly id: "codex";
157
+ readonly dbValue: "CODEX";
158
+ readonly wireKey: "codex";
159
+ readonly label: "Codex";
160
+ readonly shortLabel: "Codex";
161
+ readonly bin: "codex";
162
+ readonly reportsCost: false;
163
+ readonly install: {
164
+ readonly kind: "npm-global";
165
+ readonly package: "@openai/codex";
166
+ readonly managedBy: "npm";
167
+ };
168
+ readonly versionProbe: {
169
+ readonly argv: readonly ["--version"];
170
+ readonly pattern: "^codex-cli\\s+(\\d+\\.\\d+\\.\\d+)";
171
+ };
172
+ readonly managedByProbe: {
173
+ readonly argv: readonly ["doctor", "--json"];
174
+ readonly jsonPath: readonly ["checks", "runtime.provenance", "details", "install method"];
175
+ readonly pattern: "^([A-Za-z-]+)";
176
+ };
177
+ readonly latest: {
178
+ readonly npmPackage: "@openai/codex";
179
+ readonly npmTag: "latest";
180
+ readonly fallback: {
181
+ readonly url: "https://releases.openai.com/codex/channels/latest";
182
+ readonly format: "json";
183
+ readonly field: "tag_name";
184
+ };
185
+ };
186
+ readonly minSupported: "0.145.0";
187
+ };
188
+ };
189
+ /** A semantic version and nothing else — what a runner may report or be told. */
190
+ export declare const AGENT_VERSION_PATTERN = "^\\d+\\.\\d+\\.\\d+$";
191
+ /**
192
+ * Compare two `1.2.3` strings. `null` when either is not one.
193
+ *
194
+ * Deliberately three numeric parts and no pre-release ordering: both vendors
195
+ * publish plain triples on their release channels, and a comparison that
196
+ * pretends to understand `-alpha.3` would be a guess wearing a number.
197
+ */
198
+ export declare function compareAgentVersions(a: string, b: string): number | null;
199
+ /** The agents this runner knows about, in registry order. */
200
+ export declare const AGENT_RUNTIME_LIST: readonly AgentRuntime[];
201
+ /**
202
+ * The agent a wire name refers to, or `null`.
203
+ *
204
+ * `null` and never a default: a name the runner does not recognise must not be
205
+ * answered with somebody else's binary — the command that carries it installs
206
+ * software on a stranger's machine.
207
+ */
208
+ export declare function agentByWireKey(value: string | null | undefined): AgentRuntime | null;
209
+ /** `CLAUDE` → the entry; case is forgiven, unknown names are not. */
210
+ export declare function agentByDbValue(value: string | null | undefined): AgentRuntime | null;
211
+ /**
212
+ * The two alphabets as literal tuples — written out, not computed.
213
+ *
214
+ * `AGENT_RUNTIME_LIST.map((runtime) => runtime.dbValue)` is `string[]`, and a
215
+ * `z.enum` given that quietly accepts any string: the session descriptor would
216
+ * stop refusing an agent this build has no adapter for. So they are declared,
217
+ * and the test below fails the moment they stop matching the registry.
218
+ */
219
+ export declare const AGENT_DB_VALUES: readonly ["CLAUDE", "CODEX"];
220
+ export type DevAgentName = (typeof AGENT_DB_VALUES)[number];
221
+ export declare const AGENT_WIRE_KEYS: readonly ["claude", "codex"];
222
+ export type RelayAgentName = (typeof AGENT_WIRE_KEYS)[number];
223
+ //# sourceMappingURL=agent-registry.d.ts.map
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Mirror of the `#region agent-runtimes-mirror` block in
3
+ * `packages/shared/src/constants/agent-runtimes.ts` — the DevBridge side is the
4
+ * source of truth, exactly like `protocol.ts` mirrors the API's wire types and
5
+ * `recipe-schema.ts` mirrors the shared recipe schema.
6
+ *
7
+ * Copied rather than imported on purpose: this package is published to npm on
8
+ * its own and installed by users who have no DevBridge workspace, so a
9
+ * `@devbridge/shared` import would make the published tarball unresolvable.
10
+ *
11
+ * Unlike the older mirrors, this one is CHECKED: `agent-registry.test.ts` reads
12
+ * both files and compares the region between the `#region` markers character by
13
+ * character. Edit the shared file first, then paste the region here — nothing
14
+ * but the region, and nothing of the region left out.
15
+ *
16
+ * Why the runner needs it at all: it is the runner, not the server, that knows
17
+ * which agents exist and how to ask them their version. A command from the
18
+ * server carries an agent name from a closed list and a version, never a package
19
+ * name, a URL or anything else executable (§6 of the plan).
20
+ */
21
+ /** Registry order. It is the order agents appear in every list. */
22
+ export const AGENT_RUNTIME_IDS = ['claude', 'codex'];
23
+ export const AGENT_RUNTIMES = {
24
+ claude: {
25
+ id: 'claude',
26
+ dbValue: 'CLAUDE',
27
+ wireKey: 'claude',
28
+ label: 'Claude Code',
29
+ shortLabel: 'Claude',
30
+ bin: 'claude',
31
+ reportsCost: true,
32
+ install: { kind: 'script', url: 'https://claude.ai/install.sh', managedBy: 'native' },
33
+ versionProbe: { argv: ['--version'], pattern: '^(\\d+\\.\\d+\\.\\d+)\\s+\\(Claude Code\\)' },
34
+ managedByProbe: {
35
+ argv: ['doctor'],
36
+ pattern: '^Config install method:\\s*(\\S+)',
37
+ flags: 'm',
38
+ },
39
+ latest: {
40
+ npmPackage: '@anthropic-ai/claude-code',
41
+ npmTag: 'latest',
42
+ fallback: { url: 'https://downloads.claude.ai/claude-code-releases/latest', format: 'text' },
43
+ },
44
+ minSupported: null,
45
+ },
46
+ codex: {
47
+ id: 'codex',
48
+ dbValue: 'CODEX',
49
+ wireKey: 'codex',
50
+ label: 'Codex',
51
+ shortLabel: 'Codex',
52
+ bin: 'codex',
53
+ reportsCost: false,
54
+ install: { kind: 'npm-global', package: '@openai/codex', managedBy: 'npm' },
55
+ versionProbe: { argv: ['--version'], pattern: '^codex-cli\\s+(\\d+\\.\\d+\\.\\d+)' },
56
+ managedByProbe: {
57
+ argv: ['doctor', '--json'],
58
+ jsonPath: ['checks', 'runtime.provenance', 'details', 'install method'],
59
+ pattern: '^([A-Za-z-]+)',
60
+ },
61
+ latest: {
62
+ npmPackage: '@openai/codex',
63
+ npmTag: 'latest',
64
+ fallback: {
65
+ url: 'https://releases.openai.com/codex/channels/latest',
66
+ format: 'json',
67
+ field: 'tag_name',
68
+ },
69
+ },
70
+ // Below 0.145.0 there is no `thread/compact/start`, so a session cannot
71
+ // compact its context at all (`packages/runner/src/adapters/codex.ts`).
72
+ minSupported: '0.145.0',
73
+ },
74
+ };
75
+ /** A semantic version and nothing else — what a runner may report or be told. */
76
+ export const AGENT_VERSION_PATTERN = '^\\d+\\.\\d+\\.\\d+$';
77
+ /**
78
+ * Compare two `1.2.3` strings. `null` when either is not one.
79
+ *
80
+ * Deliberately three numeric parts and no pre-release ordering: both vendors
81
+ * publish plain triples on their release channels, and a comparison that
82
+ * pretends to understand `-alpha.3` would be a guess wearing a number.
83
+ */
84
+ export function compareAgentVersions(a, b) {
85
+ const left = parseAgentVersion(a);
86
+ const right = parseAgentVersion(b);
87
+ if (!left || !right)
88
+ return null;
89
+ for (let index = 0; index < 3; index += 1) {
90
+ const diff = (left[index] ?? 0) - (right[index] ?? 0);
91
+ if (diff !== 0)
92
+ return diff < 0 ? -1 : 1;
93
+ }
94
+ return 0;
95
+ }
96
+ function parseAgentVersion(value) {
97
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
98
+ if (!match)
99
+ return null;
100
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
101
+ }
102
+ // #endregion agent-runtimes-mirror
103
+ /** The agents this runner knows about, in registry order. */
104
+ export const AGENT_RUNTIME_LIST = AGENT_RUNTIME_IDS.map((id) => AGENT_RUNTIMES[id]);
105
+ const BY_WIRE_KEY = new Map(AGENT_RUNTIME_LIST.map((runtime) => [runtime.wireKey, runtime]));
106
+ /**
107
+ * The agent a wire name refers to, or `null`.
108
+ *
109
+ * `null` and never a default: a name the runner does not recognise must not be
110
+ * answered with somebody else's binary — the command that carries it installs
111
+ * software on a stranger's machine.
112
+ */
113
+ export function agentByWireKey(value) {
114
+ return BY_WIRE_KEY.get(String(value ?? '').toLowerCase()) ?? null;
115
+ }
116
+ /** `CLAUDE` → the entry; case is forgiven, unknown names are not. */
117
+ export function agentByDbValue(value) {
118
+ const wanted = String(value ?? '').toUpperCase();
119
+ return AGENT_RUNTIME_LIST.find((runtime) => runtime.dbValue === wanted) ?? null;
120
+ }
121
+ /**
122
+ * The two alphabets as literal tuples — written out, not computed.
123
+ *
124
+ * `AGENT_RUNTIME_LIST.map((runtime) => runtime.dbValue)` is `string[]`, and a
125
+ * `z.enum` given that quietly accepts any string: the session descriptor would
126
+ * stop refusing an agent this build has no adapter for. So they are declared,
127
+ * and the test below fails the moment they stop matching the registry.
128
+ */
129
+ export const AGENT_DB_VALUES = ['CLAUDE', 'CODEX'];
130
+ export const AGENT_WIRE_KEYS = ['claude', 'codex'];
131
+ //# sourceMappingURL=agent-registry.js.map
@@ -0,0 +1,93 @@
1
+ import { type AgentManagedBy, type AgentRuntime } from './agent-registry.js';
2
+ /**
3
+ * What is actually installed on this machine, measured by asking the binaries.
4
+ *
5
+ * The product could not answer «which Claude is on that server» at all before
6
+ * this: the version travelled nowhere, so a card showed a machine and said
7
+ * nothing about the two CLIs the whole product runs on. Everything here is one
8
+ * half of that answer — the other half is the `agent_versions` frame in
9
+ * `supervisor.ts`, which carries it up.
10
+ *
11
+ * Three rules, each from something that would otherwise bite:
12
+ *
13
+ * 1. **`null` is «not installed», and it is a normal answer.** A machine
14
+ * without Codex is not a broken machine; the card draws «Install» for it.
15
+ * A probe that FAILED is also `null` for the version, and the log line is
16
+ * where the difference lives — inventing a third state on the wire would
17
+ * make every reader handle a case they cannot act on.
18
+ * 2. **The version comes from `--version`, the install method from the agent's
19
+ * own diagnostics.** Guessing ownership from a path would call a Homebrew
20
+ * Claude «native» and offer a button that fights Homebrew.
21
+ * 3. **Measured at most once an hour.** The runner reconnects dozens of times
22
+ * a day (#227) and re-sends this frame on every `hello_ack`; spawning four
23
+ * processes each time would be a tax on the machine for no new fact.
24
+ */
25
+ /** One agent, as this machine has it. */
26
+ export interface MeasuredAgentVersion {
27
+ /** `null` — the binary is not on the daemon user's PATH, or would not answer. */
28
+ version: string | null;
29
+ managedBy: AgentManagedBy;
30
+ /**
31
+ * The binary WAS there and the probe still produced no version.
32
+ *
33
+ * The difference matters more than it looks: without it a ten-second timeout
34
+ * on a loaded machine reaches the dashboard as «Claude is not installed
35
+ * here», the agent is greyed out of the New session window, and it stays that
36
+ * way for the hour this measurement is cached. Absence is a claim, and a
37
+ * claim needs evidence — this flag is what withholds it.
38
+ */
39
+ probeFailed?: boolean;
40
+ }
41
+ export interface AgentVersionsMeasurement {
42
+ /** When the machine took the measurement, ISO — the frame's `at`. */
43
+ at: string;
44
+ /** Keyed by the registry's wire name (`claude`, `codex`). */
45
+ agents: Record<string, MeasuredAgentVersion>;
46
+ }
47
+ /** How long a measurement is believed. See rule 3 above. */
48
+ export declare const AGENT_VERSIONS_CACHE_MS: number;
49
+ /**
50
+ * Forget the cached measurement.
51
+ *
52
+ * Called after an install (stage B) and by tests. Without it the card would go
53
+ * on showing the old version for up to an hour after a successful update, which
54
+ * reads as «the button did nothing».
55
+ */
56
+ export declare function invalidateAgentVersions(): void;
57
+ export interface ProbeResult {
58
+ stdout: string;
59
+ stderr: string;
60
+ }
61
+ /** Run one probe. Rejects on a non-zero exit, a timeout or a missing binary. */
62
+ export type ProbeRunner = (file: string, argv: readonly string[], timeoutMs: number) => Promise<ProbeResult>;
63
+ /**
64
+ * Fold whatever an agent calls its install method into the five values the
65
+ * product reasons about.
66
+ *
67
+ * Unknown wording becomes `unknown`, which means «show the command, offer no
68
+ * button» — the safe direction: the alternative is a button that runs the wrong
69
+ * package manager on somebody else's machine.
70
+ */
71
+ export declare function normalizeManagedBy(raw: string | null | undefined): AgentManagedBy;
72
+ /**
73
+ * Measure one agent: its version and how it was installed.
74
+ *
75
+ * Exported because the install path (`agent-install.ts`) needs exactly this
76
+ * answer twice — once before it touches anything, once as the smoke test after.
77
+ * Reusing it rather than probing separately keeps the number of ways this
78
+ * product can answer «is that agent installed, and which one» at three rather
79
+ * than four: `installedAgents()`, the `agentClis` capability, and this.
80
+ */
81
+ export declare function measureAgent(runtime: AgentRuntime, run?: ProbeRunner): Promise<MeasuredAgentVersion>;
82
+ /**
83
+ * Measure every agent in the registry, or hand back the cached answer.
84
+ *
85
+ * Probes run in parallel: they are independent, and a Codex doctor that is slow
86
+ * to answer must not delay the Claude row.
87
+ */
88
+ export declare function measureAgentVersions(options?: {
89
+ force?: boolean;
90
+ now?: () => number;
91
+ run?: ProbeRunner;
92
+ }): Promise<AgentVersionsMeasurement>;
93
+ //# sourceMappingURL=agent-versions.d.ts.map
@@ -0,0 +1,157 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { AGENT_RUNTIME_LIST, } from './agent-registry.js';
3
+ import { whichExecutable } from './environment.js';
4
+ import { log } from './log.js';
5
+ /**
6
+ * How long a probe may take before it is written off.
7
+ *
8
+ * Ten seconds is the number every other probe in this package uses
9
+ * (`environment.ts`, `auth-relay.ts`); `codex doctor --json` checks the release
10
+ * channel over the network and took ~250 ms on a live machine, so the margin is
11
+ * for a machine under load rather than for the work.
12
+ */
13
+ const PROBE_TIMEOUT_MS = 10_000;
14
+ /** Doctor output is a page of JSON; a megabyte is a hundred times enough. */
15
+ const PROBE_MAX_BUFFER = 1_024 * 1_024;
16
+ /** How long a measurement is believed. See rule 3 above. */
17
+ export const AGENT_VERSIONS_CACHE_MS = 60 * 60 * 1_000;
18
+ let cached = null;
19
+ /**
20
+ * Forget the cached measurement.
21
+ *
22
+ * Called after an install (stage B) and by tests. Without it the card would go
23
+ * on showing the old version for up to an hour after a successful update, which
24
+ * reads as «the button did nothing».
25
+ */
26
+ export function invalidateAgentVersions() {
27
+ cached = null;
28
+ }
29
+ const runProbe = (file, argv, timeoutMs) => new Promise((resolve, reject) => {
30
+ const child = execFile(file, [...argv], { timeout: timeoutMs, maxBuffer: PROBE_MAX_BUFFER }, (error, stdout, stderr) => {
31
+ if (error)
32
+ reject(error);
33
+ else
34
+ resolve({ stdout, stderr });
35
+ });
36
+ // `claude doctor` reads stdin. An `execFile` child inherits an open pipe
37
+ // nobody ever writes to, so without this EOF the probe would sit there
38
+ // until the timeout killed it — measured, not assumed.
39
+ child.stdin?.end();
40
+ });
41
+ function matchProbe(spec, output) {
42
+ const match = new RegExp(spec.pattern, spec.flags).exec(output);
43
+ return match?.[1] ?? null;
44
+ }
45
+ function readJsonPath(raw, jsonPath) {
46
+ let value;
47
+ try {
48
+ value = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ for (const key of jsonPath) {
54
+ if (!value || typeof value !== 'object' || Array.isArray(value))
55
+ return null;
56
+ value = value[key];
57
+ }
58
+ return typeof value === 'string' ? value : null;
59
+ }
60
+ /**
61
+ * Fold whatever an agent calls its install method into the five values the
62
+ * product reasons about.
63
+ *
64
+ * Unknown wording becomes `unknown`, which means «show the command, offer no
65
+ * button» — the safe direction: the alternative is a button that runs the wrong
66
+ * package manager on somebody else's machine.
67
+ */
68
+ export function normalizeManagedBy(raw) {
69
+ const value = String(raw ?? '')
70
+ .trim()
71
+ .toLowerCase();
72
+ if (value === 'native')
73
+ return 'native';
74
+ if (value === 'npm' || value === 'npm-global' || value === 'global')
75
+ return 'npm';
76
+ if (value === 'brew' || value === 'homebrew')
77
+ return 'brew';
78
+ if (value === 'standalone' || value === 'binary' || value === 'local')
79
+ return 'standalone';
80
+ return 'unknown';
81
+ }
82
+ /**
83
+ * Measure one agent: its version and how it was installed.
84
+ *
85
+ * Exported because the install path (`agent-install.ts`) needs exactly this
86
+ * answer twice — once before it touches anything, once as the smoke test after.
87
+ * Reusing it rather than probing separately keeps the number of ways this
88
+ * product can answer «is that agent installed, and which one» at three rather
89
+ * than four: `installedAgents()`, the `agentClis` capability, and this.
90
+ */
91
+ export async function measureAgent(runtime, run = runProbe) {
92
+ const binary = whichExecutable(runtime.bin);
93
+ if (!binary)
94
+ return { version: null, managedBy: 'unknown' };
95
+ let version;
96
+ try {
97
+ const { stdout, stderr } = await run(binary, runtime.versionProbe.argv, PROBE_TIMEOUT_MS);
98
+ // Some CLIs print the version on stderr; reading both costs nothing and
99
+ // saves a per-agent exception.
100
+ version = matchProbe(runtime.versionProbe, `${stdout}\n${stderr}`);
101
+ if (!version) {
102
+ log.warn('agent-versions: could not read a version', {
103
+ agent: runtime.wireKey,
104
+ output: `${stdout}${stderr}`.slice(0, 200),
105
+ });
106
+ // The binary answered something we cannot parse — a vendor that changed
107
+ // its line. It is here, so nothing may say otherwise.
108
+ return { version: null, managedBy: 'unknown', probeFailed: true };
109
+ }
110
+ }
111
+ catch (error) {
112
+ log.warn('agent-versions: version probe failed', {
113
+ agent: runtime.wireKey,
114
+ error: String(error instanceof Error ? error.message : error),
115
+ });
116
+ return { version: null, managedBy: 'unknown', probeFailed: true };
117
+ }
118
+ // The install method is a nice-to-have: it decides whether a BUTTON appears,
119
+ // never whether the version is shown. A doctor that failed leaves `unknown`.
120
+ let managedBy = 'unknown';
121
+ try {
122
+ const probe = runtime.managedByProbe;
123
+ const { stdout, stderr } = await run(binary, probe.argv, PROBE_TIMEOUT_MS);
124
+ const output = `${stdout}\n${stderr}`;
125
+ const source = probe.jsonPath ? readJsonPath(stdout, probe.jsonPath) : output;
126
+ managedBy = normalizeManagedBy(source ? matchProbe(probe, source) : null);
127
+ }
128
+ catch (error) {
129
+ log.warn('agent-versions: install-method probe failed', {
130
+ agent: runtime.wireKey,
131
+ error: String(error instanceof Error ? error.message : error),
132
+ });
133
+ }
134
+ return { version, managedBy };
135
+ }
136
+ /**
137
+ * Measure every agent in the registry, or hand back the cached answer.
138
+ *
139
+ * Probes run in parallel: they are independent, and a Codex doctor that is slow
140
+ * to answer must not delay the Claude row.
141
+ */
142
+ export async function measureAgentVersions(options = {}) {
143
+ const now = options.now ?? Date.now;
144
+ const nowMs = now();
145
+ if (!options.force && cached && nowMs - cached.takenAtMs < AGENT_VERSIONS_CACHE_MS) {
146
+ return cached.measurement;
147
+ }
148
+ const run = options.run ?? runProbe;
149
+ const results = await Promise.all(AGENT_RUNTIME_LIST.map(async (runtime) => [runtime.wireKey, await measureAgent(runtime, run)]));
150
+ const measurement = {
151
+ at: new Date(nowMs).toISOString(),
152
+ agents: Object.fromEntries(results),
153
+ };
154
+ cached = { takenAtMs: nowMs, measurement };
155
+ return measurement;
156
+ }
157
+ //# sourceMappingURL=agent-versions.js.map
@@ -1,4 +1,12 @@
1
- export type RelayAgent = 'claude' | 'codex';
1
+ import { type RelayAgentName } from './agent-registry.js';
2
+ /**
3
+ * The agents the login relay can drive — the registry's wire alphabet (Р15).
4
+ *
5
+ * Re-exported under the name every caller already uses rather than renamed at
6
+ * ~20 call sites: the point of the registry is that the list has one home, not
7
+ * that every file learns a new word for it.
8
+ */
9
+ export type RelayAgent = RelayAgentName;
2
10
  export interface LoginStartResult {
3
11
  url: string;
4
12
  /** Device-auth style user code to enter on the provider page (Codex). */
@@ -159,7 +159,9 @@ export class AuthRelay {
159
159
  if (!commandExists(command)) {
160
160
  const binary = command.trim().split(/\s+/)[0] ?? agent;
161
161
  throw new Error(`\`${binary}\` is not installed for ${runnerIdentity().user} on this server — ` +
162
- 'install the agent CLI for that user (or use the installer’s --user mode, which does it) and try again');
162
+ // The installer stopped installing agents in 0.47.0 (Р9 of #371), so
163
+ // «use --user mode, which does it» is no longer true of anything.
164
+ 'press «Install» on this agent’s row in the server card and try again');
163
165
  }
164
166
  if (!whichExecutable('script')) {
165
167
  // util-linux, and the only reason a pty exists here at all.
@@ -1,4 +1,5 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { claudeExecutableOption } from './agent-binary.js';
2
3
  import { scrubbedEnv } from './adapters/claude.js';
3
4
  import { gitBranchDiff } from './gitops.js';
4
5
  import { isSecretPath, maskString } from './policy.js';
@@ -153,6 +154,10 @@ export async function proposeCommitMessage(input, queryFn = query) {
153
154
  options: {
154
155
  cwd: input.worktreePath,
155
156
  env: scrubbedEnv(),
157
+ // The second place Claude `Options` are built. It must follow the same
158
+ // constant, or after C3 commit messages would still be written by the
159
+ // bundled binary while sessions had moved to the system one.
160
+ ...claudeExecutableOption(),
156
161
  settingSources: [],
157
162
  maxTurns: 1,
158
163
  // Belt and braces: an empty allowlist is a statement of intent, the