@phnx-labs/agents-cli 1.20.67 → 1.20.69
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/CHANGELOG.md +100 -0
- package/README.md +14 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +126 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +267 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +39 -36
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -5
- package/dist/lib/agents.js +59 -24
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +40 -3
- package/dist/lib/usage.js +147 -16
- package/package.json +1 -1
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
|
-
import * as TOML from 'smol-toml';
|
|
16
15
|
import { capableAgents } from '../capabilities.js';
|
|
16
|
+
import { getProjectMcpConfigPath } from '../agents.js';
|
|
17
|
+
import { writeMcpConfig } from '../mcp.js';
|
|
17
18
|
import { getSystemMcpDir, getUserMcpDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
18
19
|
/**
|
|
19
20
|
* Parse an MCP YAML file into an McpItem.
|
|
@@ -132,316 +133,10 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
135
|
-
*
|
|
136
|
+
* Dispatch MCP items to the agent-specific config writer.
|
|
136
137
|
*/
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
let inString = false;
|
|
140
|
-
let escape = false;
|
|
141
|
-
let i = 0;
|
|
142
|
-
while (i < content.length) {
|
|
143
|
-
const char = content[i];
|
|
144
|
-
const next = content[i + 1];
|
|
145
|
-
if (escape) {
|
|
146
|
-
result += char;
|
|
147
|
-
escape = false;
|
|
148
|
-
i++;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (char === '\\' && inString) {
|
|
152
|
-
result += char;
|
|
153
|
-
escape = true;
|
|
154
|
-
i++;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (char === '"') {
|
|
158
|
-
inString = !inString;
|
|
159
|
-
result += char;
|
|
160
|
-
i++;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (!inString) {
|
|
164
|
-
if (char === '/' && next === '/') {
|
|
165
|
-
while (i < content.length && content[i] !== '\n') {
|
|
166
|
-
i++;
|
|
167
|
-
}
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (char === '/' && next === '*') {
|
|
171
|
-
i += 2;
|
|
172
|
-
while (i < content.length && !(content[i] === '*' && content[i + 1] === '/')) {
|
|
173
|
-
i++;
|
|
174
|
-
}
|
|
175
|
-
i += 2;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
result += char;
|
|
180
|
-
i++;
|
|
181
|
-
}
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Write MCP servers to Claude settings.json format.
|
|
186
|
-
*/
|
|
187
|
-
function syncToClaudeConfig(configPath, items) {
|
|
188
|
-
let config = {};
|
|
189
|
-
if (fs.existsSync(configPath)) {
|
|
190
|
-
try {
|
|
191
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
config = {};
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const mcpServers = {};
|
|
198
|
-
for (const item of items) {
|
|
199
|
-
if (item.transport === 'stdio') {
|
|
200
|
-
mcpServers[item.name] = {
|
|
201
|
-
command: item.command,
|
|
202
|
-
args: item.args || [],
|
|
203
|
-
env: item.env || {},
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
mcpServers[item.name] = {
|
|
208
|
-
url: item.url,
|
|
209
|
-
...(item.headers && { headers: item.headers }),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
config.mcpServers = mcpServers;
|
|
214
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
215
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Write MCP servers to Codex config.toml format.
|
|
219
|
-
*/
|
|
220
|
-
function syncToCodexConfig(configPath, items) {
|
|
221
|
-
let config = {};
|
|
222
|
-
if (fs.existsSync(configPath)) {
|
|
223
|
-
try {
|
|
224
|
-
config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
config = {};
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
const mcpServers = {};
|
|
231
|
-
for (const item of items) {
|
|
232
|
-
if (item.transport === 'stdio') {
|
|
233
|
-
mcpServers[item.name] = {
|
|
234
|
-
command: item.command,
|
|
235
|
-
args: item.args || [],
|
|
236
|
-
...(item.env && { env: item.env }),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
// Codex may not support HTTP MCPs
|
|
240
|
-
}
|
|
241
|
-
config.mcp_servers = mcpServers;
|
|
242
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
243
|
-
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Write MCP servers to Grok config.toml format ([mcp_servers] section).
|
|
247
|
-
*/
|
|
248
|
-
function syncToGrokConfig(configPath, items) {
|
|
249
|
-
let config = {};
|
|
250
|
-
if (fs.existsSync(configPath)) {
|
|
251
|
-
try {
|
|
252
|
-
config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
config = {};
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
const mcpServers = {};
|
|
259
|
-
for (const item of items) {
|
|
260
|
-
if (item.transport === 'stdio') {
|
|
261
|
-
mcpServers[item.name] = {
|
|
262
|
-
command: item.command,
|
|
263
|
-
args: item.args || [],
|
|
264
|
-
...(item.env && { env: item.env }),
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
else if (item.transport === 'http' || item.transport === 'sse') {
|
|
268
|
-
mcpServers[item.name] = {
|
|
269
|
-
url: item.url,
|
|
270
|
-
...(item.headers && { headers: item.headers }),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
config.mcp_servers = mcpServers;
|
|
275
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
276
|
-
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
277
|
-
}
|
|
278
|
-
function syncToHermesConfig(configPath, items) {
|
|
279
|
-
let config = {};
|
|
280
|
-
if (fs.existsSync(configPath)) {
|
|
281
|
-
try {
|
|
282
|
-
const parsed = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
283
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
284
|
-
config = parsed;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
config = {};
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
const mcpServers = {};
|
|
292
|
-
for (const item of items) {
|
|
293
|
-
if (item.transport === 'stdio') {
|
|
294
|
-
mcpServers[item.name] = {
|
|
295
|
-
command: item.command,
|
|
296
|
-
args: item.args || [],
|
|
297
|
-
...(item.env && { env: item.env }),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
mcpServers[item.name] = {
|
|
302
|
-
url: item.url,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
config.mcp_servers = mcpServers;
|
|
307
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
308
|
-
fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Write MCP servers to OpenCode opencode.jsonc format.
|
|
312
|
-
*/
|
|
313
|
-
function syncToOpenCodeConfig(configPath, items) {
|
|
314
|
-
let config = {};
|
|
315
|
-
if (fs.existsSync(configPath)) {
|
|
316
|
-
try {
|
|
317
|
-
const content = stripJsonComments(fs.readFileSync(configPath, 'utf-8'));
|
|
318
|
-
config = JSON.parse(content);
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
config = {};
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const mcp = {};
|
|
325
|
-
for (const item of items) {
|
|
326
|
-
if (item.transport === 'stdio') {
|
|
327
|
-
// OpenCode uses command as array
|
|
328
|
-
const commandArray = [item.command, ...(item.args || [])];
|
|
329
|
-
mcp[item.name] = {
|
|
330
|
-
type: 'local',
|
|
331
|
-
command: commandArray,
|
|
332
|
-
...(item.env && { env: item.env }),
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
mcp[item.name] = {
|
|
337
|
-
type: 'remote',
|
|
338
|
-
url: item.url,
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
config.mcp = mcp;
|
|
343
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
344
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Write MCP servers to Cursor mcp.json format.
|
|
348
|
-
*/
|
|
349
|
-
function syncToCursorConfig(configPath, items) {
|
|
350
|
-
let config = {};
|
|
351
|
-
if (fs.existsSync(configPath)) {
|
|
352
|
-
try {
|
|
353
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
config = {};
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
const mcpServers = {};
|
|
360
|
-
for (const item of items) {
|
|
361
|
-
if (item.transport === 'stdio') {
|
|
362
|
-
mcpServers[item.name] = {
|
|
363
|
-
command: item.command,
|
|
364
|
-
args: item.args || [],
|
|
365
|
-
env: item.env || {},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
mcpServers[item.name] = {
|
|
370
|
-
url: item.url,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
config.mcpServers = mcpServers;
|
|
375
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
376
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Write MCP servers to Gemini settings.json format.
|
|
380
|
-
*/
|
|
381
|
-
function syncToGeminiConfig(configPath, items) {
|
|
382
|
-
let config = {};
|
|
383
|
-
if (fs.existsSync(configPath)) {
|
|
384
|
-
try {
|
|
385
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
config = {};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
const mcpServers = {};
|
|
392
|
-
for (const item of items) {
|
|
393
|
-
if (item.transport === 'stdio') {
|
|
394
|
-
mcpServers[item.name] = {
|
|
395
|
-
command: item.command,
|
|
396
|
-
args: item.args || [],
|
|
397
|
-
env: item.env || {},
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
mcpServers[item.name] = {
|
|
402
|
-
url: item.url,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
config.mcpServers = mcpServers;
|
|
407
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
408
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Write MCP servers to OpenClaw openclaw.json format.
|
|
412
|
-
*/
|
|
413
|
-
function syncToOpenClawConfig(configPath, items) {
|
|
414
|
-
let config = {};
|
|
415
|
-
if (fs.existsSync(configPath)) {
|
|
416
|
-
try {
|
|
417
|
-
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
config = {};
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (!config.mcp || typeof config.mcp !== 'object') {
|
|
424
|
-
config.mcp = {};
|
|
425
|
-
}
|
|
426
|
-
const servers = {};
|
|
427
|
-
for (const item of items) {
|
|
428
|
-
if (item.transport === 'stdio') {
|
|
429
|
-
servers[item.name] = {
|
|
430
|
-
command: item.command,
|
|
431
|
-
args: item.args,
|
|
432
|
-
env: item.env,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
servers[item.name] = {
|
|
437
|
-
url: item.url,
|
|
438
|
-
transport: item.transport,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
config.mcp.servers = servers;
|
|
443
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
444
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
138
|
+
function syncToAgentConfig(agent, configPath, items, mode = 'overwrite') {
|
|
139
|
+
writeMcpConfig(agent, configPath, items, mode);
|
|
445
140
|
}
|
|
446
141
|
/**
|
|
447
142
|
* MCP resource handler implementing ResourceHandler<McpItem>.
|
|
@@ -511,39 +206,22 @@ export const McpHandler = {
|
|
|
511
206
|
if (items.length === 0) {
|
|
512
207
|
return;
|
|
513
208
|
}
|
|
209
|
+
// Sync resolved MCPs to the version home (user-level agent config).
|
|
514
210
|
const configPath = getMcpConfigPath(agent, versionHome);
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
case 'cursor':
|
|
530
|
-
syncToCursorConfig(configPath, mcpItems);
|
|
531
|
-
break;
|
|
532
|
-
case 'gemini':
|
|
533
|
-
syncToGeminiConfig(configPath, mcpItems);
|
|
534
|
-
break;
|
|
535
|
-
case 'openclaw':
|
|
536
|
-
syncToOpenClawConfig(configPath, mcpItems);
|
|
537
|
-
break;
|
|
538
|
-
case 'grok':
|
|
539
|
-
syncToGrokConfig(configPath, mcpItems);
|
|
540
|
-
break;
|
|
541
|
-
case 'hermes':
|
|
542
|
-
syncToHermesConfig(configPath, mcpItems);
|
|
543
|
-
break;
|
|
544
|
-
case 'forge':
|
|
545
|
-
syncToCursorConfig(configPath, mcpItems);
|
|
546
|
-
break;
|
|
211
|
+
if (configPath) {
|
|
212
|
+
const mcpItems = items.map((r) => r.item);
|
|
213
|
+
syncToAgentConfig(agent, configPath, mcpItems, 'overwrite');
|
|
214
|
+
}
|
|
215
|
+
// Sync project-layer MCPs to the agent's project-level config path so each
|
|
216
|
+
// agent CLI can discover them alongside its user-level config. Merge so we
|
|
217
|
+
// do not clobber entries added manually or by the agent's own CLI.
|
|
218
|
+
const projectAgentsDir = cwd ? getProjectAgentsDir(cwd) : null;
|
|
219
|
+
if (projectAgentsDir) {
|
|
220
|
+
const projectItems = items.filter((r) => r.layer === 'project').map((r) => r.item);
|
|
221
|
+
if (projectItems.length > 0) {
|
|
222
|
+
const projectConfigPath = getProjectMcpConfigPath(agent, path.dirname(projectAgentsDir));
|
|
223
|
+
syncToAgentConfig(agent, projectConfigPath, projectItems, 'merge');
|
|
224
|
+
}
|
|
547
225
|
}
|
|
548
226
|
},
|
|
549
227
|
format(agent) {
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { type UsageSnapshot } from './usage.js';
|
|
|
11
11
|
export interface RotateCandidate {
|
|
12
12
|
agent: AgentId;
|
|
13
13
|
version: string;
|
|
14
|
+
accountKey: string | null;
|
|
15
|
+
accountLabel: string;
|
|
14
16
|
email: string | null;
|
|
15
17
|
/**
|
|
16
18
|
* Per-org usage/quota key (e.g. `claude:org=<orgUuid>`) — the unit rate
|
|
@@ -21,7 +23,9 @@ export interface RotateCandidate {
|
|
|
21
23
|
usageKey: string | null;
|
|
22
24
|
usageStatus: AccountInfo['usageStatus'];
|
|
23
25
|
usageSnapshot: UsageSnapshot | null;
|
|
24
|
-
|
|
26
|
+
usageError: string | null;
|
|
27
|
+
plan: string | null;
|
|
28
|
+
signedIn: boolean;
|
|
25
29
|
lastActive: Date | null;
|
|
26
30
|
}
|
|
27
31
|
export interface RotateResult {
|
|
@@ -58,7 +62,7 @@ export declare function getConfiguredRunStrategy(agent: AgentId, startPath?: str
|
|
|
58
62
|
export declare function setGlobalRunStrategy(agent: AgentId, strategy: RunStrategy): void;
|
|
59
63
|
/**
|
|
60
64
|
* Whether a specific account can serve a run right now, and — when it can't —
|
|
61
|
-
* why. `signed_out` covers
|
|
65
|
+
* why. `signed_out` covers a missing usable credential; `rate_limited` and
|
|
62
66
|
* `out_of_credits` name the throttle. Used to pre-warn on a version-pinned
|
|
63
67
|
* teammate whose account rotation won't route around (a pin IS the target).
|
|
64
68
|
*/
|
|
@@ -71,7 +75,7 @@ export type AccountReadiness = {
|
|
|
71
75
|
};
|
|
72
76
|
/**
|
|
73
77
|
* Pure decision reusing the router's own eligibility gate (`hasUsageAvailable`
|
|
74
|
-
* +
|
|
78
|
+
* + canonical signed-in state, i.e. `isRotationEligible`), so a pre-flight warning can NEVER
|
|
75
79
|
* disagree with what rotation would actually do. The `reason` combines the two
|
|
76
80
|
* signals `hasUsageAvailable` reads: the live snapshot (session-inclusive
|
|
77
81
|
* rate-limit) and the coarse cached `usageStatus` (out-of-credits, which a
|
|
@@ -100,17 +104,16 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
100
104
|
* headroom, with no stampede on the lowest-usage one. Stateless — parallel
|
|
101
105
|
* callers naturally fan out via the random roll.
|
|
102
106
|
*
|
|
103
|
-
* Eligibility: signed in
|
|
107
|
+
* Eligibility: signed in according to AccountInfo and not currently
|
|
104
108
|
* rate-limited — no blocking window (session OR weekly) at 100%, matching the
|
|
105
109
|
* `agents view` badge; or the local cached status is usable when no live
|
|
106
110
|
* snapshot exists. Note the split: eligibility considers the session window
|
|
107
111
|
* (a session-maxed account can't run now), but the capacity *weight* above is
|
|
108
112
|
* driven by weekly headroom so a brief session spike doesn't distort routing.
|
|
109
113
|
*
|
|
110
|
-
* Dedupe: when multiple versions share
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* account and both 429ing.
|
|
114
|
+
* Dedupe: when multiple versions share a usage/account identity, collapse to
|
|
115
|
+
* one candidate (the least-recently-active version). The org-scoped usage key
|
|
116
|
+
* wins over email so same-email personal and Team accounts remain distinct.
|
|
114
117
|
*
|
|
115
118
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
116
119
|
* version so behavior stays predictable.
|
|
@@ -122,6 +125,7 @@ export declare function pickBalancedCandidate(candidates: RotateCandidate[]): Ro
|
|
|
122
125
|
* usage headroom.
|
|
123
126
|
*/
|
|
124
127
|
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null): RotateResult | null;
|
|
128
|
+
export declare function collectRunCandidates(agent: AgentId): Promise<RotateCandidate[]>;
|
|
125
129
|
/**
|
|
126
130
|
* Pick a healthy version for `agent` using weighted random by remaining
|
|
127
131
|
* capacity. See `pickBalancedCandidate` for algorithm details.
|
package/dist/lib/rotate.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import { getAccountInfo } from './agents.js';
|
|
9
|
+
import { accountDisplayLabel, getAccountInfo } from './agents.js';
|
|
10
10
|
import { readMeta, writeMeta, getHelpersDir } from './state.js';
|
|
11
11
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
12
12
|
import { getProjectRunConfigs } from './run-config.js';
|
|
@@ -65,14 +65,10 @@ export function setGlobalRunStrategy(agent, strategy) {
|
|
|
65
65
|
writeMeta(meta);
|
|
66
66
|
}
|
|
67
67
|
function isRotationEligible(candidate) {
|
|
68
|
-
return
|
|
69
|
-
&& candidate.authValid
|
|
70
|
-
&& hasUsageAvailable(candidate);
|
|
68
|
+
return candidate.signedIn && hasUsageAvailable(candidate);
|
|
71
69
|
}
|
|
72
70
|
function isAvailableEligible(candidate) {
|
|
73
|
-
return
|
|
74
|
-
&& candidate.authValid
|
|
75
|
-
&& hasUsageAvailable(candidate);
|
|
71
|
+
return isRotationEligible(candidate);
|
|
76
72
|
}
|
|
77
73
|
function hasUsageAvailable(candidate) {
|
|
78
74
|
const snapshot = candidate.usageSnapshot;
|
|
@@ -95,7 +91,7 @@ function hasUsageAvailable(candidate) {
|
|
|
95
91
|
}
|
|
96
92
|
/**
|
|
97
93
|
* Pure decision reusing the router's own eligibility gate (`hasUsageAvailable`
|
|
98
|
-
* +
|
|
94
|
+
* + canonical signed-in state, i.e. `isRotationEligible`), so a pre-flight warning can NEVER
|
|
99
95
|
* disagree with what rotation would actually do. The `reason` combines the two
|
|
100
96
|
* signals `hasUsageAvailable` reads: the live snapshot (session-inclusive
|
|
101
97
|
* rate-limit) and the coarse cached `usageStatus` (out-of-credits, which a
|
|
@@ -104,7 +100,7 @@ function hasUsageAvailable(candidate) {
|
|
|
104
100
|
* reported while the account is actually serving requests.
|
|
105
101
|
*/
|
|
106
102
|
export function readinessFromCandidate(candidate) {
|
|
107
|
-
if (!candidate.
|
|
103
|
+
if (!candidate.signedIn) {
|
|
108
104
|
return { ready: false, reason: 'signed_out', email: candidate.email };
|
|
109
105
|
}
|
|
110
106
|
if (hasUsageAvailable(candidate)) {
|
|
@@ -162,7 +158,7 @@ function compareCandidates(a, b) {
|
|
|
162
158
|
* key; fall back to email only when no usage identity is available.
|
|
163
159
|
*/
|
|
164
160
|
function candidateIdentity(c) {
|
|
165
|
-
return c.usageKey ?? c.email
|
|
161
|
+
return c.usageKey ?? c.accountKey ?? c.email ?? `${c.agent}@${c.version}`;
|
|
166
162
|
}
|
|
167
163
|
function dedupeAndSortCandidates(candidates) {
|
|
168
164
|
const byIdentity = new Map();
|
|
@@ -189,17 +185,16 @@ function dedupeAndSortCandidates(candidates) {
|
|
|
189
185
|
* headroom, with no stampede on the lowest-usage one. Stateless — parallel
|
|
190
186
|
* callers naturally fan out via the random roll.
|
|
191
187
|
*
|
|
192
|
-
* Eligibility: signed in
|
|
188
|
+
* Eligibility: signed in according to AccountInfo and not currently
|
|
193
189
|
* rate-limited — no blocking window (session OR weekly) at 100%, matching the
|
|
194
190
|
* `agents view` badge; or the local cached status is usable when no live
|
|
195
191
|
* snapshot exists. Note the split: eligibility considers the session window
|
|
196
192
|
* (a session-maxed account can't run now), but the capacity *weight* above is
|
|
197
193
|
* driven by weekly headroom so a brief session spike doesn't distort routing.
|
|
198
194
|
*
|
|
199
|
-
* Dedupe: when multiple versions share
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* account and both 429ing.
|
|
195
|
+
* Dedupe: when multiple versions share a usage/account identity, collapse to
|
|
196
|
+
* one candidate (the least-recently-active version). The org-scoped usage key
|
|
197
|
+
* wins over email so same-email personal and Team accounts remain distinct.
|
|
203
198
|
*
|
|
204
199
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
205
200
|
* version so behavior stays predictable.
|
|
@@ -278,12 +273,11 @@ export function pickAvailableCandidate(candidates, preferredVersion) {
|
|
|
278
273
|
: undefined;
|
|
279
274
|
return { picked: preferred ?? sorted[0], healthy: sorted, excluded };
|
|
280
275
|
}
|
|
281
|
-
async function collectRunCandidates(agent) {
|
|
276
|
+
export async function collectRunCandidates(agent) {
|
|
282
277
|
const versions = listInstalledVersions(agent);
|
|
283
278
|
const rows = await Promise.all(versions.map(async (version) => {
|
|
284
279
|
const home = getVersionHomePath(agent, version);
|
|
285
280
|
const info = await getAccountInfo(agent, home);
|
|
286
|
-
// `info.email` (from .claude.json's oauthAccount) is the auth heuristic.
|
|
287
281
|
// We used to additionally call isClaudeAuthValid(home), which reads
|
|
288
282
|
// "Claude Code-credentials-<hash>" from the system keychain. That item is
|
|
289
283
|
// written by Claude Code itself with its own process in the ACL, so our
|
|
@@ -291,15 +285,17 @@ async function collectRunCandidates(agent) {
|
|
|
291
285
|
// one per installed version, every time `agents run` cold-starts. If
|
|
292
286
|
// claude's stored token has actually expired, the spawned agent detects
|
|
293
287
|
// it at its own startup and re-auths; that's the correct UX.
|
|
294
|
-
const authValid = info.email != null;
|
|
295
288
|
return {
|
|
296
289
|
agent,
|
|
297
290
|
version,
|
|
298
291
|
home,
|
|
299
292
|
info,
|
|
293
|
+
accountKey: info.accountKey,
|
|
294
|
+
accountLabel: accountDisplayLabel(info),
|
|
300
295
|
email: info.email,
|
|
301
296
|
usageStatus: info.usageStatus,
|
|
302
|
-
|
|
297
|
+
plan: info.plan,
|
|
298
|
+
signedIn: info.signedIn,
|
|
303
299
|
lastActive: info.lastActive,
|
|
304
300
|
};
|
|
305
301
|
}));
|
|
@@ -311,10 +307,13 @@ async function collectRunCandidates(agent) {
|
|
|
311
307
|
})));
|
|
312
308
|
return rows.map(({ home: _home, info, ...candidate }) => {
|
|
313
309
|
const usageKey = getUsageLookupKey(info);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
310
|
+
const usage = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
311
|
+
return {
|
|
312
|
+
...candidate,
|
|
313
|
+
usageKey,
|
|
314
|
+
usageSnapshot: usage?.snapshot ?? null,
|
|
315
|
+
usageError: usage?.error ?? null,
|
|
316
|
+
};
|
|
318
317
|
});
|
|
319
318
|
}
|
|
320
319
|
/**
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -134,12 +134,25 @@ export interface RunMeta {
|
|
|
134
134
|
startedAt: string;
|
|
135
135
|
completedAt: string | null;
|
|
136
136
|
exitCode: number | null;
|
|
137
|
+
/** Machine-readable failure reason when the run did not complete successfully. */
|
|
138
|
+
errorMessage?: string;
|
|
139
|
+
/** Wall-clock duration of the run in milliseconds (set when the run finishes). */
|
|
140
|
+
duration?: number;
|
|
137
141
|
/** Set for `host:`-placed runs — where the job body executes (no local pid). */
|
|
138
142
|
host?: string;
|
|
139
143
|
/** The host-task sidecar id backing a `host:` run; the daemon monitor
|
|
140
144
|
* finalizes the run by reconciling it against the remote `.exit`. */
|
|
141
145
|
hostTaskId?: string;
|
|
142
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Finalize a run record with a terminal status, computing `duration` from
|
|
149
|
+
* `startedAt` and the completion timestamp. Keeps failure-reason population
|
|
150
|
+
* centralized so every completion path writes the same machine-readable fields.
|
|
151
|
+
*/
|
|
152
|
+
export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status'], exitCode: number | null, opts?: {
|
|
153
|
+
errorMessage?: string;
|
|
154
|
+
completedAt?: string;
|
|
155
|
+
}): void;
|
|
143
156
|
/**
|
|
144
157
|
* True when the job may execute on this machine: no `devices` allowlist (or
|
|
145
158
|
* empty), or the allowlist includes this device. Both sides go through
|
package/dist/lib/routines.js
CHANGED
|
@@ -50,6 +50,27 @@ export function normalizeTriggerEvent(input) {
|
|
|
50
50
|
};
|
|
51
51
|
return aliases[key] ?? null;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Finalize a run record with a terminal status, computing `duration` from
|
|
55
|
+
* `startedAt` and the completion timestamp. Keeps failure-reason population
|
|
56
|
+
* centralized so every completion path writes the same machine-readable fields.
|
|
57
|
+
*/
|
|
58
|
+
export function finalizeRunMeta(meta, status, exitCode, opts) {
|
|
59
|
+
meta.status = status;
|
|
60
|
+
meta.exitCode = exitCode;
|
|
61
|
+
meta.completedAt = opts?.completedAt ?? new Date().toISOString();
|
|
62
|
+
const started = Date.parse(meta.startedAt);
|
|
63
|
+
const completed = Date.parse(meta.completedAt);
|
|
64
|
+
meta.duration = Number.isFinite(started) && Number.isFinite(completed) && completed >= started
|
|
65
|
+
? completed - started
|
|
66
|
+
: 0;
|
|
67
|
+
if (opts?.errorMessage) {
|
|
68
|
+
meta.errorMessage = opts.errorMessage;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
delete meta.errorMessage;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
53
74
|
/**
|
|
54
75
|
* True when the job may execute on this machine: no `devices` allowlist (or
|
|
55
76
|
* empty), or the allowlist includes this device. Both sides go through
|