@phnx-labs/agents-cli 1.20.77 → 1.20.82
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 +589 -0
- package/README.md +27 -25
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +74 -0
- package/dist/commands/doctor.js +718 -40
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -8
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +195 -11
- package/dist/commands/routines.js +16 -4
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +108 -34
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +118 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.d.ts +15 -1
- package/dist/commands/sessions-picker.js +90 -7
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +126 -1
- package/dist/commands/sessions.js +365 -42
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/ssh.js +9 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +8 -13
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -23
- package/dist/lib/agents.js +72 -100
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +26 -40
- package/dist/lib/daemon.js +63 -123
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.js +35 -8
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +27 -14
- package/dist/lib/git.js +138 -43
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/remote-cmd.js +2 -0
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +20 -0
- package/dist/lib/routines.js +6 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +71 -29
- package/dist/lib/sandbox.js +0 -16
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +18 -0
- package/dist/lib/secrets/agent.js +131 -17
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +12 -9
- package/dist/lib/secrets/bundles.js +47 -31
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +10 -0
- package/dist/lib/secrets/session-store.js +64 -11
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +117 -26
- package/dist/lib/session/active.js +199 -53
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +41 -1
- package/dist/lib/session/db.js +173 -8
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +19 -2
- package/dist/lib/session/discover.js +32 -11
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/state.d.ts +1 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
- package/dist/lib/secrets/account-token.d.ts +0 -20
- package/dist/lib/secrets/account-token.js +0 -64
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -68,28 +68,25 @@ export declare function reapStrayDaemons(keepPid?: number): {
|
|
|
68
68
|
/** Append a JSONL log entry to the daemon log file (owner-only permissions). */
|
|
69
69
|
export declare function log(level: string, message: string): void;
|
|
70
70
|
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
|
|
71
|
-
export declare function runDaemon(): Promise<void>;
|
|
72
|
-
/**
|
|
73
|
-
* Read the long-lived Claude OAuth token (from `claude setup-token`) that the
|
|
74
|
-
* user stored under the `claude` secrets bundle. Resolves the bundle the same
|
|
75
|
-
* way `agents run --secrets` does. Interactive starts may prompt Keychain;
|
|
76
|
-
* headless auto-starts are broker-only and return null unless the user already
|
|
77
|
-
* unlocked the bundle in the secrets agent. That keeps a background browser
|
|
78
|
-
* command from hanging on an unseen biometric prompt. Never throws: an absent
|
|
79
|
-
* token leaves the daemon on its existing interactive OAuth session.
|
|
80
|
-
*/
|
|
81
|
-
export declare function readDaemonClaudeOAuthToken(opts?: {
|
|
82
|
-
allowPrompt?: boolean;
|
|
83
|
-
}): string | null;
|
|
84
71
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
72
|
+
* Anchor the daemon's working directory to a stable, always-present path.
|
|
73
|
+
*
|
|
74
|
+
* The daemon is long-lived and inherits whatever cwd it was launched from — often
|
|
75
|
+
* a git worktree (e.g. a `.agents/worktrees/<slug>/` a session happened to be in).
|
|
76
|
+
* When that directory is later removed (`git worktree remove`, `rm -rf`), the
|
|
77
|
+
* daemon keeps the deleted inode as its cwd — a process cannot chdir out of a
|
|
78
|
+
* deleted directory on its own — and every job it spawns inherits the dead cwd
|
|
79
|
+
* (`spawnJobAttempt` and command runs pass no explicit `cwd`, so the child uses
|
|
80
|
+
* the parent's). Bun then fails `getcwd()` during startup and every routine crashes
|
|
81
|
+
* at 0 seconds with `ENOENT: Bun could not find a file` before the agent even runs.
|
|
82
|
+
*
|
|
83
|
+
* Re-anchoring to the home directory once, at daemon startup, makes the daemon
|
|
84
|
+
* immune regardless of how it was launched (systemd unit, launchd, or a manual
|
|
85
|
+
* `agents __daemon-run` from any directory). Returns the resolved cwd, or null if
|
|
86
|
+
* anchoring failed (logged, non-fatal).
|
|
89
87
|
*/
|
|
90
|
-
export declare function
|
|
91
|
-
|
|
92
|
-
}): Record<string, string>;
|
|
88
|
+
export declare function anchorDaemonCwd(): string | null;
|
|
89
|
+
export declare function runDaemon(): Promise<void>;
|
|
93
90
|
/**
|
|
94
91
|
* Write a launchd plist or systemd unit with owner-only permissions atomically.
|
|
95
92
|
*
|
|
@@ -103,21 +100,19 @@ export declare function writeOwnerOnlyServiceManifest(filePath: string, content:
|
|
|
103
100
|
/**
|
|
104
101
|
* Generate a macOS launchd plist for auto-starting the daemon.
|
|
105
102
|
*
|
|
106
|
-
* The plist never embeds
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* Keychain-backed secure store and never touches the unit file.
|
|
103
|
+
* The plist never embeds a Claude OAuth token: the daemon holds no Claude
|
|
104
|
+
* credential at all. Routine runs authenticate through the per-account
|
|
105
|
+
* CLAUDE_CONFIG_DIR login on this device, exactly like an interactive
|
|
106
|
+
* `agents run`, so no credential ever touches the service manifest.
|
|
111
107
|
*/
|
|
112
108
|
export declare function generateLaunchdPlist(agentsBin?: string): string;
|
|
113
109
|
/**
|
|
114
110
|
* Generate a Linux systemd user unit for auto-starting the daemon.
|
|
115
111
|
*
|
|
116
|
-
* The unit never embeds
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* store and never touches the unit file.
|
|
112
|
+
* The unit never embeds a Claude OAuth token: the daemon holds no Claude
|
|
113
|
+
* credential at all. Routine runs authenticate through the per-account
|
|
114
|
+
* CLAUDE_CONFIG_DIR login on this device, exactly like an interactive
|
|
115
|
+
* `agents run`, so no credential ever touches the unit file.
|
|
121
116
|
*/
|
|
122
117
|
export declare function generateSystemdUnit(agentsBin?: string): string;
|
|
123
118
|
export { getAgentsBinPath };
|
|
@@ -140,15 +135,6 @@ export declare function ensureDaemonStarted(): {
|
|
|
140
135
|
pid: number | null;
|
|
141
136
|
method: string;
|
|
142
137
|
} | null;
|
|
143
|
-
/**
|
|
144
|
-
* Environment for the detached daemon fallback. The launchd/systemd paths
|
|
145
|
-
* deliver the long-lived OAuth token via the service manifest's environment;
|
|
146
|
-
* the detached path has no manifest, so inject it here. Read happens during an
|
|
147
|
-
* interactive `routines start`, so a Keychain Touch ID prompt can be satisfied;
|
|
148
|
-
* the daemon then passes it to every routine run it spawns. An already-set
|
|
149
|
-
* value (e.g. inherited from launchd) is left untouched.
|
|
150
|
-
*/
|
|
151
|
-
export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv, bundleEnv?: Record<string, string>): NodeJS.ProcessEnv;
|
|
152
138
|
/**
|
|
153
139
|
* Resolve how to launch the daemon: `node <entry> __daemon-run`, matching the
|
|
154
140
|
* exact form that works under a direct `__daemon-run`.
|
|
@@ -197,7 +183,7 @@ interface StartDetachedOptions {
|
|
|
197
183
|
agentsBin?: string;
|
|
198
184
|
/** Log file the daemon's stdio is redirected to (defaults to the daemon log). */
|
|
199
185
|
logPath?: string;
|
|
200
|
-
/** Environment for the child (defaults to the
|
|
186
|
+
/** Environment for the child (defaults to the daemon's current process env). */
|
|
201
187
|
env?: NodeJS.ProcessEnv;
|
|
202
188
|
}
|
|
203
189
|
export declare function startDetached(opts?: StartDetachedOptions): {
|
package/dist/lib/daemon.js
CHANGED
|
@@ -11,18 +11,16 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
14
|
-
import { isAlive, killTree, backgroundSpawnOptions } from './platform/index.js';
|
|
14
|
+
import { isAlive, killTree, backgroundSpawnOptions, waitForExit } from './platform/index.js';
|
|
15
15
|
import { listJobs as listAllJobs } from './routines.js';
|
|
16
16
|
import { syncAllProjectRoutines } from './routines-project.js';
|
|
17
17
|
import { JobScheduler } from './scheduler.js';
|
|
18
18
|
import { MonitorEngine } from './monitors/engine.js';
|
|
19
19
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
20
20
|
import { detectOverdueJobs, notifyOverdue } from './overdue.js';
|
|
21
|
-
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
22
21
|
import { notifyRoutineStart, notifyRoutineFinish, notifyRoutineStartFailed } from './routine-notify.js';
|
|
23
22
|
import { BrowserService } from './browser/service.js';
|
|
24
23
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
25
|
-
import { readAndResolveBundleEnv } from './secrets/bundles.js';
|
|
26
24
|
import { redactSecrets } from './redact.js';
|
|
27
25
|
import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from './cli-entry.js';
|
|
28
26
|
const PID_FILE = 'daemon.pid';
|
|
@@ -35,12 +33,6 @@ const PLIST_NAME = 'com.phnx-labs.agents-daemon';
|
|
|
35
33
|
const SYSTEMD_UNIT = 'agents-daemon.service';
|
|
36
34
|
const MONITOR_TICK_MS = 60_000;
|
|
37
35
|
const WEDGE_THRESHOLD_TICKS = 3;
|
|
38
|
-
// A long-lived `claude setup-token` value stored in this secrets bundle/key is
|
|
39
|
-
// baked into the daemon's service-manager environment so headless routine runs
|
|
40
|
-
// authenticate without depending on the short-lived interactive Keychain OAuth
|
|
41
|
-
// session (which expires between runs and produces intermittent 401s).
|
|
42
|
-
const DAEMON_OAUTH_BUNDLE = 'claude';
|
|
43
|
-
const DAEMON_OAUTH_KEY = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
44
36
|
/**
|
|
45
37
|
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
46
38
|
* broker. The startup host decision is one-shot; this drives the periodic
|
|
@@ -179,6 +171,10 @@ export function isDaemonWedged() {
|
|
|
179
171
|
const elapsed = Date.now() - Date.parse(hb.lastTick);
|
|
180
172
|
return elapsed > WEDGE_THRESHOLD_TICKS * MONITOR_TICK_MS;
|
|
181
173
|
}
|
|
174
|
+
/** How long stopDaemon waits for a SIGTERMed daemon to exit before escalating. */
|
|
175
|
+
const STOP_GRACE_MS = 5000;
|
|
176
|
+
/** How long it waits after the hard tree-kill before giving up. */
|
|
177
|
+
const STOP_KILL_GRACE_MS = 2000;
|
|
182
178
|
/** Check if the daemon process is alive by sending signal 0 to the stored PID. */
|
|
183
179
|
export function isDaemonRunning() {
|
|
184
180
|
const pid = readDaemonPid();
|
|
@@ -297,6 +293,34 @@ export function log(level, message) {
|
|
|
297
293
|
catch { /* best effort */ }
|
|
298
294
|
}
|
|
299
295
|
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
|
|
296
|
+
/**
|
|
297
|
+
* Anchor the daemon's working directory to a stable, always-present path.
|
|
298
|
+
*
|
|
299
|
+
* The daemon is long-lived and inherits whatever cwd it was launched from — often
|
|
300
|
+
* a git worktree (e.g. a `.agents/worktrees/<slug>/` a session happened to be in).
|
|
301
|
+
* When that directory is later removed (`git worktree remove`, `rm -rf`), the
|
|
302
|
+
* daemon keeps the deleted inode as its cwd — a process cannot chdir out of a
|
|
303
|
+
* deleted directory on its own — and every job it spawns inherits the dead cwd
|
|
304
|
+
* (`spawnJobAttempt` and command runs pass no explicit `cwd`, so the child uses
|
|
305
|
+
* the parent's). Bun then fails `getcwd()` during startup and every routine crashes
|
|
306
|
+
* at 0 seconds with `ENOENT: Bun could not find a file` before the agent even runs.
|
|
307
|
+
*
|
|
308
|
+
* Re-anchoring to the home directory once, at daemon startup, makes the daemon
|
|
309
|
+
* immune regardless of how it was launched (systemd unit, launchd, or a manual
|
|
310
|
+
* `agents __daemon-run` from any directory). Returns the resolved cwd, or null if
|
|
311
|
+
* anchoring failed (logged, non-fatal).
|
|
312
|
+
*/
|
|
313
|
+
export function anchorDaemonCwd() {
|
|
314
|
+
const home = os.homedir();
|
|
315
|
+
try {
|
|
316
|
+
process.chdir(home);
|
|
317
|
+
return home;
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
log('WARN', `Could not anchor daemon cwd to ${home}: ${err.message}`);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
300
324
|
export async function runDaemon() {
|
|
301
325
|
// Single-instance guard: a direct `agents __daemon-run` (manual, or a
|
|
302
326
|
// service-manager restart racing a live predecessor) must not clobber a
|
|
@@ -309,47 +333,14 @@ export async function runDaemon() {
|
|
|
309
333
|
process.exit(0);
|
|
310
334
|
}
|
|
311
335
|
log('INFO', `Daemon started (PID: ${process.pid})`);
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
321
|
-
const bundleEnv = readDaemonClaudeBundleEnv();
|
|
322
|
-
const oauthToken = (bundleEnv[DAEMON_OAUTH_KEY] ?? '').trim();
|
|
323
|
-
if (oauthToken) {
|
|
324
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
325
|
-
// Also inject each per-account CLAUDE_CODE_OAUTH_TOKEN_<slug> present in the
|
|
326
|
-
// bundle, so a routine authenticates its rotation-pinned account via that
|
|
327
|
-
// account's own long-lived, non-rotating setup-token (runner.ts
|
|
328
|
-
// buildRoutineSpawnEnv) instead of the interactive session that rotates and
|
|
329
|
-
// logs the fleet out (Claude Code #25609 / #56339).
|
|
330
|
-
let perAccount = 0;
|
|
331
|
-
for (const [k, v] of Object.entries(bundleEnv)) {
|
|
332
|
-
if (k.startsWith('CLAUDE_CODE_OAUTH_TOKEN_') && (v ?? '').trim() && !process.env[k]) {
|
|
333
|
-
process.env[k] = v.trim();
|
|
334
|
-
perAccount++;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
log('INFO', `Loaded Claude OAuth token from secrets bundle for routine runs${perAccount ? ` (+${perAccount} per-account)` : ''}`);
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
// No token available (e.g. a headless macOS daemon whose keychain was
|
|
341
|
-
// locked at start resolves broker-only and gets nothing). Historically
|
|
342
|
-
// this was silent, so every Claude routine the daemon spawned failed auth
|
|
343
|
-
// with no signal. Make it loud: WARN in the log and fire a desktop alert.
|
|
344
|
-
log('WARN', 'No Claude OAuth token available — Claude routine runs will fail auth on this host. ' +
|
|
345
|
-
'Restart the daemon with the keychain unlocked, or unlock the `claude` secrets bundle.');
|
|
346
|
-
notifyDesktop({
|
|
347
|
-
title: 'agents daemon: no Claude credential',
|
|
348
|
-
body: 'Claude routines will fail auth on this host. Restart the daemon with the keychain unlocked.',
|
|
349
|
-
action: 'routines:list',
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
336
|
+
anchorDaemonCwd();
|
|
337
|
+
// The daemon holds NO Claude credential of its own. Routine runs authenticate
|
|
338
|
+
// exactly like an interactive `agents run`: through the per-account
|
|
339
|
+
// CLAUDE_CONFIG_DIR login on this device (its own auto-refreshing
|
|
340
|
+
// .credentials.json). Claude Code's interactive access token is short-lived but
|
|
341
|
+
// refreshes itself per-device; a routine whose account login has gone dead is
|
|
342
|
+
// skipped up front by the auth-health preflight (runner.ts) with a re-login
|
|
343
|
+
// hint, rather than papered over by an injected fallback token.
|
|
353
344
|
// Reap any stray duplicate daemon of this install that slipped past the start
|
|
354
345
|
// lock or was orphaned by a hard-crash — before it can double-fire jobs.
|
|
355
346
|
try {
|
|
@@ -833,38 +824,6 @@ export async function runDaemon() {
|
|
|
833
824
|
process.on('SIGINT', () => handleShutdown());
|
|
834
825
|
await new Promise(() => { });
|
|
835
826
|
}
|
|
836
|
-
/**
|
|
837
|
-
* Read the long-lived Claude OAuth token (from `claude setup-token`) that the
|
|
838
|
-
* user stored under the `claude` secrets bundle. Resolves the bundle the same
|
|
839
|
-
* way `agents run --secrets` does. Interactive starts may prompt Keychain;
|
|
840
|
-
* headless auto-starts are broker-only and return null unless the user already
|
|
841
|
-
* unlocked the bundle in the secrets agent. That keeps a background browser
|
|
842
|
-
* command from hanging on an unseen biometric prompt. Never throws: an absent
|
|
843
|
-
* token leaves the daemon on its existing interactive OAuth session.
|
|
844
|
-
*/
|
|
845
|
-
export function readDaemonClaudeOAuthToken(opts = {}) {
|
|
846
|
-
const token = (readDaemonClaudeBundleEnv(opts)[DAEMON_OAUTH_KEY] ?? '').trim();
|
|
847
|
-
return token.length > 0 ? token : null;
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* Read the FULL `claude` bundle env — the main `CLAUDE_CODE_OAUTH_TOKEN` plus any
|
|
851
|
-
* per-account `CLAUDE_CODE_OAUTH_TOKEN_<slug>` setup-tokens. Same resolution and
|
|
852
|
-
* never-throws contract as {@link readDaemonClaudeOAuthToken}; returns `{}` when
|
|
853
|
-
* the bundle can't be read (broker-only headless miss, absent bundle, etc.).
|
|
854
|
-
*/
|
|
855
|
-
export function readDaemonClaudeBundleEnv(opts = {}) {
|
|
856
|
-
try {
|
|
857
|
-
const allowPrompt = opts.allowPrompt ?? Boolean(process.stdin.isTTY);
|
|
858
|
-
const { env } = readAndResolveBundleEnv(DAEMON_OAUTH_BUNDLE, {
|
|
859
|
-
caller: 'daemon',
|
|
860
|
-
agentOnly: !allowPrompt,
|
|
861
|
-
});
|
|
862
|
-
return env;
|
|
863
|
-
}
|
|
864
|
-
catch {
|
|
865
|
-
return {};
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
827
|
/** Escape a string for safe inclusion in an XML <string> node. */
|
|
869
828
|
function xmlEscape(s) {
|
|
870
829
|
return s
|
|
@@ -892,11 +851,10 @@ export function writeOwnerOnlyServiceManifest(filePath, content) {
|
|
|
892
851
|
/**
|
|
893
852
|
* Generate a macOS launchd plist for auto-starting the daemon.
|
|
894
853
|
*
|
|
895
|
-
* The plist never embeds
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
* Keychain-backed secure store and never touches the unit file.
|
|
854
|
+
* The plist never embeds a Claude OAuth token: the daemon holds no Claude
|
|
855
|
+
* credential at all. Routine runs authenticate through the per-account
|
|
856
|
+
* CLAUDE_CONFIG_DIR login on this device, exactly like an interactive
|
|
857
|
+
* `agents run`, so no credential ever touches the service manifest.
|
|
900
858
|
*/
|
|
901
859
|
export function generateLaunchdPlist(agentsBin = getAgentsBinPath()) {
|
|
902
860
|
const launch = getDaemonLaunch(agentsBin);
|
|
@@ -934,11 +892,10 @@ function systemdExecArg(value) {
|
|
|
934
892
|
/**
|
|
935
893
|
* Generate a Linux systemd user unit for auto-starting the daemon.
|
|
936
894
|
*
|
|
937
|
-
* The unit never embeds
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
* store and never touches the unit file.
|
|
895
|
+
* The unit never embeds a Claude OAuth token: the daemon holds no Claude
|
|
896
|
+
* credential at all. Routine runs authenticate through the per-account
|
|
897
|
+
* CLAUDE_CONFIG_DIR login on this device, exactly like an interactive
|
|
898
|
+
* `agents run`, so no credential ever touches the unit file.
|
|
942
899
|
*/
|
|
943
900
|
export function generateSystemdUnit(agentsBin = getAgentsBinPath()) {
|
|
944
901
|
const launch = getDaemonLaunch(agentsBin);
|
|
@@ -1082,31 +1039,6 @@ function startDaemonLocked(agentsBin) {
|
|
|
1082
1039
|
}
|
|
1083
1040
|
return startDetached({ agentsBin });
|
|
1084
1041
|
}
|
|
1085
|
-
/**
|
|
1086
|
-
* Environment for the detached daemon fallback. The launchd/systemd paths
|
|
1087
|
-
* deliver the long-lived OAuth token via the service manifest's environment;
|
|
1088
|
-
* the detached path has no manifest, so inject it here. Read happens during an
|
|
1089
|
-
* interactive `routines start`, so a Keychain Touch ID prompt can be satisfied;
|
|
1090
|
-
* the daemon then passes it to every routine run it spawns. An already-set
|
|
1091
|
-
* value (e.g. inherited from launchd) is left untouched.
|
|
1092
|
-
*/
|
|
1093
|
-
export function buildDetachedDaemonEnv(baseEnv = process.env, bundleEnv = readDaemonClaudeBundleEnv()) {
|
|
1094
|
-
const env = { ...baseEnv };
|
|
1095
|
-
if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
1096
|
-
const main = (bundleEnv[DAEMON_OAUTH_KEY] ?? '').trim();
|
|
1097
|
-
if (main)
|
|
1098
|
-
env.CLAUDE_CODE_OAUTH_TOKEN = main;
|
|
1099
|
-
}
|
|
1100
|
-
// Per-account setup-tokens — same rationale as the runDaemon startup injection:
|
|
1101
|
-
// a routine authenticates its rotation-pinned account via that account's own
|
|
1102
|
-
// long-lived, non-rotating token. An already-set value wins.
|
|
1103
|
-
for (const [k, v] of Object.entries(bundleEnv)) {
|
|
1104
|
-
if (k.startsWith('CLAUDE_CODE_OAUTH_TOKEN_') && (v ?? '').trim() && !env[k]) {
|
|
1105
|
-
env[k] = v.trim();
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return env;
|
|
1109
|
-
}
|
|
1110
1042
|
/**
|
|
1111
1043
|
* Resolve how to launch the daemon: `node <entry> __daemon-run`, matching the
|
|
1112
1044
|
* exact form that works under a direct `__daemon-run`.
|
|
@@ -1186,7 +1118,7 @@ export function startDetached(opts = {}) {
|
|
|
1186
1118
|
const child = spawn(command, args, {
|
|
1187
1119
|
stdio: ['ignore', logFd, logFd],
|
|
1188
1120
|
...backgroundSpawnOptions({ fdStdio: true }),
|
|
1189
|
-
env: opts.env ??
|
|
1121
|
+
env: opts.env ?? process.env,
|
|
1190
1122
|
});
|
|
1191
1123
|
// A failed spawn (ENOENT/EACCES) emits 'error' asynchronously; without a
|
|
1192
1124
|
// listener that would crash the parent as an unhandled EventEmitter error.
|
|
@@ -1262,11 +1194,19 @@ export function stopDaemon() {
|
|
|
1262
1194
|
process.kill(pid, 'SIGTERM');
|
|
1263
1195
|
}
|
|
1264
1196
|
catch { /* process already exited */ }
|
|
1265
|
-
//
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1197
|
+
// Wait for it to actually go. This used to be a setTimeout escalation plus
|
|
1198
|
+
// an immediate removeDaemonPid(), which had two failure modes: in a
|
|
1199
|
+
// short-lived process (the npm postinstall) the timer never fired at all,
|
|
1200
|
+
// and clearing the pid file while the old daemon still ran made
|
|
1201
|
+
// isDaemonRunning() report false, so startDaemon() launched a SECOND
|
|
1202
|
+
// daemon. Its hosted broker then unlinked the live socket and rebound,
|
|
1203
|
+
// orphaning the first broker with every unlocked bundle still in its RAM
|
|
1204
|
+
// and unreachable — two brokers on one socket path, seen on a real machine
|
|
1205
|
+
// after an install into a second prefix.
|
|
1206
|
+
if (!waitForExit(pid, STOP_GRACE_MS)) {
|
|
1207
|
+
killTree(pid);
|
|
1208
|
+
waitForExit(pid, STOP_KILL_GRACE_MS);
|
|
1209
|
+
}
|
|
1270
1210
|
}
|
|
1271
1211
|
}
|
|
1272
1212
|
removeDaemonPid();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-device harness divergence for `agents doctor` (RUSH-2027).
|
|
3
|
+
*
|
|
4
|
+
* `agents fleet status` already fans out `agents doctor --json` per device and
|
|
5
|
+
* builds a {@link FleetHealthReport} of coarse device health (reachable, CLIs
|
|
6
|
+
* installed, agents-cli version skew, local sync drift). It deliberately does
|
|
7
|
+
* NOT compare fine-grained *resource presence* across devices — so a plugin like
|
|
8
|
+
* `swarm` installed on one box but missing on another is silent until a user
|
|
9
|
+
* types `/swarm:run` on the wrong machine and gets `Unknown command`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the missing comparison. Each device self-reports its installed
|
|
12
|
+
* inventory (resource kinds, per-agent version sets, `.agents`/`.system` repo
|
|
13
|
+
* state) in its `doctor --json` payload; {@link compareFleetInventories} takes
|
|
14
|
+
* the collected per-device inventories and, treating the local machine as the
|
|
15
|
+
* baseline, flags:
|
|
16
|
+
*
|
|
17
|
+
* 1. Resource presence gaps — a resource present on one device, missing on
|
|
18
|
+
* another (either direction relative to the local baseline).
|
|
19
|
+
* 2. Agent version gaps — an agent version installed on one device but
|
|
20
|
+
* not another (e.g. `yosemite-s0 missing claude@2.1.220`).
|
|
21
|
+
* 3. `.agents`/`.system` repo drift — a device whose config-repo HEAD, branch,
|
|
22
|
+
* or dirty state diverges from the local baseline.
|
|
23
|
+
*
|
|
24
|
+
* Pure and SSH-free: the SSH fan-out lives in the doctor command; this module
|
|
25
|
+
* only consumes the already-collected payloads, so the divergence logic is
|
|
26
|
+
* unit-tested against fixture inventories with no live fleet.
|
|
27
|
+
*/
|
|
28
|
+
/** Resource kinds compared across devices. Mirrors {@link DoctorKind} plus the
|
|
29
|
+
* top-level `workflows`/`memory` inventory that `getAvailableResources` emits;
|
|
30
|
+
* `promptcuts` is a single present/absent bit surfaced as one named resource. */
|
|
31
|
+
export type FleetResourceKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts' | 'workflows';
|
|
32
|
+
export declare const FLEET_RESOURCE_KINDS: FleetResourceKind[];
|
|
33
|
+
/** Per-repo state for the `.agents` (user) or `.system` config repo, as a device
|
|
34
|
+
* self-reports it. `null` fields mean "not a git repo / unreadable" on that box. */
|
|
35
|
+
export interface RepoState {
|
|
36
|
+
/** Current branch, or null when detached / unreadable. */
|
|
37
|
+
branch: string | null;
|
|
38
|
+
/** Short HEAD commit (8 chars), or null when unreadable. */
|
|
39
|
+
head: string | null;
|
|
40
|
+
/** True when the working tree has uncommitted changes. */
|
|
41
|
+
dirty: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The self-reported harness inventory a single device emits in `doctor --json`.
|
|
45
|
+
* Comparable device-to-device with no further probing.
|
|
46
|
+
*/
|
|
47
|
+
export interface FleetInventory {
|
|
48
|
+
/** Installed resource names per kind. `rules` here are the top-level rules
|
|
49
|
+
* files (memory presets), not per-agent compiled AGENTS.md. */
|
|
50
|
+
resources: Record<FleetResourceKind, string[]>;
|
|
51
|
+
/** Installed version ids per agent id (e.g. `{ claude: ['2.1.170','2.1.220'] }`). */
|
|
52
|
+
agentVersions: Record<string, string[]>;
|
|
53
|
+
/** State of the user (`~/.agents`) and system (`~/.agents/.system`) config repos. */
|
|
54
|
+
repos: {
|
|
55
|
+
agents: RepoState | null;
|
|
56
|
+
system: RepoState | null;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** A device's inventory paired with its name (and reachability). A device that
|
|
60
|
+
* was unreachable / failed to report carries `inventory: null` and is skipped
|
|
61
|
+
* by the comparison (its absence is a `fleet status` concern, not divergence). */
|
|
62
|
+
export interface DeviceInventory {
|
|
63
|
+
name: string;
|
|
64
|
+
inventory: FleetInventory | null;
|
|
65
|
+
}
|
|
66
|
+
export type FleetDivergenceKind = 'resource-missing-remote' | 'resource-missing-local' | 'agent-version-missing-remote' | 'agent-version-missing-local' | 'repo-drift';
|
|
67
|
+
/** One cross-device divergence finding, baseline = the local machine. */
|
|
68
|
+
export interface FleetDivergence {
|
|
69
|
+
kind: FleetDivergenceKind;
|
|
70
|
+
/** The device the finding is about (the remote box, or the local box for a
|
|
71
|
+
* `*-missing-local` finding where a remote has something local lacks). */
|
|
72
|
+
device: string;
|
|
73
|
+
/** Resource kind for a resource finding; agent id for a version finding;
|
|
74
|
+
* `agents`/`system` for a repo finding. */
|
|
75
|
+
category: string;
|
|
76
|
+
/** The specific resource name / version id / repo label that diverges. */
|
|
77
|
+
name: string;
|
|
78
|
+
/** Human-readable one-liner, ready for the warnings list. */
|
|
79
|
+
message: string;
|
|
80
|
+
}
|
|
81
|
+
export interface FleetDivergenceReport {
|
|
82
|
+
/** Name of the device used as the comparison baseline (the local machine). */
|
|
83
|
+
baseline: string;
|
|
84
|
+
divergences: FleetDivergence[];
|
|
85
|
+
/** Devices that reported an inventory and were compared. */
|
|
86
|
+
comparedDevices: string[];
|
|
87
|
+
/** Devices that could not be compared (offline / no inventory in payload). */
|
|
88
|
+
skippedDevices: string[];
|
|
89
|
+
hasDivergence: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compare a set of per-device inventories against the local baseline and emit
|
|
93
|
+
* every cross-device divergence. The baseline is the device whose name equals
|
|
94
|
+
* {@link baselineName} (the local machine); if that device has no inventory the
|
|
95
|
+
* report is empty (nothing to compare against). Devices with no inventory are
|
|
96
|
+
* recorded under `skippedDevices` and never produce false "missing" findings.
|
|
97
|
+
*
|
|
98
|
+
* Ordering is deterministic (device, then category, then name) so the human and
|
|
99
|
+
* JSON output — and the tests — are stable.
|
|
100
|
+
*/
|
|
101
|
+
export declare function compareFleetInventories(devices: DeviceInventory[], baselineName: string): FleetDivergenceReport;
|