@phnx-labs/agents-cli 1.20.66 → 1.20.68
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 +88 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.js +45 -1
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/ssh.js +103 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +40 -2
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +12 -8
- package/dist/lib/agents.d.ts +11 -5
- package/dist/lib/agents.js +35 -24
- 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/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- 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/exec.d.ts +15 -0
- package/dist/lib/exec.js +31 -6
- 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/resources/mcp.js +20 -342
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +24 -47
- package/dist/lib/secrets/agent.js +11 -15
- 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/types.d.ts +10 -0
- package/dist/lib/usage.d.ts +17 -3
- package/dist/lib/usage.js +63 -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/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
|
package/dist/lib/runner.js
CHANGED
|
@@ -17,12 +17,12 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import * as os from 'os';
|
|
20
|
-
import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, checkJobDeviceEligibility, } from './routines.js';
|
|
20
|
+
import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
|
|
21
21
|
import { getRunsDir } from './state.js';
|
|
22
22
|
import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
23
23
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
24
24
|
import { createTimer, maybeRotate, redactPrompt } from './events.js';
|
|
25
|
-
import { normalizeMode, buildExecEnv, detectRateLimit, } from './exec.js';
|
|
25
|
+
import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, } from './exec.js';
|
|
26
26
|
import { loadTask as loadHostTask } from './hosts/tasks.js';
|
|
27
27
|
import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
|
|
28
28
|
import { backgroundSpawnOptions } from './platform/process.js';
|
|
@@ -129,12 +129,11 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
129
129
|
// kimi daemon jobs always run headless via `--prompt`, which cannot be
|
|
130
130
|
// combined with any startup-mode flag (--plan/--auto/--yolo all abort with
|
|
131
131
|
// "Cannot combine --prompt with --X"). edit/auto/skip reduce to kimi's default
|
|
132
|
-
// headless auto-run, so emit no flag
|
|
133
|
-
// equivalent, so
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
132
|
+
// headless auto-run, so emit no flag. plan has no headless read-only
|
|
133
|
+
// equivalent, so resolveHeadlessMode downgrades a plan request to auto with a
|
|
134
|
+
// stderr warning (kimi's headlessPlan is false) — routines run headless, so
|
|
135
|
+
// interactive is always false here. The returned mode carries no flag either.
|
|
136
|
+
resolveHeadlessMode('kimi', mode, false);
|
|
138
137
|
appendModelAndReasoning(cmd, config);
|
|
139
138
|
}
|
|
140
139
|
if (config.agent === 'droid') {
|
|
@@ -530,9 +529,8 @@ export async function executeJob(config, deps) {
|
|
|
530
529
|
agent: effectiveAgent,
|
|
531
530
|
version: primaryVersion,
|
|
532
531
|
}, deps);
|
|
533
|
-
|
|
534
|
-
meta.
|
|
535
|
-
meta.exitCode = loopResult.stoppedBy === 'error' ? 1 : 0;
|
|
532
|
+
const loopFailed = loopResult.stoppedBy === 'error';
|
|
533
|
+
finalizeRunMeta(meta, loopFailed ? 'failed' : 'completed', loopFailed ? 1 : 0, loopFailed ? { errorMessage: `loop stopped: ${loopResult.stoppedBy}` } : undefined);
|
|
536
534
|
writeRunMeta(meta);
|
|
537
535
|
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
538
536
|
return { meta, reportPath: null };
|
|
@@ -575,17 +573,14 @@ export async function executeJob(config, deps) {
|
|
|
575
573
|
meta.pid = attempt.pid;
|
|
576
574
|
writeRunMeta(meta);
|
|
577
575
|
if (attempt.status === 'timeout') {
|
|
578
|
-
meta
|
|
579
|
-
meta.completedAt = new Date().toISOString();
|
|
576
|
+
finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'run timed out' });
|
|
580
577
|
writeRunMeta(meta);
|
|
581
578
|
timer.end({ status: 'timeout', runId });
|
|
582
579
|
const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
583
580
|
return { meta, reportPath };
|
|
584
581
|
}
|
|
585
582
|
if (attempt.status === 'completed') {
|
|
586
|
-
meta
|
|
587
|
-
meta.status = 'completed';
|
|
588
|
-
meta.completedAt = new Date().toISOString();
|
|
583
|
+
finalizeRunMeta(meta, 'completed', 0);
|
|
589
584
|
writeRunMeta(meta);
|
|
590
585
|
timer.end({ status: 'completed', exitCode: 0, runId });
|
|
591
586
|
const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
@@ -604,9 +599,7 @@ export async function executeJob(config, deps) {
|
|
|
604
599
|
if (attempt.error) {
|
|
605
600
|
process.stderr.write(`[agents] routine ${config.name}: spawn failed for ${label}: ${attempt.error}\n`);
|
|
606
601
|
}
|
|
607
|
-
meta
|
|
608
|
-
meta.status = 'failed';
|
|
609
|
-
meta.completedAt = new Date().toISOString();
|
|
602
|
+
finalizeRunMeta(meta, 'failed', attempt.exitCode ?? 1, attempt.error ? { errorMessage: attempt.error } : undefined);
|
|
610
603
|
writeRunMeta(meta);
|
|
611
604
|
timer.end({
|
|
612
605
|
status: 'failed',
|
|
@@ -618,9 +611,7 @@ export async function executeJob(config, deps) {
|
|
|
618
611
|
return { meta, reportPath };
|
|
619
612
|
}
|
|
620
613
|
// Unreachable: chain is always non-empty, but keep a safe fallback.
|
|
621
|
-
meta
|
|
622
|
-
meta.exitCode = 1;
|
|
623
|
-
meta.completedAt = new Date().toISOString();
|
|
614
|
+
finalizeRunMeta(meta, 'failed', 1);
|
|
624
615
|
writeRunMeta(meta);
|
|
625
616
|
timer.end({ status: 'failed', exitCode: 1, runId });
|
|
626
617
|
return { meta, reportPath: null };
|
|
@@ -677,9 +668,7 @@ async function executeJobOnHost(config, opts) {
|
|
|
677
668
|
// Sync path: a real exit code finalizes now. -1 (follow window closed) and
|
|
678
669
|
// the detached path leave the meta `running` for the monitor to reconcile.
|
|
679
670
|
if (!opts.detached && exitCode !== null && exitCode !== undefined && exitCode !== -1) {
|
|
680
|
-
meta
|
|
681
|
-
meta.exitCode = exitCode;
|
|
682
|
-
meta.completedAt = new Date().toISOString();
|
|
671
|
+
finalizeRunMeta(meta, exitCode === 0 ? 'completed' : 'failed', exitCode);
|
|
683
672
|
}
|
|
684
673
|
writeRunMeta(meta);
|
|
685
674
|
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
@@ -756,9 +745,7 @@ async function executeCommandJobForeground(config) {
|
|
|
756
745
|
finish({ exitCode: 1, status: 'failed', error: err.message });
|
|
757
746
|
});
|
|
758
747
|
});
|
|
759
|
-
meta.status
|
|
760
|
-
meta.exitCode = result.exitCode ?? (result.status === 'completed' ? 0 : 1);
|
|
761
|
-
meta.completedAt = new Date().toISOString();
|
|
748
|
+
finalizeRunMeta(meta, result.status, result.exitCode ?? (result.status === 'completed' ? 0 : 1), result.error ? { errorMessage: result.error } : undefined);
|
|
762
749
|
writeRunMeta(meta);
|
|
763
750
|
if (result.error) {
|
|
764
751
|
process.stderr.write(`[agents] routine ${config.name}: command spawn failed: ${result.error}\n`);
|
|
@@ -848,9 +835,7 @@ export async function executeJobDetached(config) {
|
|
|
848
835
|
fs.closeSync(stdoutFd);
|
|
849
836
|
}
|
|
850
837
|
catch { /* fd already closed */ }
|
|
851
|
-
meta
|
|
852
|
-
meta.exitCode = 1;
|
|
853
|
-
meta.completedAt = new Date().toISOString();
|
|
838
|
+
finalizeRunMeta(meta, 'failed', 1, { errorMessage: err.message });
|
|
854
839
|
writeRunMeta(meta);
|
|
855
840
|
process.stderr.write(`[agents] daemon: spawn failed for job "${config.name}": ${err.message}\n`);
|
|
856
841
|
});
|
|
@@ -908,18 +893,16 @@ function executeCommandJobDetached(config) {
|
|
|
908
893
|
// fire-and-forget call, so the exit event fires here. (monitorRunningJobs no
|
|
909
894
|
// longer force-fails command jobs; it reads exit-code only on the restart edge.)
|
|
910
895
|
let settled = false;
|
|
911
|
-
const settle = (status, exitCode) => {
|
|
896
|
+
const settle = (status, exitCode, errorMessage) => {
|
|
912
897
|
if (settled)
|
|
913
898
|
return;
|
|
914
899
|
settled = true;
|
|
915
|
-
meta
|
|
916
|
-
meta.exitCode = exitCode;
|
|
917
|
-
meta.completedAt = new Date().toISOString();
|
|
900
|
+
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
918
901
|
writeRunMeta(meta);
|
|
919
902
|
};
|
|
920
903
|
child.on('exit', (code) => settle(code === 0 ? 'completed' : 'failed', code ?? 1));
|
|
921
904
|
child.on('error', (err) => {
|
|
922
|
-
settle('failed', 1);
|
|
905
|
+
settle('failed', 1, err.message);
|
|
923
906
|
process.stderr.write(`[agents] daemon: command spawn failed for job "${config.name}": ${err.message}\n`);
|
|
924
907
|
});
|
|
925
908
|
child.unref();
|
|
@@ -1073,9 +1056,7 @@ function finalizeHostRun(meta) {
|
|
|
1073
1056
|
const healed = reconcileHostTask(task);
|
|
1074
1057
|
if (healed.status !== 'completed' && healed.status !== 'failed')
|
|
1075
1058
|
return;
|
|
1076
|
-
meta.status
|
|
1077
|
-
meta.exitCode = healed.exitCode ?? (healed.status === 'completed' ? 0 : 1);
|
|
1078
|
-
meta.completedAt = healed.finishedAt ?? new Date().toISOString();
|
|
1059
|
+
finalizeRunMeta(meta, healed.status, healed.exitCode ?? (healed.status === 'completed' ? 0 : 1), { completedAt: healed.finishedAt ?? undefined });
|
|
1079
1060
|
writeRunMeta(meta);
|
|
1080
1061
|
}
|
|
1081
1062
|
catch { /* unreachable host or unreadable sidecar — retry next sweep */ }
|
|
@@ -1115,8 +1096,7 @@ export function monitorRunningJobs() {
|
|
|
1115
1096
|
const isCommandRun = Boolean(meta.command) || !meta.agent;
|
|
1116
1097
|
const wallClockMs = Date.now() - Date.parse(meta.startedAt);
|
|
1117
1098
|
if (Number.isFinite(wallClockMs) && wallClockMs > MAX_WALL_CLOCK_MS) {
|
|
1118
|
-
meta
|
|
1119
|
-
meta.completedAt = new Date().toISOString();
|
|
1099
|
+
finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'exceeded max wall clock' });
|
|
1120
1100
|
writeRunMeta(meta);
|
|
1121
1101
|
if (!isCommandRun)
|
|
1122
1102
|
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
@@ -1130,20 +1110,17 @@ export function monitorRunningJobs() {
|
|
|
1130
1110
|
// missed the exit event — recover the true code from the exit-code file
|
|
1131
1111
|
// the child wrote; its absence means the child was killed/crashed.
|
|
1132
1112
|
const ec = readCommandExitCode(runDirPath);
|
|
1133
|
-
meta
|
|
1134
|
-
meta.exitCode = ec;
|
|
1113
|
+
finalizeRunMeta(meta, ec === 0 ? 'completed' : 'failed', ec);
|
|
1135
1114
|
}
|
|
1136
1115
|
else {
|
|
1137
1116
|
const inferred = inferFinalStatusFromLog(stdoutPath, meta.agent);
|
|
1138
1117
|
if (inferred) {
|
|
1139
|
-
meta.status
|
|
1140
|
-
meta.exitCode = inferred.exitCode;
|
|
1118
|
+
finalizeRunMeta(meta, inferred.status, inferred.exitCode);
|
|
1141
1119
|
}
|
|
1142
1120
|
else {
|
|
1143
|
-
meta
|
|
1121
|
+
finalizeRunMeta(meta, 'failed', null, { errorMessage: 'process exited before final status could be inferred' });
|
|
1144
1122
|
}
|
|
1145
1123
|
}
|
|
1146
|
-
meta.completedAt = new Date().toISOString();
|
|
1147
1124
|
writeRunMeta(meta);
|
|
1148
1125
|
if (!isCommandRun)
|
|
1149
1126
|
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
@@ -33,6 +33,7 @@ import { getHelpersDir, readMeta } from '../state.js';
|
|
|
33
33
|
import { isAlive } from '../platform/process.js';
|
|
34
34
|
import { getKeychainHelperPath } from './install-helper.js';
|
|
35
35
|
import { getCliVersion, getCliVersionFresh } from '../version.js';
|
|
36
|
+
import { getCliLaunch } from '../cli-entry.js';
|
|
36
37
|
/** Bumped when the wire protocol changes; a client that pings a mismatched
|
|
37
38
|
* server kills and respawns it rather than talking a stale dialect. */
|
|
38
39
|
const PROTOCOL_VERSION = 1;
|
|
@@ -148,23 +149,18 @@ function writeAgentToken() {
|
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
150
151
|
* Argv for re-invoking THIS cli with a hidden subcommand, so a side-by-side dev
|
|
151
|
-
* build spawns its own helpers rather than the registry-installed one.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `
|
|
152
|
+
* build spawns its own helpers rather than the registry-installed one. Routed
|
|
153
|
+
* through the shared getCliLaunch so it handles both install shapes — a JS entry
|
|
154
|
+
* (`node dist/index.js …`) and a Bun standalone binary (run directly). The old
|
|
155
|
+
* hand-rolled `[process.execPath, process.argv[1], …]` broke on standalone builds:
|
|
156
|
+
* process.argv[1] is the bun virtual entry `/$bunfs/root/agents` (fs.existsSync
|
|
157
|
+
* reports it as present), so it was passed as an argv element and the broker died
|
|
158
|
+
* with `unknown command '/$bunfs/root/agents'` — the daemon-hosted broker never
|
|
159
|
+
* bound its socket and every unlock reported "Could not start the secrets broker".
|
|
155
160
|
*/
|
|
156
161
|
function cliSpawn(sub) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (entry)
|
|
160
|
-
return { cmd: process.execPath, args: [entry, ...sub] };
|
|
161
|
-
// No resolvable entrypoint (unusual) — fall back to the PATH shim.
|
|
162
|
-
let bin = 'agents';
|
|
163
|
-
try {
|
|
164
|
-
bin = execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
165
|
-
}
|
|
166
|
-
catch { /* default */ }
|
|
167
|
-
return { cmd: bin, args: sub };
|
|
162
|
+
const { command, args } = getCliLaunch(sub);
|
|
163
|
+
return { cmd: command, args };
|
|
168
164
|
}
|
|
169
165
|
function brokerSpawn() {
|
|
170
166
|
return cliSpawn(['secrets', '_agent-run']);
|