@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.
- package/README.md +44 -2
- package/dist/adapters/claude.js +6 -2
- package/dist/adapters/codex.js +7 -4
- package/dist/agent-auto-update.d.ts +75 -0
- package/dist/agent-auto-update.js +134 -0
- package/dist/agent-binary.d.ts +48 -0
- package/dist/agent-binary.js +55 -0
- package/dist/agent-cleanup.d.ts +78 -0
- package/dist/agent-cleanup.js +184 -0
- package/dist/agent-install.d.ts +140 -0
- package/dist/agent-install.js +475 -0
- package/dist/agent-registry.d.ts +223 -0
- package/dist/agent-registry.js +131 -0
- package/dist/agent-versions.d.ts +93 -0
- package/dist/agent-versions.js +157 -0
- package/dist/auth-relay.d.ts +9 -1
- package/dist/auth-relay.js +3 -1
- package/dist/commit-message.js +5 -0
- package/dist/config.d.ts +44 -4
- package/dist/config.js +43 -0
- package/dist/index.js +46 -12
- package/dist/levels.d.ts +49 -0
- package/dist/levels.js +51 -0
- package/dist/protocol.d.ts +155 -28
- package/dist/protocol.js +33 -1
- package/dist/recipe-schema.d.ts +12 -12
- package/dist/self-update.d.ts +14 -0
- package/dist/self-update.js +45 -13
- package/dist/supervisor.d.ts +186 -2
- package/dist/supervisor.js +503 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
package/dist/auth-relay.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
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). */
|
package/dist/auth-relay.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/commit-message.js
CHANGED
|
@@ -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
|