@phnx-labs/agents-cli 1.20.86 → 1.20.88
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 +172 -0
- package/README.md +3 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/routines.js +2 -2
- package/dist/commands/sessions-browser.d.ts +18 -0
- package/dist/commands/sessions-browser.js +126 -24
- package/dist/commands/sessions-picker.d.ts +21 -8
- package/dist/commands/sessions-picker.js +83 -7
- package/dist/commands/sessions.d.ts +19 -0
- package/dist/commands/sessions.js +148 -18
- package/dist/commands/ssh.js +62 -4
- package/dist/commands/teams-picker.d.ts +2 -0
- package/dist/commands/teams-picker.js +2 -1
- package/dist/commands/teams.d.ts +4 -1
- package/dist/commands/teams.js +106 -70
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/view.js +14 -3
- package/dist/index.js +31 -1
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/claude-account-token.d.ts +12 -0
- package/dist/lib/claude-account-token.js +63 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +19 -1
- package/dist/lib/daemon.js +17 -2
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/devices/registry.d.ts +25 -0
- package/dist/lib/devices/registry.js +82 -1
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +13 -0
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +51 -9
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/runner.d.ts +14 -1
- package/dist/lib/runner.js +37 -8
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/db.d.ts +21 -0
- package/dist/lib/session/db.js +45 -4
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/remote-list.d.ts +7 -0
- package/dist/lib/session/remote-list.js +8 -4
- package/dist/lib/session/state.js +69 -2
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/team-filter.d.ts +22 -3
- package/dist/lib/session/team-filter.js +106 -17
- package/dist/lib/session/types.d.ts +9 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/signin-badge.d.ts +17 -0
- package/dist/lib/signin-badge.js +19 -0
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +22 -87
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
|
@@ -40,6 +40,23 @@ export interface RepoState {
|
|
|
40
40
|
/** True when the working tree has uncommitted changes. */
|
|
41
41
|
dirty: boolean;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Per-version sign-in state a device self-reports, so the fleet doctor can show
|
|
45
|
+
* every installed version's account (and a provable logged-out) without a second
|
|
46
|
+
* SSH round-trip. `provable` is true only when the credential is absent from BOTH
|
|
47
|
+
* the version home and the active/global HOME (see `credentialPresence`); an
|
|
48
|
+
* unprovable absence (opaque/keychain agent) is a warning, not a critical.
|
|
49
|
+
*/
|
|
50
|
+
export interface FleetVersionSignIn {
|
|
51
|
+
version: string;
|
|
52
|
+
/** A usable local credential was found for this version (or shared globally). */
|
|
53
|
+
signedIn: boolean;
|
|
54
|
+
/** Human account label (email, org badge, or opaque id), when derivable. */
|
|
55
|
+
account: string | null;
|
|
56
|
+
/** True only when a logged-out state is PROVABLE (credential absent per-version
|
|
57
|
+
* AND globally) — the caller gates a critical on this. */
|
|
58
|
+
provable: boolean;
|
|
59
|
+
}
|
|
43
60
|
/**
|
|
44
61
|
* The self-reported harness inventory a single device emits in `doctor --json`.
|
|
45
62
|
* Comparable device-to-device with no further probing.
|
|
@@ -55,6 +72,11 @@ export interface FleetInventory {
|
|
|
55
72
|
agents: RepoState | null;
|
|
56
73
|
system: RepoState | null;
|
|
57
74
|
};
|
|
75
|
+
/** Per-version sign-in state per agent id, for the fleet doctor's accounts
|
|
76
|
+
* line and cross-fleet logged-out criticals. Optional — an older CLI that
|
|
77
|
+
* predates this field omits it, and the caller degrades to a warning
|
|
78
|
+
* ("older agents-cli — can't report per-version sign-in"). */
|
|
79
|
+
signIn?: Record<string, FleetVersionSignIn[]>;
|
|
58
80
|
}
|
|
59
81
|
/** A device's inventory paired with its name (and reachability). A device that
|
|
60
82
|
* was unreachable / failed to report carries `inventory: null` and is skipped
|
|
@@ -45,20 +45,25 @@ function repoLabel(repo) {
|
|
|
45
45
|
}
|
|
46
46
|
/** Describe how a remote repo state diverges from the local baseline, or null
|
|
47
47
|
* when they match. Compares HEAD first (the load-bearing difference), then
|
|
48
|
-
* branch, then a dirty tree on either side
|
|
48
|
+
* branch, then a dirty tree on either side.
|
|
49
|
+
*
|
|
50
|
+
* HEAD and branch differences are symmetric — by convention the remote is the
|
|
51
|
+
* one "diverged from the baseline" — but a dirty tree belongs to exactly one
|
|
52
|
+
* box, and blaming the wrong one sends the user to a clean machine. */
|
|
49
53
|
function describeRepoDrift(local, remote) {
|
|
50
54
|
if (local.head && remote.head && local.head !== remote.head) {
|
|
51
|
-
return `HEAD ${remote.head} != local ${local.head}
|
|
55
|
+
return { detail: `repo diverged: HEAD ${remote.head} != local ${local.head}`, blame: 'remote' };
|
|
52
56
|
}
|
|
53
57
|
if (local.branch !== remote.branch) {
|
|
54
|
-
return
|
|
58
|
+
return {
|
|
59
|
+
detail: `repo diverged: branch ${remote.branch ?? 'detached'} != local ${local.branch ?? 'detached'}`,
|
|
60
|
+
blame: 'remote',
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
|
-
// Flag a dirty tree on EITHER side (symmetric with HEAD/branch above), and name
|
|
57
|
-
// the side that has the uncommitted changes — the remote, or the local baseline.
|
|
58
63
|
if (remote.dirty !== local.dirty) {
|
|
59
64
|
return remote.dirty
|
|
60
|
-
? '
|
|
61
|
-
: '
|
|
65
|
+
? { detail: 'tree has uncommitted changes', blame: 'remote' }
|
|
66
|
+
: { detail: 'tree has uncommitted changes', blame: 'local' };
|
|
62
67
|
}
|
|
63
68
|
return null;
|
|
64
69
|
}
|
|
@@ -78,6 +83,9 @@ export function compareFleetInventories(devices, baselineName) {
|
|
|
78
83
|
const comparedDevices = [];
|
|
79
84
|
const skippedDevices = [];
|
|
80
85
|
const divergences = [];
|
|
86
|
+
/** Repos already reported as the BASELINE's problem — one dirty local tree is
|
|
87
|
+
* one finding, not one per remote compared against. */
|
|
88
|
+
const localBlamed = new Set();
|
|
81
89
|
if (!baseline) {
|
|
82
90
|
// No local baseline to compare against — record every remote as skipped so
|
|
83
91
|
// the caller can say so, but emit no divergences (we can't know the truth).
|
|
@@ -163,15 +171,31 @@ export function compareFleetInventories(devices, baselineName) {
|
|
|
163
171
|
if (!localRepo || !remoteRepo)
|
|
164
172
|
continue; // one side isn't a readable repo
|
|
165
173
|
const drift = describeRepoDrift(localRepo, remoteRepo);
|
|
166
|
-
if (drift)
|
|
174
|
+
if (!drift)
|
|
175
|
+
continue;
|
|
176
|
+
if (drift.blame === 'local') {
|
|
177
|
+
// The BASELINE owns this one. File it against the baseline, and only
|
|
178
|
+
// once: the local tree being dirty is a single fact about this machine,
|
|
179
|
+
// not one problem per remote we happened to compare against.
|
|
180
|
+
if (localBlamed.has(repo))
|
|
181
|
+
continue;
|
|
182
|
+
localBlamed.add(repo);
|
|
167
183
|
divergences.push({
|
|
168
184
|
kind: 'repo-drift',
|
|
169
|
-
device:
|
|
185
|
+
device: baselineName,
|
|
170
186
|
category: repo,
|
|
171
187
|
name: repoLabel(repo),
|
|
172
|
-
message: `${
|
|
188
|
+
message: `${baselineName} ${repoLabel(repo)} ${drift.detail}`,
|
|
173
189
|
});
|
|
190
|
+
continue;
|
|
174
191
|
}
|
|
192
|
+
divergences.push({
|
|
193
|
+
kind: 'repo-drift',
|
|
194
|
+
device: remote.name,
|
|
195
|
+
category: repo,
|
|
196
|
+
name: repoLabel(repo),
|
|
197
|
+
message: `${remote.name} ${repoLabel(repo)} ${drift.detail}`,
|
|
198
|
+
});
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
divergences.sort((a, b) => a.device.localeCompare(b.device) ||
|
|
@@ -8,12 +8,23 @@
|
|
|
8
8
|
* FleetInventory} that both the local baseline and every remote box serialize
|
|
9
9
|
* into their doctor payload; the comparator then diffs those payloads.
|
|
10
10
|
*/
|
|
11
|
-
import { type FleetInventory } from './fleet-divergence.js';
|
|
11
|
+
import { type FleetInventory, type FleetVersionSignIn } from './fleet-divergence.js';
|
|
12
|
+
/**
|
|
13
|
+
* Probe every installed version's sign-in state, per agent. For each version we
|
|
14
|
+
* read its own home's account (via the shim-set config dir) and, when logged
|
|
15
|
+
* out, decide whether that is PROVABLE: an agent that can't be inspected
|
|
16
|
+
* (`!supportsAccountInspection`) never claims logged out, and a version that
|
|
17
|
+
* merely lacks its own credential but shares the global login (`active`) is
|
|
18
|
+
* signed in, not out. Pure reads (file-presence + cheap account parse), no
|
|
19
|
+
* network, no keychain prompt. Only agents with at least one installed version
|
|
20
|
+
* appear, so the map lines up with {@link FleetInventory.agentVersions}.
|
|
21
|
+
*/
|
|
22
|
+
export declare function collectLocalFleetSignIn(): Promise<Record<string, FleetVersionSignIn[]>>;
|
|
12
23
|
/**
|
|
13
24
|
* Collect this machine's harness inventory: installed resources per kind,
|
|
14
|
-
* installed version ids per agent,
|
|
15
|
-
* reads — never mutates the install. `promptcuts` (a
|
|
16
|
-
* in {@link getAvailableResources}) is surfaced as a
|
|
17
|
-
* compares like any other named resource.
|
|
25
|
+
* installed version ids per agent, `.agents`/`.system` repo state, and
|
|
26
|
+
* per-version sign-in. Pure reads — never mutates the install. `promptcuts` (a
|
|
27
|
+
* single present/absent bit in {@link getAvailableResources}) is surfaced as a
|
|
28
|
+
* one-element list so it compares like any other named resource.
|
|
18
29
|
*/
|
|
19
|
-
export declare function collectLocalFleetInventory(cwd?: string): FleetInventory
|
|
30
|
+
export declare function collectLocalFleetInventory(cwd?: string): Promise<FleetInventory>;
|
|
@@ -8,24 +8,71 @@
|
|
|
8
8
|
* FleetInventory} that both the local baseline and every remote box serialize
|
|
9
9
|
* into their doctor payload; the comparator then diffs those payloads.
|
|
10
10
|
*/
|
|
11
|
-
import { getAvailableResources, listInstalledVersions } from '../versions.js';
|
|
11
|
+
import { getAvailableResources, getVersionHomePath, listInstalledVersions } from '../versions.js';
|
|
12
12
|
import { getUserAgentsDir, getSystemAgentsDir } from '../state.js';
|
|
13
13
|
import { readRepoState } from '../git.js';
|
|
14
|
-
import { ALL_AGENT_IDS } from '../agents.js';
|
|
15
|
-
import { FLEET_RESOURCE_KINDS } from './fleet-divergence.js';
|
|
14
|
+
import { ALL_AGENT_IDS, accountDisplayLabel, credentialPresence, getAccountInfo, supportsAccountInspection, } from '../agents.js';
|
|
15
|
+
import { FLEET_RESOURCE_KINDS, } from './fleet-divergence.js';
|
|
16
16
|
function toRepoState(snap) {
|
|
17
17
|
if (!snap)
|
|
18
18
|
return null;
|
|
19
19
|
return { branch: snap.branch, head: snap.head, dirty: snap.dirty };
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Probe every installed version's sign-in state, per agent. For each version we
|
|
23
|
+
* read its own home's account (via the shim-set config dir) and, when logged
|
|
24
|
+
* out, decide whether that is PROVABLE: an agent that can't be inspected
|
|
25
|
+
* (`!supportsAccountInspection`) never claims logged out, and a version that
|
|
26
|
+
* merely lacks its own credential but shares the global login (`active`) is
|
|
27
|
+
* signed in, not out. Pure reads (file-presence + cheap account parse), no
|
|
28
|
+
* network, no keychain prompt. Only agents with at least one installed version
|
|
29
|
+
* appear, so the map lines up with {@link FleetInventory.agentVersions}.
|
|
30
|
+
*/
|
|
31
|
+
export async function collectLocalFleetSignIn() {
|
|
32
|
+
const out = {};
|
|
33
|
+
await Promise.all(ALL_AGENT_IDS.map(async (agent) => {
|
|
34
|
+
const versions = listInstalledVersions(agent);
|
|
35
|
+
if (versions.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const rows = await Promise.all(versions.map(async (version) => {
|
|
38
|
+
const home = getVersionHomePath(agent, version);
|
|
39
|
+
let signedIn = false;
|
|
40
|
+
let account = null;
|
|
41
|
+
try {
|
|
42
|
+
const info = await getAccountInfo(agent, home);
|
|
43
|
+
signedIn = info.signedIn;
|
|
44
|
+
account = accountDisplayLabel(info) || null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* advisory only — treat as logged out, provability decided below */
|
|
48
|
+
}
|
|
49
|
+
// Provable logout: the agent is inspectable AND the credential is absent
|
|
50
|
+
// from BOTH the version home and the active/global HOME. An opaque or
|
|
51
|
+
// keychain-only agent, or one sharing the global login, is never a
|
|
52
|
+
// provable logout.
|
|
53
|
+
let provable = false;
|
|
54
|
+
if (!signedIn && supportsAccountInspection(agent)) {
|
|
55
|
+
const presence = credentialPresence(agent, home);
|
|
56
|
+
// `knownLocation` is load-bearing: an agent can sit in the inspection
|
|
57
|
+
// set with no credential path (cursor does), and then both probes are
|
|
58
|
+
// false only because there is nothing to look for. Treating that as a
|
|
59
|
+
// provable logout prints a CRITICAL for a version that is signed in.
|
|
60
|
+
provable = presence.knownLocation && !presence.perVersion && !presence.active;
|
|
61
|
+
}
|
|
62
|
+
return { version, signedIn, account, provable };
|
|
63
|
+
}));
|
|
64
|
+
out[agent] = rows;
|
|
65
|
+
}));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
21
68
|
/**
|
|
22
69
|
* Collect this machine's harness inventory: installed resources per kind,
|
|
23
|
-
* installed version ids per agent,
|
|
24
|
-
* reads — never mutates the install. `promptcuts` (a
|
|
25
|
-
* in {@link getAvailableResources}) is surfaced as a
|
|
26
|
-
* compares like any other named resource.
|
|
70
|
+
* installed version ids per agent, `.agents`/`.system` repo state, and
|
|
71
|
+
* per-version sign-in. Pure reads — never mutates the install. `promptcuts` (a
|
|
72
|
+
* single present/absent bit in {@link getAvailableResources}) is surfaced as a
|
|
73
|
+
* one-element list so it compares like any other named resource.
|
|
27
74
|
*/
|
|
28
|
-
export function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
75
|
+
export async function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
29
76
|
const available = getAvailableResources(cwd);
|
|
30
77
|
const resources = {};
|
|
31
78
|
for (const kind of FLEET_RESOURCE_KINDS) {
|
|
@@ -53,5 +100,6 @@ export function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
|
53
100
|
agents: toRepoState(readRepoState(getUserAgentsDir())),
|
|
54
101
|
system: toRepoState(readRepoState(getSystemAgentsDir())),
|
|
55
102
|
},
|
|
103
|
+
signIn: await collectLocalFleetSignIn(),
|
|
56
104
|
};
|
|
57
105
|
}
|
|
@@ -142,3 +142,28 @@ export declare function addIgnored(name: string): Promise<Set<string>>;
|
|
|
142
142
|
/** Remove a node name from the ignore-list (un-ignore). Returns false if it was
|
|
143
143
|
* not ignored. */
|
|
144
144
|
export declare function removeIgnored(name: string): Promise<boolean>;
|
|
145
|
+
/**
|
|
146
|
+
* Auto-launch preferences: which registered devices are eligible for Factory's
|
|
147
|
+
* auto-host selection, and which are preferred. Stored as a sibling to the
|
|
148
|
+
* registry and ignore-list under ~/.agents/.history/devices/.
|
|
149
|
+
*/
|
|
150
|
+
export interface AutoLaunchPreference {
|
|
151
|
+
enabled?: boolean;
|
|
152
|
+
preferred?: boolean;
|
|
153
|
+
}
|
|
154
|
+
export interface AutoLaunchPreferences {
|
|
155
|
+
devices: Record<string, AutoLaunchPreference>;
|
|
156
|
+
updatedAt: string;
|
|
157
|
+
}
|
|
158
|
+
/** Load auto-launch preferences. Missing or malformed file => empty map. */
|
|
159
|
+
export declare function loadAutoLaunchPreferences(): Promise<Record<string, AutoLaunchPreference>>;
|
|
160
|
+
/** True if the device is enabled for auto-launch. Missing entry defaults to true. */
|
|
161
|
+
export declare function isAutoLaunchEnabled(name: string): Promise<boolean>;
|
|
162
|
+
/** Set whether a device is enabled for auto-launch. Setting to the default
|
|
163
|
+
* (enabled) removes the entry to keep the file minimal. */
|
|
164
|
+
export declare function setAutoLaunchEnabled(name: string, enabled: boolean): Promise<void>;
|
|
165
|
+
/** True if the device is preferred for auto-launch ranking. */
|
|
166
|
+
export declare function isAutoLaunchPreferred(name: string): Promise<boolean>;
|
|
167
|
+
/** Set whether a device is preferred for auto-launch. Setting to the default
|
|
168
|
+
* (not preferred) removes the flag to keep the file minimal. */
|
|
169
|
+
export declare function setAutoLaunchPreferred(name: string, preferred: boolean): Promise<void>;
|
|
@@ -18,7 +18,7 @@ import * as fsSync from 'fs';
|
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import { randomBytes } from 'crypto';
|
|
20
20
|
import lockfile from 'proper-lockfile';
|
|
21
|
-
import { getDevicesRegistryPath, getDevicesIgnoredPath } from '../state.js';
|
|
21
|
+
import { getDevicesRegistryPath, getDevicesIgnoredPath, getDevicesAutoLaunchPath } from '../state.js';
|
|
22
22
|
/** A device's effective role, defaulting to `worker` when unset. */
|
|
23
23
|
export function deviceRole(d) {
|
|
24
24
|
return d.role ?? 'worker';
|
|
@@ -288,3 +288,84 @@ export async function removeIgnored(name) {
|
|
|
288
288
|
return true;
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
|
+
function autoLaunchPath() {
|
|
292
|
+
return getDevicesAutoLaunchPath();
|
|
293
|
+
}
|
|
294
|
+
/** Load auto-launch preferences. Missing or malformed file => empty map. */
|
|
295
|
+
export async function loadAutoLaunchPreferences() {
|
|
296
|
+
const p = autoLaunchPath();
|
|
297
|
+
let raw;
|
|
298
|
+
try {
|
|
299
|
+
raw = await fs.readFile(p, 'utf-8');
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err && err.code === 'ENOENT')
|
|
303
|
+
return {};
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(raw);
|
|
308
|
+
return parsed.devices && typeof parsed.devices === 'object' ? parsed.devices : {};
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
throw new Error(`Device auto-launch preferences corrupted at ${p}: ${err?.message ?? err}. Inspect and restore from backup.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** True if the device is enabled for auto-launch. Missing entry defaults to true. */
|
|
315
|
+
export async function isAutoLaunchEnabled(name) {
|
|
316
|
+
assertValidDeviceName(name);
|
|
317
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
318
|
+
return prefs[name]?.enabled !== false;
|
|
319
|
+
}
|
|
320
|
+
/** Set whether a device is enabled for auto-launch. Setting to the default
|
|
321
|
+
* (enabled) removes the entry to keep the file minimal. */
|
|
322
|
+
export async function setAutoLaunchEnabled(name, enabled) {
|
|
323
|
+
assertValidDeviceName(name);
|
|
324
|
+
const p = autoLaunchPath();
|
|
325
|
+
await withRegistryLock(p, async () => {
|
|
326
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
327
|
+
if (enabled) {
|
|
328
|
+
if (prefs[name]) {
|
|
329
|
+
const { enabled: _, ...rest } = prefs[name];
|
|
330
|
+
if (Object.keys(rest).length === 0) {
|
|
331
|
+
delete prefs[name];
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
prefs[name] = rest;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
prefs[name] = { ...prefs[name], enabled: false };
|
|
340
|
+
}
|
|
341
|
+
await atomicWriteJson(p, { devices: prefs, updatedAt: new Date().toISOString() });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/** True if the device is preferred for auto-launch ranking. */
|
|
345
|
+
export async function isAutoLaunchPreferred(name) {
|
|
346
|
+
assertValidDeviceName(name);
|
|
347
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
348
|
+
return prefs[name]?.preferred === true;
|
|
349
|
+
}
|
|
350
|
+
/** Set whether a device is preferred for auto-launch. Setting to the default
|
|
351
|
+
* (not preferred) removes the flag to keep the file minimal. */
|
|
352
|
+
export async function setAutoLaunchPreferred(name, preferred) {
|
|
353
|
+
assertValidDeviceName(name);
|
|
354
|
+
const p = autoLaunchPath();
|
|
355
|
+
await withRegistryLock(p, async () => {
|
|
356
|
+
const prefs = await loadAutoLaunchPreferences();
|
|
357
|
+
if (preferred) {
|
|
358
|
+
prefs[name] = { ...prefs[name], preferred: true };
|
|
359
|
+
}
|
|
360
|
+
else if (prefs[name]) {
|
|
361
|
+
const { preferred: _, ...rest } = prefs[name];
|
|
362
|
+
if (Object.keys(rest).length === 0) {
|
|
363
|
+
delete prefs[name];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
prefs[name] = rest;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
await atomicWriteJson(p, { devices: prefs, updatedAt: new Date().toISOString() });
|
|
370
|
+
});
|
|
371
|
+
}
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
export declare function levelFor(event: EventType): EventLevel;
|
|
18
18
|
export interface EventMeta {
|
|
19
19
|
ts: string;
|
|
@@ -156,6 +156,13 @@ export declare function emitCommand(command: string, args?: string[], payload?:
|
|
|
156
156
|
* Emit an error event with full details.
|
|
157
157
|
*/
|
|
158
158
|
export declare function emitError(err: Error | string, payload?: EventPayload): void;
|
|
159
|
+
/**
|
|
160
|
+
* Emit a friction event — a structured, point-of-use record of a failure or
|
|
161
|
+
* block the CLI just hit. `surface` is the subsystem (teams, browser, secrets,
|
|
162
|
+
* guard, …); `failureId` is a stable slug that lets the nightly routine group
|
|
163
|
+
* the same failure across sessions (e.g. 'remote-cwd-on-add', 'not-installed').
|
|
164
|
+
*/
|
|
165
|
+
export declare function emitFriction(surface: string, failureId: string, payload?: EventPayload): void;
|
|
159
166
|
/**
|
|
160
167
|
* Remove log files older than the retention period.
|
|
161
168
|
* Removes numbered gzip archives whose filesystem mtime exceeds retention.
|
package/dist/lib/events.js
CHANGED
|
@@ -523,6 +523,19 @@ export function emitError(err, payload = {}) {
|
|
|
523
523
|
errorStack: truncate(error.stack, 1000),
|
|
524
524
|
});
|
|
525
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Emit a friction event — a structured, point-of-use record of a failure or
|
|
528
|
+
* block the CLI just hit. `surface` is the subsystem (teams, browser, secrets,
|
|
529
|
+
* guard, …); `failureId` is a stable slug that lets the nightly routine group
|
|
530
|
+
* the same failure across sessions (e.g. 'remote-cwd-on-add', 'not-installed').
|
|
531
|
+
*/
|
|
532
|
+
export function emitFriction(surface, failureId, payload = {}) {
|
|
533
|
+
emit('friction', {
|
|
534
|
+
...payload,
|
|
535
|
+
surface,
|
|
536
|
+
failureId,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
526
539
|
// ─── Gzip rotation ──────────────────────────────────────────────────────────
|
|
527
540
|
/** Rotate the active file while its append lock is held. */
|
|
528
541
|
function maybeGzipRotateLocked(logPath) {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -66,7 +66,14 @@ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
|
|
|
66
66
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
67
67
|
* (agents run / teams) and the routine runner.
|
|
68
68
|
*/
|
|
69
|
-
export declare function resolveHeadlessMode(agent: AgentId, requested: Mode, interactive: boolean): Mode;
|
|
69
|
+
export declare function resolveHeadlessMode(agent: AgentId, requested: Mode, interactive: boolean, warningContext?: string, warningState?: ModeWarningState): Mode;
|
|
70
|
+
export interface ModeWarningState {
|
|
71
|
+
/** Agents already warned about, so one run warns once per agent. A fallback
|
|
72
|
+
* chain degrades each agent independently and the agent that actually ran is
|
|
73
|
+
* usually not the first, so this cannot be a single boolean. */
|
|
74
|
+
emitted?: Set<AgentId>;
|
|
75
|
+
quiet?: boolean;
|
|
76
|
+
}
|
|
70
77
|
/**
|
|
71
78
|
* The mode an agent should run in when the caller has no preference.
|
|
72
79
|
*
|
|
@@ -94,6 +101,10 @@ export interface ExecOptions {
|
|
|
94
101
|
cwd?: string;
|
|
95
102
|
/** Force headless mode even when no prompt is provided (e.g. piping via stdin). */
|
|
96
103
|
headless?: boolean;
|
|
104
|
+
/** Prefix for mode-degradation warnings emitted by shared headless paths. */
|
|
105
|
+
modeWarningContext?: string;
|
|
106
|
+
/** Shared across command previews/spawns/loop iterations so degradation warns once. */
|
|
107
|
+
modeWarningState?: ModeWarningState;
|
|
97
108
|
json?: boolean;
|
|
98
109
|
model?: string;
|
|
99
110
|
addDirs?: string[];
|
|
@@ -400,8 +411,8 @@ export declare function detectAuthFailure(text: string): boolean;
|
|
|
400
411
|
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
401
412
|
* and the `result`+`is_error` text are the reliable signals.
|
|
402
413
|
*
|
|
403
|
-
* Gated on the Claude stream-json shape
|
|
404
|
-
*
|
|
414
|
+
* Gated on the Claude-compatible stream-json shape emitted by Claude and Cursor;
|
|
415
|
+
* callers pass their agent so unrelated stream formats cannot match by accident.
|
|
405
416
|
*/
|
|
406
417
|
export declare function detectAuthFailureEvent(logText: string, agent: AgentId): boolean;
|
|
407
418
|
/**
|
package/dist/lib/exec.js
CHANGED
|
@@ -28,6 +28,7 @@ import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
|
28
28
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
29
29
|
import { isTmuxInstalled } from './tmux/binary.js';
|
|
30
30
|
import { shellQuote } from './ssh-exec.js';
|
|
31
|
+
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
31
32
|
/**
|
|
32
33
|
* Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
|
|
33
34
|
*
|
|
@@ -123,10 +124,35 @@ export function resolveMode(agent, requested) {
|
|
|
123
124
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
124
125
|
* (agents run / teams) and the routine runner.
|
|
125
126
|
*/
|
|
126
|
-
export function resolveHeadlessMode(agent, requested, interactive) {
|
|
127
|
+
export function resolveHeadlessMode(agent, requested, interactive, warningContext, warningState) {
|
|
127
128
|
const mode = resolveMode(agent, requested);
|
|
129
|
+
const warn = (message) => {
|
|
130
|
+
if (warningState?.quiet)
|
|
131
|
+
return;
|
|
132
|
+
if (warningState) {
|
|
133
|
+
warningState.emitted ??= new Set();
|
|
134
|
+
if (warningState.emitted.has(agent))
|
|
135
|
+
return;
|
|
136
|
+
warningState.emitted.add(agent);
|
|
137
|
+
}
|
|
138
|
+
process.stderr.write(message);
|
|
139
|
+
};
|
|
140
|
+
if (mode !== requested) {
|
|
141
|
+
const subject = warningContext ? `${warningContext}: ` : '';
|
|
142
|
+
if (requested === 'plan') {
|
|
143
|
+
const limitation = agent === 'cursor'
|
|
144
|
+
? "cursor's read-only plan mode is not enabled in this build"
|
|
145
|
+
: `${agent} has no read-only 'plan' mode`;
|
|
146
|
+
warn(`[agents] ${subject}${limitation}; ` +
|
|
147
|
+
`running '${mode}' (writable) instead${agent === 'cursor' ? ' (RUSH-2101)' : ''}. ` +
|
|
148
|
+
`Pass --mode ${mode} to silence this.\n`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
warn(`[agents] ${subject}${agent} has no '${requested}' mode; using '${mode}'.\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
128
154
|
if (!interactive && mode === 'plan' && AGENTS[agent].capabilities.headlessPlan === false) {
|
|
129
|
-
|
|
155
|
+
warn(`warning: ${agent} has no headless plan mode; running --mode auto instead\n`);
|
|
130
156
|
return resolveMode(agent, 'auto');
|
|
131
157
|
}
|
|
132
158
|
return mode;
|
|
@@ -259,7 +285,15 @@ export function buildExecEnv(options) {
|
|
|
259
285
|
? resolvedVersion
|
|
260
286
|
: (resolvedVersion && isVersionInstalled('claude', resolvedVersion) ? resolvedVersion : null);
|
|
261
287
|
if (version) {
|
|
262
|
-
|
|
288
|
+
const versionHome = getVersionHomePath('claude', version);
|
|
289
|
+
result.CLAUDE_CONFIG_DIR = path.join(versionHome, '.claude');
|
|
290
|
+
const setupToken = resolveClaudeSetupToken(versionHome);
|
|
291
|
+
if (setupToken) {
|
|
292
|
+
// A token keyed to this version home's own account replaces any ambient
|
|
293
|
+
// shared value inherited from the launcher. options.env still wins below
|
|
294
|
+
// for explicit caller overrides.
|
|
295
|
+
result.CLAUDE_CODE_OAUTH_TOKEN = setupToken;
|
|
296
|
+
}
|
|
263
297
|
// A managed pin lives in a per-version dir; Claude Code's own background
|
|
264
298
|
// auto-updater would rewrite that pinned binary in place (and has left it
|
|
265
299
|
// half-swapped and broken). Disable it so a pin stays a pin. Honor an
|
|
@@ -416,7 +450,6 @@ export const AGENT_COMMANDS = {
|
|
|
416
450
|
base: ['cursor-agent'],
|
|
417
451
|
promptFlag: '-p',
|
|
418
452
|
modeFlags: {
|
|
419
|
-
// cursor-agent has no read-only flag; we only expose edit + skip.
|
|
420
453
|
edit: [],
|
|
421
454
|
skip: ['-f'],
|
|
422
455
|
},
|
|
@@ -660,13 +693,18 @@ export function buildExecCommand(options) {
|
|
|
660
693
|
// degrades to `auto` with a stderr warning (see resolveHeadlessMode)
|
|
661
694
|
// - `skip` on an unsupported agent → throws a clear error
|
|
662
695
|
// After resolution, the chosen mode is guaranteed to be in template.modeFlags.
|
|
663
|
-
const resolvedMode = resolveHeadlessMode(options.agent, normalizeMode(options.mode), interactive);
|
|
696
|
+
const resolvedMode = resolveHeadlessMode(options.agent, normalizeMode(options.mode), interactive, options.modeWarningContext, options.modeWarningState);
|
|
664
697
|
const modeFlags = template.modeFlags[resolvedMode];
|
|
665
698
|
if (!modeFlags) {
|
|
666
699
|
// Defense in depth: would only fire if AGENTS.capabilities.modes and
|
|
667
700
|
// AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
|
|
668
701
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
669
702
|
}
|
|
703
|
+
if (options.agent === 'cursor' && resolvedMode === 'edit' && !interactive) {
|
|
704
|
+
// A configured headless run is the workspace trust decision. Keep this
|
|
705
|
+
// narrower than --yolo/-f, which also bypasses permission checks.
|
|
706
|
+
cmd.push('--trust');
|
|
707
|
+
}
|
|
670
708
|
// Codex's workspace-write sandbox blocks $HOME (verified against the live CLI
|
|
671
709
|
// and OpenAI's sandbox docs: writable roots extend scope "without removing the
|
|
672
710
|
// sandbox entirely"). But the model routinely shells out to `agents ...`, whose
|
|
@@ -1260,7 +1298,10 @@ async function spawnAgent(options) {
|
|
|
1260
1298
|
agent: options.agent,
|
|
1261
1299
|
version: options.version,
|
|
1262
1300
|
cwd: options.cwd || process.cwd(),
|
|
1263
|
-
mode
|
|
1301
|
+
// The mode that ran, not the one requested — `agents run` passes the
|
|
1302
|
+
// requested mode so the resolver can warn, but telemetry must agree with
|
|
1303
|
+
// the audit log. See RUSH-2106 for removing that ambiguity at the source.
|
|
1304
|
+
mode: resolveMode(options.agent, normalizeMode(options.mode)),
|
|
1264
1305
|
model: options.model,
|
|
1265
1306
|
interactive,
|
|
1266
1307
|
sessionId: options.sessionId,
|
|
@@ -1540,6 +1581,7 @@ export function detectRateLimit(text) {
|
|
|
1540
1581
|
export const AUTH_FAILURE_PATTERNS = [
|
|
1541
1582
|
/OAuth (?:access token has been revoked|session expired)/i,
|
|
1542
1583
|
/(?:Please run|run) \/login/i,
|
|
1584
|
+
/Please run 'agent login' first/i,
|
|
1543
1585
|
/\bNot logged in\b/i,
|
|
1544
1586
|
/Invalid authentication credentials/i,
|
|
1545
1587
|
/Failed to authenticate/i,
|
|
@@ -1564,11 +1606,11 @@ export function detectAuthFailure(text) {
|
|
|
1564
1606
|
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
1565
1607
|
* and the `result`+`is_error` text are the reliable signals.
|
|
1566
1608
|
*
|
|
1567
|
-
* Gated on the Claude stream-json shape
|
|
1568
|
-
*
|
|
1609
|
+
* Gated on the Claude-compatible stream-json shape emitted by Claude and Cursor;
|
|
1610
|
+
* callers pass their agent so unrelated stream formats cannot match by accident.
|
|
1569
1611
|
*/
|
|
1570
1612
|
export function detectAuthFailureEvent(logText, agent) {
|
|
1571
|
-
if (agent !== 'claude')
|
|
1613
|
+
if (agent !== 'claude' && agent !== 'cursor')
|
|
1572
1614
|
return false;
|
|
1573
1615
|
const lines = logText.split('\n');
|
|
1574
1616
|
for (const line of lines) {
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export declare function formatDie(msg: string, opts?: DieOptions): {
|
|
|
27
27
|
* keep the original red-stderr behavior.
|
|
28
28
|
*/
|
|
29
29
|
export declare function die(msg: string, code?: number, opts?: DieOptions): never;
|
|
30
|
+
/**
|
|
31
|
+
* `die()` with a structured friction event attached. Use this at CLI error
|
|
32
|
+
* chokepoints so the nightly routine can classify and rank recurring failures
|
|
33
|
+
* without re-parsing transcripts. `surface` is the subsystem (teams, browser,
|
|
34
|
+
* secrets, guard, …); `failureId` is a stable slug (e.g. 'remote-cwd-on-add').
|
|
35
|
+
*/
|
|
36
|
+
export declare function dieFriction(surface: string, failureId: string, msg: string, code?: number, opts?: DieOptions): never;
|
|
30
37
|
/**
|
|
31
38
|
* Truncate `s` to at most `max` characters, appending a single-char ellipsis
|
|
32
39
|
* (`…`) when shortened. Character-count based (not ANSI/width aware — use
|
package/dist/lib/format.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { readSync } from 'node:fs';
|
|
12
|
+
import { emitFriction } from './events.js';
|
|
12
13
|
/**
|
|
13
14
|
* Render a fatal error to the right stream. Pure — no I/O, no `process.exit` — so
|
|
14
15
|
* the human-vs-agent split is unit-testable. A `--json` caller gets
|
|
@@ -43,6 +44,16 @@ export function die(msg, code = 1, opts = {}) {
|
|
|
43
44
|
console.error(text);
|
|
44
45
|
process.exit(code);
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* `die()` with a structured friction event attached. Use this at CLI error
|
|
49
|
+
* chokepoints so the nightly routine can classify and rank recurring failures
|
|
50
|
+
* without re-parsing transcripts. `surface` is the subsystem (teams, browser,
|
|
51
|
+
* secrets, guard, …); `failureId` is a stable slug (e.g. 'remote-cwd-on-add').
|
|
52
|
+
*/
|
|
53
|
+
export function dieFriction(surface, failureId, msg, code = 1, opts = {}) {
|
|
54
|
+
emitFriction(surface, failureId, { error: msg });
|
|
55
|
+
die(msg, code, opts);
|
|
56
|
+
}
|
|
46
57
|
/**
|
|
47
58
|
* Truncate `s` to at most `max` characters, appending a single-char ellipsis
|
|
48
59
|
* (`…`) when shortened. Character-count based (not ANSI/width aware — use
|
|
Binary file
|