@phnx-labs/agents-cli 1.22.74 → 1.22.76
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 +152 -0
- package/README.md +21 -9
- package/dist/bootstrap.js +7 -7
- package/dist/cli/command-registry.js +5 -0
- package/dist/commands/artifacts-setup.js +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/auth.js +7 -1
- package/dist/commands/browser.js +104 -10
- package/dist/commands/commands.js +7 -6
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +26 -7
- package/dist/commands/config.js +27 -4
- package/dist/commands/cost.js +6 -4
- package/dist/commands/doctor.d.ts +6 -5
- package/dist/commands/doctor.js +32 -274
- package/dist/commands/exec.d.ts +2 -0
- package/dist/commands/exec.js +9 -2
- package/dist/commands/harness.d.ts +1 -0
- package/dist/commands/harness.js +11 -3
- package/dist/commands/hooks.js +7 -6
- package/dist/commands/mcp.js +7 -6
- package/dist/commands/memory.js +7 -7
- package/dist/commands/monitors.js +3 -2
- package/dist/commands/open.d.ts +25 -12
- package/dist/commands/open.js +24 -10
- package/dist/commands/permissions.js +7 -6
- package/dist/commands/plugins.js +21 -17
- package/dist/commands/route.js +33 -16
- package/dist/commands/rules.js +7 -12
- package/dist/commands/sessions-share.js +1 -1
- package/dist/commands/setup-watchdog.js +2 -2
- package/dist/commands/setup.js +22 -1
- package/dist/commands/share.js +26 -10
- package/dist/commands/skills.js +7 -6
- package/dist/commands/subagents.js +7 -6
- package/dist/commands/sync.js +81 -10
- package/dist/commands/view.js +4 -1
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +10 -10
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +22 -4
- package/dist/commands/workflows.js +7 -6
- package/dist/lib/account-registry.js +27 -6
- package/dist/lib/accounting/rotate.d.ts +3 -1
- package/dist/lib/accounting/rotate.js +8 -4
- package/dist/lib/auth-health.d.ts +2 -0
- package/dist/lib/auth-health.js +2 -0
- package/dist/lib/browser/chrome.d.ts +21 -0
- package/dist/lib/browser/chrome.js +60 -3
- package/dist/lib/browser/drivers/local.d.ts +21 -0
- package/dist/lib/browser/drivers/local.js +102 -9
- package/dist/lib/browser/profiles.d.ts +29 -1
- package/dist/lib/browser/profiles.js +50 -1
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/computer/computer-rpc.d.ts +6 -1
- package/dist/lib/computer/computer-rpc.js +23 -3
- package/dist/lib/computer/des.d.ts +1 -0
- package/dist/lib/computer/des.js +114 -0
- package/dist/lib/computer/rfb-client.d.ts +53 -0
- package/dist/lib/computer/rfb-client.js +562 -0
- package/dist/lib/config-keys.d.ts +7 -2
- package/dist/lib/config-keys.js +17 -2
- package/dist/lib/daemon/auth-sync-service.js +3 -0
- package/dist/lib/daemon/daemon.js +17 -10
- package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
- package/dist/lib/daemon/session-summarizer-service.js +39 -0
- package/dist/lib/daemon/usage-sync-service.js +3 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.d.ts +2 -2
- package/dist/lib/daemon-ticks.js +2 -1
- package/dist/lib/daemon-webhooks.js +15 -2
- package/dist/lib/deeplink/register.js +10 -9
- package/dist/lib/deeplink/url.d.ts +4 -4
- package/dist/lib/deeplink/url.js +4 -4
- package/dist/lib/device-config.js +25 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -4
- package/dist/lib/devices/doctor-findings.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/stats-cache.d.ts +4 -0
- package/dist/lib/devices/stats-cache.js +19 -0
- package/dist/lib/drift-sync.d.ts +3 -1
- package/dist/lib/drift-sync.js +16 -5
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +16 -1
- package/dist/lib/fleet-shared-repo-sync.d.ts +12 -0
- package/dist/lib/fleet-shared-repo-sync.js +101 -4
- package/dist/lib/fleet-shared-state.d.ts +8 -0
- package/dist/lib/heal.d.ts +4 -3
- package/dist/lib/heal.js +5 -4
- package/dist/lib/hosts/ready.d.ts +1 -1
- package/dist/lib/hosts/ready.js +16 -4
- package/dist/lib/hosts/reconnect.js +4 -2
- package/dist/lib/identity/client.d.ts +6 -0
- package/dist/lib/identity/index.d.ts +16 -0
- package/dist/lib/identity/index.js +25 -1
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +28 -9
- package/dist/lib/reconcile-and-repair.d.ts +109 -0
- package/dist/lib/reconcile-and-repair.js +267 -0
- package/dist/lib/routers.d.ts +12 -1
- package/dist/lib/routers.js +30 -1
- package/dist/lib/scheduling/routines.js +8 -2
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/db.d.ts +47 -7
- package/dist/lib/session/db.js +114 -12
- package/dist/lib/session/mirror.js +58 -0
- package/dist/lib/session/remote/remote-list.d.ts +2 -0
- package/dist/lib/session/remote/remote-list.js +4 -0
- package/dist/lib/session/remote/watch.js +22 -2
- package/dist/lib/session/session-cache.d.ts +19 -0
- package/dist/lib/session/session-cache.js +46 -0
- package/dist/lib/session/types.d.ts +34 -0
- package/dist/lib/share/backend.d.ts +6 -4
- package/dist/lib/share/backend.js +10 -8
- package/dist/lib/share/config.d.ts +4 -3
- package/dist/lib/share/config.js +10 -1
- package/dist/lib/share/delete.d.ts +1 -1
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/html.d.ts +1 -1
- package/dist/lib/share/html.js +1 -1
- package/dist/lib/share/provision.d.ts +1 -1
- package/dist/lib/share/provision.js +2 -2
- package/dist/lib/share/publish.d.ts +23 -7
- package/dist/lib/share/publish.js +58 -12
- package/dist/lib/share/worker-template.js +221 -60
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/state.d.ts +15 -0
- package/dist/lib/state.js +29 -7
- package/dist/lib/summarizer/config.d.ts +46 -0
- package/dist/lib/summarizer/config.js +83 -0
- package/dist/lib/summarizer/pass.d.ts +45 -0
- package/dist/lib/summarizer/pass.js +112 -0
- package/dist/lib/summarizer/summarize.d.ts +68 -0
- package/dist/lib/summarizer/summarize.js +120 -0
- package/dist/lib/teams/agents.d.ts +4 -3
- package/dist/lib/teams/agents.js +12 -4
- package/dist/lib/teams/scheduler.d.ts +4 -2
- package/dist/lib/teams/scheduler.js +6 -6
- package/dist/lib/tmux/session.d.ts +2 -0
- package/dist/lib/tmux/session.js +7 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/verbs.d.ts +23 -0
- package/dist/lib/verbs.js +24 -0
- package/dist/lib/view-types.d.ts +4 -0
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ import { isSchedulerEnabled, assertSchedulerEnabled, isDaemonEnabled } from '../
|
|
|
32
32
|
import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, readSubsystemHealth, SUBSYSTEM_DAEMON_START } from '../daemon-health.js';
|
|
33
33
|
import { ServiceSupervisor } from './supervisor.js';
|
|
34
34
|
import { SessionIndexService } from './session-index-service.js';
|
|
35
|
+
import { SessionSummarizerService } from './session-summarizer-service.js';
|
|
35
36
|
import { SecretsBrokerService } from './secrets-broker-service.js';
|
|
36
37
|
import { MonitorEngineService } from './monitor-engine-service.js';
|
|
37
38
|
import { AccountUsageService, AccountAuthService } from './account-state-daemon-service.js';
|
|
@@ -271,7 +272,7 @@ function acquireStartLock() {
|
|
|
271
272
|
*/
|
|
272
273
|
const STOP_LOCK_WAIT_MS = 10_000;
|
|
273
274
|
const STOP_LOCK_POLL_MS = 50;
|
|
274
|
-
function
|
|
275
|
+
function acquireLifecycleLock() {
|
|
275
276
|
const deadline = Date.now() + STOP_LOCK_WAIT_MS;
|
|
276
277
|
for (;;) {
|
|
277
278
|
const release = acquireStartLock();
|
|
@@ -481,14 +482,13 @@ export function isDaemonRunning() {
|
|
|
481
482
|
* liveness check and the write.
|
|
482
483
|
*/
|
|
483
484
|
export function claimDaemonInstance() {
|
|
484
|
-
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
// claimer we bailed for becomes the singleton, so last-wins still holds.
|
|
485
|
+
// A stop owns this same lock through teardown. Waiting here is load-bearing:
|
|
486
|
+
// returning false while stopDaemon() holds it lets this replacement exit 0,
|
|
487
|
+
// then the stop completes with no singleton left alive. The bounded lifecycle
|
|
488
|
+
// acquisition also preserves concurrent-start serialization: after the first
|
|
489
|
+
// claimer publishes its pid, the waiter takes the lock and performs the normal
|
|
490
|
+
// last-wins takeover rather than ever running the read-evict-write unlocked.
|
|
491
|
+
const release = acquireLifecycleLock();
|
|
492
492
|
if (!release)
|
|
493
493
|
return false;
|
|
494
494
|
try {
|
|
@@ -1063,6 +1063,13 @@ export async function runDaemon() {
|
|
|
1063
1063
|
supervisor.register(new SessionIndexService());
|
|
1064
1064
|
else
|
|
1065
1065
|
log('INFO', 'Session-index warm service disabled');
|
|
1066
|
+
// Session summarizer (PHNX-3939) — registered when the service toggle is on,
|
|
1067
|
+
// but each tick is a no-op unless the operator also set summarizer.enabled and
|
|
1068
|
+
// a model endpoint, so registering it costs nothing while unconfigured.
|
|
1069
|
+
if (isEnabled('session-summarizer'))
|
|
1070
|
+
supervisor.register(new SessionSummarizerService());
|
|
1071
|
+
else
|
|
1072
|
+
log('INFO', 'Session summarizer service disabled');
|
|
1066
1073
|
// Watchdog, device-probe, self-heal, and keychain-reap are all periodic
|
|
1067
1074
|
// services managed by the ServiceSupervisor (RUSH-3193 P3). Each is gated
|
|
1068
1075
|
// the same way as the socket services above; state-dir-check is registered
|
|
@@ -2205,7 +2212,7 @@ export function findSurvivingStateDirDaemons(exclude) {
|
|
|
2205
2212
|
* never reports success on an unverified stop.
|
|
2206
2213
|
*/
|
|
2207
2214
|
export function stopDaemon() {
|
|
2208
|
-
const releaseLock =
|
|
2215
|
+
const releaseLock = acquireLifecycleLock();
|
|
2209
2216
|
if (!releaseLock) {
|
|
2210
2217
|
return {
|
|
2211
2218
|
ok: false,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-summarizer service (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* The single daemon-owned executor that computes a per-session goal / progress
|
|
5
|
+
* checkpoints / checklist and writes them to the transcript-keyed
|
|
6
|
+
* `session_summaries` cache, from which the display/merge path serves them onto
|
|
7
|
+
* the `sessions watch` stream. It NEVER runs on a request path.
|
|
8
|
+
*
|
|
9
|
+
* Off by default: `runSummarizerPass` no-ops unless `summarizer.enabled` and a
|
|
10
|
+
* model endpoint (`summarizer.baseUrl` + `summarizer.model`) are configured, so a
|
|
11
|
+
* daemon with the feature unconfigured makes zero model calls. Reader-gated and
|
|
12
|
+
* bounded per tick like the other session services, so it costs nothing while no
|
|
13
|
+
* one is watching.
|
|
14
|
+
*/
|
|
15
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
16
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
17
|
+
export declare class SessionSummarizerService extends BasePeriodicService {
|
|
18
|
+
readonly id: DaemonServiceId;
|
|
19
|
+
readonly intervalMs = 20000;
|
|
20
|
+
readonly deadlineMs = 60000;
|
|
21
|
+
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
22
|
+
protected onStop(): Promise<void>;
|
|
23
|
+
protected onTick(ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-summarizer service (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* The single daemon-owned executor that computes a per-session goal / progress
|
|
5
|
+
* checkpoints / checklist and writes them to the transcript-keyed
|
|
6
|
+
* `session_summaries` cache, from which the display/merge path serves them onto
|
|
7
|
+
* the `sessions watch` stream. It NEVER runs on a request path.
|
|
8
|
+
*
|
|
9
|
+
* Off by default: `runSummarizerPass` no-ops unless `summarizer.enabled` and a
|
|
10
|
+
* model endpoint (`summarizer.baseUrl` + `summarizer.model`) are configured, so a
|
|
11
|
+
* daemon with the feature unconfigured makes zero model calls. Reader-gated and
|
|
12
|
+
* bounded per tick like the other session services, so it costs nothing while no
|
|
13
|
+
* one is watching.
|
|
14
|
+
*/
|
|
15
|
+
import { BasePeriodicService } from './service.js';
|
|
16
|
+
import { runSummarizerPass } from '../summarizer/pass.js';
|
|
17
|
+
/** Slower than the state tick — a summary is a coarse signal, not live status. */
|
|
18
|
+
const SESSION_SUMMARIZER_TICK_MS = 20_000;
|
|
19
|
+
/** Hard cap per tick; a local model call is bounded, this leaves generous headroom. */
|
|
20
|
+
const SESSION_SUMMARIZER_DEADLINE_MS = 60_000;
|
|
21
|
+
export class SessionSummarizerService extends BasePeriodicService {
|
|
22
|
+
id = 'session-summarizer';
|
|
23
|
+
intervalMs = SESSION_SUMMARIZER_TICK_MS;
|
|
24
|
+
deadlineMs = SESSION_SUMMARIZER_DEADLINE_MS;
|
|
25
|
+
async onStart(_ctx) {
|
|
26
|
+
// No handles to open — each tick reads config + the warm session cache fresh.
|
|
27
|
+
}
|
|
28
|
+
async onStop() {
|
|
29
|
+
// Nothing to release.
|
|
30
|
+
}
|
|
31
|
+
async onTick(ctx, signal) {
|
|
32
|
+
const r = await runSummarizerPass({ signal });
|
|
33
|
+
if (r.disabled)
|
|
34
|
+
return; // off / unconfigured — stay silent
|
|
35
|
+
if (r.computed > 0 || r.skipped > 0) {
|
|
36
|
+
ctx.log('INFO', `session-summarizer: computed ${r.computed}, skipped ${r.skipped}, reused ${r.reused}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -48,6 +48,9 @@ export class UsageSyncService extends BasePeriodicService {
|
|
|
48
48
|
ctx.log('WARN', `usage-sync: ${transport.skipped}`);
|
|
49
49
|
if (transport.error)
|
|
50
50
|
ctx.log('WARN', `usage-sync: shared-store transport: ${transport.error}`);
|
|
51
|
+
if (transport.untrackedBackedUp?.length) {
|
|
52
|
+
ctx.log('WARN', `usage-sync: backed up ${transport.untrackedBackedUp.length} untracked shared-store collision(s) to ${transport.untrackedBackupDir}: ${transport.untrackedBackedUp.join(', ')}`);
|
|
53
|
+
}
|
|
51
54
|
if (!transport.success)
|
|
52
55
|
return;
|
|
53
56
|
const consumed = consumeUsageSnapshotsFromSharedStore();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* enabled without pulling in the whole daemon lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
/** Every service the daemon can host. IDs are kebab-case and stable. */
|
|
10
|
-
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'keychain-reap' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
|
|
10
|
+
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'keychain-reap' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'session-summarizer';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -107,6 +107,11 @@ export const DAEMON_SERVICES = [
|
|
|
107
107
|
title: 'Session-index warm',
|
|
108
108
|
description: 'Keeps this host\'s transcript index current so a locally-started session is discoverable within seconds.',
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
id: 'session-summarizer',
|
|
112
|
+
title: 'Session summarizer',
|
|
113
|
+
description: 'Computes a per-session goal / progress checkpoints / checklist off the request path and delivers them on the session stream. Off unless summarizer.enabled and a local model endpoint are configured (PHNX-3939).',
|
|
114
|
+
},
|
|
110
115
|
{
|
|
111
116
|
id: 'auth-sync',
|
|
112
117
|
title: 'Auth bundle sync',
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* timer converge on the same published result.
|
|
16
16
|
*/
|
|
17
17
|
import type { FleetStatusRow } from './fleet-status.js';
|
|
18
|
-
import type
|
|
18
|
+
import { type AuthProbeRow } from './auth-health.js';
|
|
19
|
+
export { AUTH_PROBE_MAX_AGE_MS } from './auth-health.js';
|
|
19
20
|
export declare function isFreshFleetAuthSnapshot(value: {
|
|
20
21
|
row: FleetStatusRow;
|
|
21
22
|
authRows: AuthProbeRow[];
|
|
@@ -34,7 +35,6 @@ export declare function isFreshFleetAuthSnapshot(value: {
|
|
|
34
35
|
* that would blind every non-primary box to revocation. Fleet status still
|
|
35
36
|
* publishes every tick — it does not ride that endpoint.
|
|
36
37
|
*/
|
|
37
|
-
export declare const AUTH_PROBE_MAX_AGE_MS: number;
|
|
38
38
|
/**
|
|
39
39
|
* True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
|
|
40
40
|
* — i.e. reusing them would not let a verdict get staler than one probe window.
|
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* by the cross-process refresh lease so an explicit CLI refresh and the daemon
|
|
15
15
|
* timer converge on the same published result.
|
|
16
16
|
*/
|
|
17
|
+
import { AUTH_PROBE_MAX_AGE_MS } from './auth-health.js';
|
|
18
|
+
export { AUTH_PROBE_MAX_AGE_MS } from './auth-health.js';
|
|
17
19
|
export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
|
|
18
20
|
return value.row.capturedAt >= minimumCapturedAt
|
|
19
21
|
&& value.authRows.length > 0
|
|
@@ -33,7 +35,6 @@ export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
|
|
|
33
35
|
* that would blind every non-primary box to revocation. Fleet status still
|
|
34
36
|
* publishes every tick — it does not ride that endpoint.
|
|
35
37
|
*/
|
|
36
|
-
export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
|
|
37
38
|
/**
|
|
38
39
|
* True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
|
|
39
40
|
* — i.e. reusing them would not let a verdict get staler than one probe window.
|
|
@@ -18,6 +18,7 @@ import * as yaml from 'yaml';
|
|
|
18
18
|
import { exec } from 'child_process';
|
|
19
19
|
import { getDaemonConfigDir, getRuntimeStateDir } from './state.js';
|
|
20
20
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
21
|
+
import { closeServerBounded } from './secrets/agent.js';
|
|
21
22
|
import { readAndResolveBundleEnv } from './secrets/bundles.js';
|
|
22
23
|
import { startWebhookServer, createFileDeliveryStore, waitForListening } from './triggers/webhook.js';
|
|
23
24
|
import { buildFunnelUpCommand, FUNNEL_PORTS } from './funnel.js';
|
|
@@ -195,8 +196,13 @@ export async function startHostedWebhookReceivers(opts) {
|
|
|
195
196
|
log('WARN', `webhook ${webhook.source}:${webhook.event} dispatch failed after ack: ${err.message}`);
|
|
196
197
|
},
|
|
197
198
|
});
|
|
199
|
+
const sockets = new Set();
|
|
200
|
+
server.on('connection', (socket) => {
|
|
201
|
+
sockets.add(socket);
|
|
202
|
+
socket.once('close', () => sockets.delete(socket));
|
|
203
|
+
});
|
|
198
204
|
await waitForListening(server);
|
|
199
|
-
servers.push(server);
|
|
205
|
+
servers.push({ server, sockets });
|
|
200
206
|
log('INFO', `webhook receiver bound on 127.0.0.1:${port} (bundle ${receiver.bundle})`);
|
|
201
207
|
if (receiver.funnel)
|
|
202
208
|
reconcileFunnel(receiver.funnel.publicPort, port, log);
|
|
@@ -210,6 +216,13 @@ export async function startHostedWebhookReceivers(opts) {
|
|
|
210
216
|
}
|
|
211
217
|
return {
|
|
212
218
|
count: servers.length,
|
|
213
|
-
close: () => Promise.all(servers.map(
|
|
219
|
+
close: () => Promise.all(servers.map(async ({ server, sockets }) => {
|
|
220
|
+
// Stop accepting first, then force every persistent HTTP connection
|
|
221
|
+
// closed so keep-alive cannot hold daemon shutdown open indefinitely.
|
|
222
|
+
const closing = closeServerBounded(server);
|
|
223
|
+
for (const socket of sockets)
|
|
224
|
+
socket.destroy();
|
|
225
|
+
await closing;
|
|
226
|
+
})).then(() => undefined),
|
|
214
227
|
};
|
|
215
228
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Register the `agents://` URL scheme with the OS so a click in an artifact
|
|
3
|
-
* routes to `agents
|
|
3
|
+
* routes to the machine-only `agents _callback <url>` verb (see url.ts +
|
|
4
|
+
* commands/open.ts). Humans manage the handler with `agents setup url-scheme`.
|
|
4
5
|
*
|
|
5
6
|
* A browser page cannot spawn a shell; a registered URL scheme is the
|
|
6
7
|
* OS-sanctioned hand-off. Each platform gets its own handler:
|
|
@@ -13,8 +14,8 @@
|
|
|
13
14
|
*
|
|
14
15
|
* The content generators below are pure and unit-tested. The `register*` /
|
|
15
16
|
* `unregister*` / `status*` functions apply them and never throw — they return a
|
|
16
|
-
* {@link SchemeStatus} so callers (setup, `agents
|
|
17
|
-
* report without a try/catch.
|
|
17
|
+
* {@link SchemeStatus} so callers (setup, `agents setup url-scheme register`,
|
|
18
|
+
* doctor) can report without a try/catch.
|
|
18
19
|
*/
|
|
19
20
|
import { execFileSync } from 'node:child_process';
|
|
20
21
|
import * as fs from 'node:fs';
|
|
@@ -73,7 +74,7 @@ export function linuxDesktopEntry(invocation) {
|
|
|
73
74
|
'Type=Application',
|
|
74
75
|
'Name=Agents URL Handler',
|
|
75
76
|
'Comment=Resume an agents session from an agents:// deep link',
|
|
76
|
-
`Exec=${invocation}
|
|
77
|
+
`Exec=${invocation} _callback %u`,
|
|
77
78
|
'Terminal=false',
|
|
78
79
|
'NoDisplay=true',
|
|
79
80
|
'MimeType=x-scheme-handler/agents;',
|
|
@@ -91,7 +92,7 @@ export function macAppleScriptSource(invocation) {
|
|
|
91
92
|
const literal = invocation.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
92
93
|
return [
|
|
93
94
|
'on open location this_URL',
|
|
94
|
-
`\tdo shell script "${literal}
|
|
95
|
+
`\tdo shell script "${literal} _callback " & quoted form of this_URL`,
|
|
95
96
|
'end open location',
|
|
96
97
|
].join('\n');
|
|
97
98
|
}
|
|
@@ -113,7 +114,7 @@ export function windowsRegistryCommands(invocation) {
|
|
|
113
114
|
return [
|
|
114
115
|
['add', base, '/ve', '/d', 'URL:agents Protocol', '/f'],
|
|
115
116
|
['add', base, '/v', 'URL Protocol', '/d', '', '/f'],
|
|
116
|
-
['add', `${base}\\shell\\open\\command`, '/ve', '/d', `${invocation}
|
|
117
|
+
['add', `${base}\\shell\\open\\command`, '/ve', '/d', `${invocation} _callback "%1"`, '/f'],
|
|
117
118
|
];
|
|
118
119
|
}
|
|
119
120
|
// ---------------------------------------------------------------------------
|
|
@@ -134,19 +135,19 @@ export function agentsUrlSchemeStatus(platform = os.platform(), home = os.homedi
|
|
|
134
135
|
const p = linuxDesktopPath(home);
|
|
135
136
|
return fs.existsSync(p)
|
|
136
137
|
? { registered: true, platform, detail: `handler: ${p}` }
|
|
137
|
-
: { registered: false, platform, detail: `no handler (${p}) — run: agents
|
|
138
|
+
: { registered: false, platform, detail: `no handler (${p}) — run: agents setup url-scheme register` };
|
|
138
139
|
}
|
|
139
140
|
if (platform === 'darwin') {
|
|
140
141
|
const p = macAppPath(home);
|
|
141
142
|
return fs.existsSync(p)
|
|
142
143
|
? { registered: true, platform, detail: `handler: ${p}` }
|
|
143
|
-
: { registered: false, platform, detail: `no handler (${p}) — run: agents
|
|
144
|
+
: { registered: false, platform, detail: `no handler (${p}) — run: agents setup url-scheme register` };
|
|
144
145
|
}
|
|
145
146
|
if (platform === 'win32') {
|
|
146
147
|
const ok = windowsSchemeRegistered();
|
|
147
148
|
return ok
|
|
148
149
|
? { registered: true, platform, detail: 'handler: HKCU\\Software\\Classes\\agents' }
|
|
149
|
-
: { registered: false, platform, detail: 'no handler — run: agents
|
|
150
|
+
: { registered: false, platform, detail: 'no handler — run: agents setup url-scheme register' };
|
|
150
151
|
}
|
|
151
152
|
return { registered: false, platform, detail: `unsupported platform: ${platform}` };
|
|
152
153
|
}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A rendered artifact (a plan or report) embeds `agents://session/<id>` in its
|
|
5
5
|
* provenance line. Clicking it hands the URL to the OS, which routes it to the
|
|
6
|
-
* registered handler (see register.ts) that runs `agents
|
|
7
|
-
*
|
|
8
|
-
* consumes.
|
|
6
|
+
* registered handler (see register.ts) that runs the machine-only `agents _callback
|
|
7
|
+
* <url>` verb (`open` remains a hidden back-compat alias). This module turns that URL
|
|
8
|
+
* into a validated {@link AgentsSessionLink} the resume dispatcher consumes.
|
|
9
9
|
*
|
|
10
10
|
* Parsing is deliberately strict: the session id is the only thing that ever
|
|
11
|
-
* reaches a child process, and the `
|
|
11
|
+
* reaches a child process, and the `_callback` command passes it as argv (never
|
|
12
12
|
* interpolated into a shell), so a hostile URL cannot inject a command. Anything
|
|
13
13
|
* that is not `agents://session/<valid-id>` is rejected with a reason.
|
|
14
14
|
*
|
package/dist/lib/deeplink/url.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A rendered artifact (a plan or report) embeds `agents://session/<id>` in its
|
|
5
5
|
* provenance line. Clicking it hands the URL to the OS, which routes it to the
|
|
6
|
-
* registered handler (see register.ts) that runs `agents
|
|
7
|
-
*
|
|
8
|
-
* consumes.
|
|
6
|
+
* registered handler (see register.ts) that runs the machine-only `agents _callback
|
|
7
|
+
* <url>` verb (`open` remains a hidden back-compat alias). This module turns that URL
|
|
8
|
+
* into a validated {@link AgentsSessionLink} the resume dispatcher consumes.
|
|
9
9
|
*
|
|
10
10
|
* Parsing is deliberately strict: the session id is the only thing that ever
|
|
11
|
-
* reaches a child process, and the `
|
|
11
|
+
* reaches a child process, and the `_callback` command passes it as argv (never
|
|
12
12
|
* interpolated into a shell), so a hostile URL cannot inject a command. Anything
|
|
13
13
|
* that is not `agents://session/<valid-id>` is rejected with a reason.
|
|
14
14
|
*
|
|
@@ -75,6 +75,31 @@ export const CONFIG_KEYS = [
|
|
|
75
75
|
? null
|
|
76
76
|
: `auto.pool must be one of ${AUTO_POOL_MODES.join(' | ')}.`,
|
|
77
77
|
},
|
|
78
|
+
{
|
|
79
|
+
name: 'summarizer.enabled',
|
|
80
|
+
yamlKey: 'summarizerEnabled',
|
|
81
|
+
scope: 'user',
|
|
82
|
+
type: 'bool',
|
|
83
|
+
defaultValue: false,
|
|
84
|
+
description: 'Whether the daemon computes a per-session goal / progress checkpoints / checklist and delivers them on the ' +
|
|
85
|
+
'session stream (PHNX-3939). Off by default — zero model calls, no behavior change until enabled. Needs a local ' +
|
|
86
|
+
'Anthropic-wire model endpoint via summarizer.baseUrl + summarizer.model.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'summarizer.baseUrl',
|
|
90
|
+
yamlKey: 'summarizerBaseUrl',
|
|
91
|
+
scope: 'user',
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Base URL of the Anthropic-wire model endpoint the summarizer calls (Ollama / vLLM / LiteLLM), ' +
|
|
94
|
+
'e.g. http://localhost:11434. Overridden per-process by AGENTS_SUMMARIZER_BASEURL.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'summarizer.model',
|
|
98
|
+
yamlKey: 'summarizerModel',
|
|
99
|
+
scope: 'user',
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Model the summarizer requests from summarizer.baseUrl, e.g. qwen2.5:3b. Overridden per-process by AGENTS_SUMMARIZER_MODEL.',
|
|
102
|
+
},
|
|
78
103
|
{
|
|
79
104
|
name: 'browser.viewer',
|
|
80
105
|
yamlKey: 'browserViewer',
|
|
@@ -113,7 +113,7 @@ export interface LocalFindingInputs {
|
|
|
113
113
|
/** Read-only Windows OpenSSH AuthorizedKeysFile/content/ACL audit. */
|
|
114
114
|
windowsSshEnrollment?: WindowsSshEnrollmentAudit | null;
|
|
115
115
|
/** `<agent>@<version>` keys whose home is an isolated copy. Their findings are
|
|
116
|
-
* never collapsed across versions: the agent-wide `agents
|
|
116
|
+
* never collapsed across versions: the agent-wide `agents sync <agent>@all`
|
|
117
117
|
* sweep deliberately skips isolated copies, so a collapsed row would print a
|
|
118
118
|
* remediation that does not fix them. */
|
|
119
119
|
isolatedVersions?: string[];
|
|
@@ -135,14 +135,14 @@ export declare function buildLocalFindings(input: LocalFindingInputs): DoctorFin
|
|
|
135
135
|
/**
|
|
136
136
|
* Fold findings that say the SAME thing about several versions of one agent into
|
|
137
137
|
* a single row carrying `versions`, and widen its remediation to the agent-wide
|
|
138
|
-
* sweep (`agents
|
|
138
|
+
* sweep (`agents sync claude@all --yes` heals every non-isolated version in one
|
|
139
139
|
* go). Five identical `plugin 'code' — mirror missing` rows, one per installed
|
|
140
140
|
* claude, is the same fact five times.
|
|
141
141
|
*
|
|
142
142
|
* Three things never merge, because for each of them the widened remediation
|
|
143
143
|
* would be wrong:
|
|
144
|
-
* - **Isolated copies** — the agent-wide sweep deliberately skips them
|
|
145
|
-
*
|
|
144
|
+
* - **Isolated copies** — the agent-wide sweep deliberately skips them, so a
|
|
145
|
+
* folded row would print a command that leaves one broken.
|
|
146
146
|
* - **Findings with no agent** (repo-behind, rc-secret-export, …) — their
|
|
147
147
|
* `version` field is an alias, not a version.
|
|
148
148
|
* - **Logouts** ({@link NEVER_COLLAPSED}) — a login is inherently per-version:
|
|
@@ -235,7 +235,13 @@ export function remediationFor(finding) {
|
|
|
235
235
|
case 'missing-resource':
|
|
236
236
|
case 'content-drift':
|
|
237
237
|
case 'stale':
|
|
238
|
-
|
|
238
|
+
// doctor diagnoses; `agents sync` fixes (the superset of the old
|
|
239
|
+
// `doctor --fix`). A bare `agents sync <agent>` hits only the default
|
|
240
|
+
// version, so an agent-only row collapsed across versions must ask for
|
|
241
|
+
// @all to reach every one it covers.
|
|
242
|
+
if (agent && version)
|
|
243
|
+
return `agents sync ${agent}@${version} --yes`;
|
|
244
|
+
return agent ? `agents sync ${agent}@all --yes` : 'agents sync';
|
|
239
245
|
case 'hook-runtime-visibility-unavailable':
|
|
240
246
|
return 'upgrade agents-cli on this device';
|
|
241
247
|
case 'never-synced':
|
|
@@ -261,8 +267,8 @@ export function remediationFor(finding) {
|
|
|
261
267
|
return `agents repo pull ${version ?? 'user'}`;
|
|
262
268
|
case 'fleet-resource-gap':
|
|
263
269
|
// The resource is absent from this box's CENTRAL repos, not from a version
|
|
264
|
-
// home, so `agents
|
|
265
|
-
//
|
|
270
|
+
// home, so `agents sync` (which reconciles central -> homes) has nothing to
|
|
271
|
+
// copy. The divergence row cannot say WHICH repo declares it,
|
|
266
272
|
// and neither `agents repo pull` nor the sync umbrella touches the system
|
|
267
273
|
// repo (`commands/repo.ts:1186`, `lib/sync-umbrella.ts:104`) — that one is
|
|
268
274
|
// npm-shipped and moves with the CLI. So name both paths rather than a
|
|
@@ -317,8 +323,8 @@ function finding(f) {
|
|
|
317
323
|
* or more collapse into a count plus the first two subjects
|
|
318
324
|
* (`32 hooks missing (incl. 'git-guard', 'rm-guard')`). Naming every item — the
|
|
319
325
|
* pre-RUSH-2069-review behavior — flooded the section with dozens of near-identical
|
|
320
|
-
* rows for one root cause; the count carries the same signal and
|
|
321
|
-
* same command either way.
|
|
326
|
+
* rows for one root cause; the count carries the same signal and `agents sync`
|
|
327
|
+
* is the same command either way.
|
|
322
328
|
*/
|
|
323
329
|
function emitGroup(out, items, severity, kind, device, agent, version, noun, verb) {
|
|
324
330
|
if (items.length === 0)
|
|
@@ -735,14 +741,14 @@ function execPolicyFinding(device, execPolicy) {
|
|
|
735
741
|
/**
|
|
736
742
|
* Fold findings that say the SAME thing about several versions of one agent into
|
|
737
743
|
* a single row carrying `versions`, and widen its remediation to the agent-wide
|
|
738
|
-
* sweep (`agents
|
|
744
|
+
* sweep (`agents sync claude@all --yes` heals every non-isolated version in one
|
|
739
745
|
* go). Five identical `plugin 'code' — mirror missing` rows, one per installed
|
|
740
746
|
* claude, is the same fact five times.
|
|
741
747
|
*
|
|
742
748
|
* Three things never merge, because for each of them the widened remediation
|
|
743
749
|
* would be wrong:
|
|
744
|
-
* - **Isolated copies** — the agent-wide sweep deliberately skips them
|
|
745
|
-
*
|
|
750
|
+
* - **Isolated copies** — the agent-wide sweep deliberately skips them, so a
|
|
751
|
+
* folded row would print a command that leaves one broken.
|
|
746
752
|
* - **Findings with no agent** (repo-behind, rc-secret-export, …) — their
|
|
747
753
|
* `version` field is an alias, not a version.
|
|
748
754
|
* - **Logouts** ({@link NEVER_COLLAPSED}) — a login is inherently per-version:
|
|
@@ -22,6 +22,7 @@ import { atomicWriteJsonSync } from '../fs-atomic.js';
|
|
|
22
22
|
import { machineId } from '../machine-id.js';
|
|
23
23
|
import { addIgnoredEntry, unionDeviceIgnored } from './device-docs.js';
|
|
24
24
|
import { logAndContinueOnLockCompromised } from '../lock-compromise.js';
|
|
25
|
+
import { removeStatsCacheEntry } from './stats-cache.js';
|
|
25
26
|
/**
|
|
26
27
|
* Whether a fan-out should dial this device, honouring the preference stated on
|
|
27
28
|
* {@link DeviceProfile.reachability}: the live SSH probe wins over the cached
|
|
@@ -281,6 +282,7 @@ export async function removeDevice(name) {
|
|
|
281
282
|
return false;
|
|
282
283
|
delete reg[name];
|
|
283
284
|
await saveDevices(reg);
|
|
285
|
+
removeStatsCacheEntry(name);
|
|
284
286
|
return true;
|
|
285
287
|
});
|
|
286
288
|
}
|
|
@@ -38,6 +38,10 @@ export declare function readStatsCache(): Record<string, DeviceStats>;
|
|
|
38
38
|
* single-device refresh) never drops the rest of the fleet's cached stats.
|
|
39
39
|
*/
|
|
40
40
|
export declare function writeStatsCache(entries: Record<string, DeviceStats>): void;
|
|
41
|
+
/** Drop one removed device so cache-only rows cannot outlive the registry. */
|
|
42
|
+
export declare function removeStatsCacheEntry(name: string): void;
|
|
43
|
+
/** Immutable cache pruning primitive, exported for lifecycle regression tests. */
|
|
44
|
+
export declare function pruneStatsCache(entries: Record<string, DeviceStats>, name: string): Record<string, DeviceStats>;
|
|
41
45
|
export interface FleetStatsResult {
|
|
42
46
|
/** name → stats for every requested device (cache-served + freshly probed). */
|
|
43
47
|
stats: Map<string, DeviceStats>;
|
|
@@ -114,6 +114,25 @@ export function writeStatsCache(entries) {
|
|
|
114
114
|
// best-effort; a failed write just means the next read falls back to a live probe
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
/** Drop one removed device so cache-only rows cannot outlive the registry. */
|
|
118
|
+
export function removeStatsCacheEntry(name) {
|
|
119
|
+
try {
|
|
120
|
+
const current = readStatsCache();
|
|
121
|
+
if (!(name in current))
|
|
122
|
+
return;
|
|
123
|
+
const entries = pruneStatsCache(current, name);
|
|
124
|
+
fs.writeFileSync(cacheFilePath(), JSON.stringify({ version: 1, entries }, null, 2));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Cache cleanup is best-effort; registry removal remains authoritative.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Immutable cache pruning primitive, exported for lifecycle regression tests. */
|
|
131
|
+
export function pruneStatsCache(entries, name) {
|
|
132
|
+
const { [name]: _removed, ...remaining } = entries;
|
|
133
|
+
void _removed;
|
|
134
|
+
return remaining;
|
|
135
|
+
}
|
|
117
136
|
/**
|
|
118
137
|
* Load fleet stats cache-first. See the module doc for the default vs
|
|
119
138
|
* `--refresh` behaviour. Never throws — an unreachable box degrades to a
|
package/dist/lib/drift-sync.d.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* - computeSyncStatus() — the unified detection engine (sync-status.ts)
|
|
8
8
|
* - pullRepo() — fast-forward the `.system` repo (git.ts)
|
|
9
9
|
* - promptAgentVersionSelection() — the "which agent types / versions?" picker
|
|
10
|
-
* -
|
|
10
|
+
* - repairAfterSync() — the shared post-reconcile repair pass
|
|
11
|
+
* `agents sync` runs (heal + hook rewire +
|
|
12
|
+
* managed hook runtime shim repair)
|
|
11
13
|
*
|
|
12
14
|
* Combined flow (one confirmation): if `.system` is behind AND resources drifted,
|
|
13
15
|
* a single "Sync all detected" both pulls `.system` and reconciles the chosen
|
package/dist/lib/drift-sync.js
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* - computeSyncStatus() — the unified detection engine (sync-status.ts)
|
|
8
8
|
* - pullRepo() — fast-forward the `.system` repo (git.ts)
|
|
9
9
|
* - promptAgentVersionSelection() — the "which agent types / versions?" picker
|
|
10
|
-
* -
|
|
10
|
+
* - repairAfterSync() — the shared post-reconcile repair pass
|
|
11
|
+
* `agents sync` runs (heal + hook rewire +
|
|
12
|
+
* managed hook runtime shim repair)
|
|
11
13
|
*
|
|
12
14
|
* Combined flow (one confirmation): if `.system` is behind AND resources drifted,
|
|
13
15
|
* a single "Sync all detected" both pulls `.system` and reconciles the chosen
|
|
@@ -19,7 +21,7 @@ import chalk from 'chalk';
|
|
|
19
21
|
import { select, confirm } from '@inquirer/prompts';
|
|
20
22
|
import { AGENTS } from './agents.js';
|
|
21
23
|
import { pullRepo } from './git.js';
|
|
22
|
-
import {
|
|
24
|
+
import { repairAfterSync, renderRepairAfterSync } from './reconcile-and-repair.js';
|
|
23
25
|
import { promptAgentVersionSelection } from './installations/versions.js';
|
|
24
26
|
import { isInteractiveTerminal, isPromptCancelled } from './format.js';
|
|
25
27
|
import { computeSyncStatus, } from './sync-status.js';
|
|
@@ -58,14 +60,23 @@ async function pullSystem(status) {
|
|
|
58
60
|
console.log(chalk.red(`Could not pull .system: ${res.error ?? 'unknown error'}`));
|
|
59
61
|
return false;
|
|
60
62
|
}
|
|
61
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Reconcile a set of versions grouped by agent through the SHARED post-reconcile
|
|
65
|
+
* repair pass (`repairAfterSync`) — the same superset the three `agents sync`
|
|
66
|
+
* handlers run. Beyond the resources `heal()` fills, this also re-wires hooks
|
|
67
|
+
* left unwired and repairs broken managed hook runtime shims, so drift-sync is
|
|
68
|
+
* not a third orchestrator that silently skips shim repair. Renders each pass's
|
|
69
|
+
* rewire / shim-repair detail; the heal rollup is printed separately by the
|
|
70
|
+
* caller via `reportHealed`.
|
|
71
|
+
*/
|
|
62
72
|
async function healVersions(versionsByAgent, cwd) {
|
|
63
73
|
const out = [];
|
|
64
74
|
for (const [agent, versions] of versionsByAgent) {
|
|
65
75
|
if (versions.length === 0)
|
|
66
76
|
continue;
|
|
67
|
-
const
|
|
68
|
-
out.push(...
|
|
77
|
+
const repair = await repairAfterSync({ agent, versions, cwd });
|
|
78
|
+
out.push(...repair.heal.versions);
|
|
79
|
+
renderRepairAfterSync(repair, (line) => console.log(line));
|
|
69
80
|
}
|
|
70
81
|
return out;
|
|
71
82
|
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -311,6 +311,8 @@ export declare function resolveLaunchId(envLaunchId: string | undefined): string
|
|
|
311
311
|
* into unrelated invocations.
|
|
312
312
|
*/
|
|
313
313
|
export declare function buildExecEnv(options: ExecOptions): NodeJS.ProcessEnv;
|
|
314
|
+
/** Materialize config roots for vendor CLIs that do not create parents recursively. */
|
|
315
|
+
export declare function ensureVendorHomeDir(agent: AgentId, versionHome: string): string | null;
|
|
314
316
|
/**
|
|
315
317
|
* Describes how to translate ExecOptions into CLI arguments for a specific agent.
|
|
316
318
|
*
|
package/dist/lib/exec.js
CHANGED
|
@@ -9,7 +9,7 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { ALL_MODES, REMOTE_INTERACTIVE_ENV } from './types.js';
|
|
12
|
-
import { AGENTS, findInPath } from './agents.js';
|
|
12
|
+
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
13
13
|
import { parseTimeout } from './scheduling/routines.js';
|
|
14
14
|
import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './installations/versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
@@ -447,6 +447,19 @@ export function buildExecEnv(options) {
|
|
|
447
447
|
...options.env,
|
|
448
448
|
};
|
|
449
449
|
}
|
|
450
|
+
/** Materialize config roots for vendor CLIs that do not create parents recursively. */
|
|
451
|
+
export function ensureVendorHomeDir(agent, versionHome) {
|
|
452
|
+
if (agent !== 'cursor' && agent !== 'grok' && agent !== 'copilot')
|
|
453
|
+
return null;
|
|
454
|
+
const vendorHome = path.join(versionHome, agentConfigDirName(agent));
|
|
455
|
+
fs.mkdirSync(vendorHome, { recursive: true });
|
|
456
|
+
return vendorHome;
|
|
457
|
+
}
|
|
458
|
+
function ensureVendorHomeForSpawn(options) {
|
|
459
|
+
const { versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
|
|
460
|
+
if (versionHome)
|
|
461
|
+
ensureVendorHomeDir(options.agent, versionHome);
|
|
462
|
+
}
|
|
450
463
|
/**
|
|
451
464
|
* CLI command templates for every supported agent.
|
|
452
465
|
*
|
|
@@ -1131,6 +1144,7 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1131
1144
|
// practice (POSIX uses the bash shim, which execs the binary and never reaches
|
|
1132
1145
|
// buildExecEnv — `installations/shims.ts` generateShimScript).
|
|
1133
1146
|
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
|
|
1147
|
+
ensureVendorHomeDir(agent, getVersionHomePath(agent, version));
|
|
1134
1148
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
1135
1149
|
// Pre-launch marker for the SECOND live launch path: the Windows generated
|
|
1136
1150
|
// `.cmd` shim delegates here and spawns the harness directly, so without this
|
|
@@ -1715,6 +1729,7 @@ async function spawnAgent(options) {
|
|
|
1715
1729
|
if (options.agent === 'claude' && !options.resume && !options.sessionId) {
|
|
1716
1730
|
options = { ...options, sessionId: randomUUID() };
|
|
1717
1731
|
}
|
|
1732
|
+
ensureVendorHomeForSpawn(options);
|
|
1718
1733
|
// Record the run's --name against its session id (when both are known at
|
|
1719
1734
|
// launch) so `agents sessions <name>` resolves it. Best-effort; unnamed runs
|
|
1720
1735
|
// and agents whose id isn't known up front simply skip this.
|