@phnx-labs/agents-cli 1.20.87 → 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 +60 -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.js +1 -0
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -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/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- 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/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/sync/agents.js +0 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- 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/usage.d.ts +72 -1
- package/dist/lib/usage.js +21 -27
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -1667,7 +1667,7 @@ export function registerRunCommand(program) {
|
|
|
1667
1667
|
const { pickSessionInteractive } = await import('./sessions.js');
|
|
1668
1668
|
const { buildContinuePrompt } = await import('../lib/loop.js');
|
|
1669
1669
|
// Freshen the index for this agent before any lookup (incremental, cached).
|
|
1670
|
-
// AgentId is wider than SessionAgentId (
|
|
1670
|
+
// AgentId is wider than SessionAgentId (amp/kiro/goose/copilot keep no transcripts);
|
|
1671
1671
|
// those simply yield no matches and fall through to the not-found error.
|
|
1672
1672
|
const sessionAgent = agent;
|
|
1673
1673
|
await discoverSessions({ agent: sessionAgent, version });
|
|
@@ -1899,8 +1899,8 @@ export function registerRunCommand(program) {
|
|
|
1899
1899
|
// safest native mode (modes[0], typically edit). That covers both the
|
|
1900
1900
|
// implicit default and an explicit `--mode plan`, so multi-agent
|
|
1901
1901
|
// scripts can pass a uniform plan flag without per-agent branching.
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1902
|
+
// Mode degradation is never silent: buildExecCommand emits one stderr
|
|
1903
|
+
// warning for the requested-to-resolved transition unless --quiet is set.
|
|
1904
1904
|
// `skip` still hard-fails when unsupported — pretending we bypassed
|
|
1905
1905
|
// permissions would be unsafe.
|
|
1906
1906
|
const modeIsDefault = modeSource === 'default';
|
|
@@ -1913,20 +1913,7 @@ export function registerRunCommand(program) {
|
|
|
1913
1913
|
console.error(chalk.red(err.message));
|
|
1914
1914
|
process.exit(1);
|
|
1915
1915
|
}
|
|
1916
|
-
|
|
1917
|
-
mode = resolvedMode;
|
|
1918
|
-
if (!options.quiet) {
|
|
1919
|
-
if (requestedMode === 'plan' && !modeIsDefault) {
|
|
1920
|
-
process.stderr.write(chalk.yellow(`[agents] ${agent} has no read-only 'plan' mode; using '${mode}' (writable) instead. Pass --mode ${mode} to silence this.\n`));
|
|
1921
|
-
}
|
|
1922
|
-
else {
|
|
1923
|
-
process.stderr.write(chalk.gray(`[agents] ${agent} has no '${requestedMode}' mode; using '${mode}'\n`));
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
else {
|
|
1928
|
-
mode = resolvedMode;
|
|
1929
|
-
}
|
|
1916
|
+
mode = resolvedMode;
|
|
1930
1917
|
// Fail fast on the headless-plan stall footgun: a slash command run
|
|
1931
1918
|
// headless under the implicit default 'plan' mode hangs forever at
|
|
1932
1919
|
// ExitPlanMode (no TTY to approve the plan). Tell the user how to fix it
|
|
@@ -1935,7 +1922,7 @@ export function registerRunCommand(program) {
|
|
|
1935
1922
|
const stallCmd = headlessPlanStallCommand({
|
|
1936
1923
|
prompt,
|
|
1937
1924
|
interactive: options.interactive,
|
|
1938
|
-
mode,
|
|
1925
|
+
mode: resolvedMode,
|
|
1939
1926
|
modeIsDefault,
|
|
1940
1927
|
});
|
|
1941
1928
|
if (stallCmd) {
|
|
@@ -2029,7 +2016,7 @@ export function registerRunCommand(program) {
|
|
|
2029
2016
|
version,
|
|
2030
2017
|
prompt,
|
|
2031
2018
|
interactive: options.interactive || forceInteractive,
|
|
2032
|
-
mode,
|
|
2019
|
+
mode: requestedMode,
|
|
2033
2020
|
effort,
|
|
2034
2021
|
cwd: options.cwd,
|
|
2035
2022
|
model,
|
|
@@ -2040,6 +2027,7 @@ export function registerRunCommand(program) {
|
|
|
2040
2027
|
name: options.name,
|
|
2041
2028
|
resume: resumeNative,
|
|
2042
2029
|
verbose: options.verbose,
|
|
2030
|
+
modeWarningState: { quiet: options.quiet },
|
|
2043
2031
|
// --raw, --no-tmux (commander negation → options.tmux === false), and
|
|
2044
2032
|
// --disable-tmux all bypass the interactive tmux wrapper. AGENTS_NO_TMUX=1
|
|
2045
2033
|
// does the same via the env check in exec.ts.
|
package/dist/commands/inspect.js
CHANGED
|
@@ -32,6 +32,7 @@ import { listMcpServerConfigs, discoverMcpConfigsFromRepo } from '../lib/mcp.js'
|
|
|
32
32
|
import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../lib/plugins.js';
|
|
33
33
|
import { PLUGIN_GROUP_COLORS } from './plugins.js';
|
|
34
34
|
import { countSessionsInScope } from '../lib/session/discover.js';
|
|
35
|
+
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
35
36
|
import { damerauLevenshtein } from '../lib/fuzzy.js';
|
|
36
37
|
import { terminalWidth, truncateToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
|
|
37
38
|
/** Resource kinds the inspect command can drill into. */
|
|
@@ -1256,14 +1257,11 @@ function safeStat(p) {
|
|
|
1256
1257
|
return null;
|
|
1257
1258
|
}
|
|
1258
1259
|
}
|
|
1259
|
-
const SESSION_AGENTS = new Set([
|
|
1260
|
-
'claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid',
|
|
1261
|
-
]);
|
|
1262
1260
|
function safeCountSessions(agent) {
|
|
1263
|
-
if (!
|
|
1261
|
+
if (!isSessionTrackedAgent(agent))
|
|
1264
1262
|
return 0;
|
|
1265
1263
|
try {
|
|
1266
|
-
return countSessionsInScope({ agent
|
|
1264
|
+
return countSessionsInScope({ agent });
|
|
1267
1265
|
}
|
|
1268
1266
|
catch {
|
|
1269
1267
|
return 0;
|
|
@@ -19,7 +19,7 @@ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js'
|
|
|
19
19
|
import { getRoutinesDir } from '../lib/state.js';
|
|
20
20
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
21
21
|
import { safeJoin } from '../lib/paths.js';
|
|
22
|
-
import { executeJob, executeJobDetached, monitorRunningJobs } from '../lib/runner.js';
|
|
22
|
+
import { executeJob, executeJobDetached, monitorRunningJobs, ROUTINE_AGENT_IDS } from '../lib/runner.js';
|
|
23
23
|
import { JobScheduler } from '../lib/scheduler.js';
|
|
24
24
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
25
25
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
@@ -572,7 +572,7 @@ export function registerRoutinesCommands(program) {
|
|
|
572
572
|
.command('add [nameOrPath]')
|
|
573
573
|
.description('Create a new routine from a YAML file or inline flags. Starts the scheduler automatically if it is not already running.')
|
|
574
574
|
.option('-s, --schedule <cron>', 'Cron schedule in standard format (5 fields: minute hour day month weekday)')
|
|
575
|
-
.option('-a, --agent <agent>',
|
|
575
|
+
.option('-a, --agent <agent>', `Which agent runs this routine: ${ROUTINE_AGENT_IDS.join(', ')}`)
|
|
576
576
|
.option('--workflow <name>', 'Run an installed workflow (~/.agents/workflows/<name>) via `agents run`. Mutually exclusive with --agent.')
|
|
577
577
|
.option('--command <sh>', 'Run a plain shell command directly (no agent, no auth, no sandbox) — for deterministic housekeeping routines. Mutually exclusive with --agent and --workflow; --prompt is not used.')
|
|
578
578
|
.option('-p, --prompt <prompt>', 'Task instruction for the agent')
|
package/dist/commands/ssh.js
CHANGED
|
@@ -313,7 +313,7 @@ function printFleetResults(results) {
|
|
|
313
313
|
if (failed > 0)
|
|
314
314
|
process.exitCode = 1;
|
|
315
315
|
}
|
|
316
|
-
function localHealthRow(self, stats) {
|
|
316
|
+
async function localHealthRow(self, stats) {
|
|
317
317
|
return {
|
|
318
318
|
name: self,
|
|
319
319
|
platform: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
@@ -324,7 +324,7 @@ function localHealthRow(self, stats) {
|
|
|
324
324
|
orphans: countOrphans(),
|
|
325
325
|
// Local baseline inventory for cross-device divergence (RUSH-2027) — the
|
|
326
326
|
// yardstick every remote box is compared against.
|
|
327
|
-
inventory: collectLocalFleetInventory(process.cwd()),
|
|
327
|
+
inventory: await collectLocalFleetInventory(process.cwd()),
|
|
328
328
|
};
|
|
329
329
|
}
|
|
330
330
|
async function probeRemoteHealth(target) {
|
|
@@ -369,7 +369,7 @@ async function runFleetStatus(opts) {
|
|
|
369
369
|
// read from a fresh probe, not a stale tailscale snapshot (RUSH-1965).
|
|
370
370
|
// Best-effort: a registry write must never break the status render.
|
|
371
371
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
372
|
-
const rows = [localHealthRow(self, statsMap.get(self))];
|
|
372
|
+
const rows = [await localHealthRow(self, statsMap.get(self))];
|
|
373
373
|
const remoteTargets = remoteFleetTargets(planned, self)
|
|
374
374
|
.map((t) => ({
|
|
375
375
|
name: t.device.name,
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* - codex: parsed from latest session log's rate_limits event
|
|
7
7
|
* - kimi: live Kimi Code /usages API call (cached for 2 minutes)
|
|
8
8
|
* - droid: live Factory billing/limits API call (cached for 2 minutes)
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
9
|
+
* - grok: parsed from the latest local usage event
|
|
10
|
+
* - cursor: live Cursor usage API call (cached for 2 minutes)
|
|
11
|
+
* - others: marked as "not exposed by CLI"
|
|
11
12
|
*/
|
|
12
13
|
import type { Command } from 'commander';
|
|
13
14
|
import type { AgentId } from '../lib/types.js';
|
package/dist/commands/usage.js
CHANGED
|
@@ -2,14 +2,7 @@ import { addHostOption } from '../lib/hosts/option.js';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
|
|
4
4
|
import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
|
|
5
|
-
import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
6
|
-
/**
|
|
7
|
-
* Agents whose CLI surfaces usage data we can read today. Kept in sync with the
|
|
8
|
-
* live/last-seen sources `getUsageInfo` dispatches on in `../lib/usage.js`
|
|
9
|
-
* (claude, codex, kimi, droid) — an agent with a usage source but missing here
|
|
10
|
-
* would wrongly print "does not publish usage data" for a signed-in account.
|
|
11
|
-
*/
|
|
12
|
-
const USAGE_SUPPORTED = new Set(['claude', 'codex', 'kimi', 'droid']);
|
|
5
|
+
import { agentReportsUsage, formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
13
6
|
export function registerUsageCommand(program) {
|
|
14
7
|
addHostOption(program.command('usage [agent]'))
|
|
15
8
|
.description('Show rate-limit / quota usage per agent')
|
|
@@ -55,7 +48,7 @@ async function collectAgentUsage(agentId) {
|
|
|
55
48
|
// Plain name — color is applied only at text-render time (formatAgentUsage), so
|
|
56
49
|
// `--json` never emits ANSI escapes in `label` (e.g. under FORCE_COLOR=1).
|
|
57
50
|
const label = AGENTS[agentId].name;
|
|
58
|
-
if (!
|
|
51
|
+
if (!agentReportsUsage(agentId)) {
|
|
59
52
|
return { agent: agentId, label, status: 'unsupported' };
|
|
60
53
|
}
|
|
61
54
|
const versions = listInstalledVersions(agentId);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -165,7 +165,7 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
|
|
|
165
165
|
*/
|
|
166
166
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
167
167
|
/** Agents whose local credential formats expose enough state for account selection. */
|
|
168
|
-
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
168
|
+
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "cursor", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
169
169
|
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
170
170
|
export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
171
171
|
/**
|
|
@@ -176,6 +176,36 @@ export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
|
176
176
|
export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
177
177
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
178
178
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
179
|
+
/** Where an agent's credential file lives, split into the per-version copy and
|
|
180
|
+
* the active/global copy under the real HOME. */
|
|
181
|
+
export interface CredentialPresence {
|
|
182
|
+
/** The credential file exists inside the passed version home. */
|
|
183
|
+
perVersion: boolean;
|
|
184
|
+
/** The credential file exists under the active/global HOME (the one the login
|
|
185
|
+
* symlink actually targets), independent of the version home. */
|
|
186
|
+
active: boolean;
|
|
187
|
+
/** Whether agents-cli knows WHERE this agent's credential lives at all — i.e.
|
|
188
|
+
* the agent has an entry in {@link CREDENTIAL_FILE_SEGMENTS}. When false both
|
|
189
|
+
* probes are trivially false because there is nothing to look for, so absence
|
|
190
|
+
* is NOT evidence of a logout and no caller may treat it as provable.
|
|
191
|
+
*
|
|
192
|
+
* This is deliberately separate from `supportsAccountInspection`: the two
|
|
193
|
+
* registries move independently, and an agent has already been added to the
|
|
194
|
+
* inspection set without a credential path (cursor), which without this flag
|
|
195
|
+
* produced a false "logged out" critical for every installed version. */
|
|
196
|
+
knownLocation: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* File-presence probe for an agent's credential, split by location: whether it
|
|
200
|
+
* exists in a SPECIFIC version home (`perVersion`) and whether it exists under
|
|
201
|
+
* the active/global HOME (`active`). A logged-out claim is only *provable* when
|
|
202
|
+
* BOTH are absent — a version that merely lacks its own copy but shares the
|
|
203
|
+
* global login is signed in, not logged out. Pure file existence; no decrypt,
|
|
204
|
+
* no network, no keychain prompt. Agents with no inspectable identity return
|
|
205
|
+
* `{ perVersion: false, active: false }` and must NEVER yield a provable-logout
|
|
206
|
+
* claim (the caller gates on {@link supportsAccountInspection}).
|
|
207
|
+
*/
|
|
208
|
+
export declare function credentialPresence(agentId: AgentId, versionHome: string): CredentialPresence;
|
|
179
209
|
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
|
180
210
|
export interface DroidAuthPayload {
|
|
181
211
|
access_token?: string;
|
package/dist/lib/agents.js
CHANGED
|
@@ -1014,6 +1014,7 @@ export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
|
1014
1014
|
'claude',
|
|
1015
1015
|
'codex',
|
|
1016
1016
|
'gemini',
|
|
1017
|
+
'cursor',
|
|
1017
1018
|
'grok',
|
|
1018
1019
|
'antigravity',
|
|
1019
1020
|
'kimi',
|
|
@@ -1077,6 +1078,60 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
1077
1078
|
}
|
|
1078
1079
|
return null;
|
|
1079
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* The on-disk credential file(s) each account-inspectable agent authenticates
|
|
1083
|
+
* from, expressed as path segments under a home. Mirrors the exact files
|
|
1084
|
+
* {@link getAccountInfo} reads, so a presence check here matches what a real
|
|
1085
|
+
* launch would find. Each entry is a list of alternatives — the FIRST that
|
|
1086
|
+
* exists counts as present (claude writes either `.claude/.claude.json` under
|
|
1087
|
+
* the shimmed config dir or a home-level `.claude.json`). Agents whose login is
|
|
1088
|
+
* stored only in the OS keychain on some platforms (antigravity, and claude's
|
|
1089
|
+
* token) still expose a credential FILE — the presence of that file is the
|
|
1090
|
+
* signal we key off; its absence on BOTH the per-version home and the active
|
|
1091
|
+
* home is what makes a logged-out claim provable.
|
|
1092
|
+
*/
|
|
1093
|
+
const CREDENTIAL_FILE_SEGMENTS = {
|
|
1094
|
+
claude: [['.claude', '.claude.json'], ['.claude.json']],
|
|
1095
|
+
codex: [['.codex', 'auth.json']],
|
|
1096
|
+
gemini: [['.gemini', 'google_accounts.json']],
|
|
1097
|
+
grok: [['.grok', 'auth.json']],
|
|
1098
|
+
kimi: [['.kimi-code', 'credentials', 'kimi-code.json']],
|
|
1099
|
+
droid: [['.factory', 'auth.v2.file']],
|
|
1100
|
+
antigravity: [['.gemini', 'antigravity-cli', 'antigravity-oauth-token']],
|
|
1101
|
+
opencode: [['.local', 'share', 'opencode', 'auth.json']],
|
|
1102
|
+
};
|
|
1103
|
+
/** Whether an agent's credential file exists under a given home. */
|
|
1104
|
+
function credentialFileExistsUnder(agentId, home) {
|
|
1105
|
+
const alternatives = CREDENTIAL_FILE_SEGMENTS[agentId];
|
|
1106
|
+
if (!alternatives)
|
|
1107
|
+
return false;
|
|
1108
|
+
for (const segments of alternatives) {
|
|
1109
|
+
const p = path.join(home, ...segments);
|
|
1110
|
+
try {
|
|
1111
|
+
if (fs.existsSync(p))
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
catch { /* unreadable */ }
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* File-presence probe for an agent's credential, split by location: whether it
|
|
1120
|
+
* exists in a SPECIFIC version home (`perVersion`) and whether it exists under
|
|
1121
|
+
* the active/global HOME (`active`). A logged-out claim is only *provable* when
|
|
1122
|
+
* BOTH are absent — a version that merely lacks its own copy but shares the
|
|
1123
|
+
* global login is signed in, not logged out. Pure file existence; no decrypt,
|
|
1124
|
+
* no network, no keychain prompt. Agents with no inspectable identity return
|
|
1125
|
+
* `{ perVersion: false, active: false }` and must NEVER yield a provable-logout
|
|
1126
|
+
* claim (the caller gates on {@link supportsAccountInspection}).
|
|
1127
|
+
*/
|
|
1128
|
+
export function credentialPresence(agentId, versionHome) {
|
|
1129
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1130
|
+
const perVersion = credentialFileExistsUnder(agentId, versionHome);
|
|
1131
|
+
const active = credentialFileExistsUnder(agentId, realHome);
|
|
1132
|
+
const knownLocation = (CREDENTIAL_FILE_SEGMENTS[agentId]?.length ?? 0) > 0;
|
|
1133
|
+
return { perVersion, active, knownLocation };
|
|
1134
|
+
}
|
|
1080
1135
|
/**
|
|
1081
1136
|
* Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
|
|
1082
1137
|
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AgentId } from './types.js';
|
|
6
6
|
export declare function shouldInstallCommandAsSkill(agent: AgentId, version: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Agents whose native command files serve a separate surface while their CLI
|
|
9
|
+
* consumes the generated skill form. Keep this registry distinct from
|
|
10
|
+
* `shouldInstallCommandAsSkill`: these targets need both writes, not a format
|
|
11
|
+
* replacement.
|
|
12
|
+
*/
|
|
13
|
+
export declare const COMMAND_SKILL_DUAL_WRITE_TARGETS: {
|
|
14
|
+
cursor: true;
|
|
15
|
+
};
|
|
16
|
+
export declare function shouldAlsoInstallCommandAsSkill(agent: AgentId, version: string): boolean;
|
|
7
17
|
export declare function commandSkillName(commandName: string): string;
|
|
8
18
|
export declare function buildCommandSkillContent(commandName: string, sourcePath: string): string;
|
|
9
19
|
export declare function skillSourceExists(skillName: string, skillSourceDirs: Array<string | null | undefined>): boolean;
|
|
@@ -52,6 +52,20 @@ function readSkillCommandMarker(skillMdPath) {
|
|
|
52
52
|
export function shouldInstallCommandAsSkill(agent, version) {
|
|
53
53
|
return !supports(agent, 'commands', version).ok && supports(agent, 'skills', version).ok;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Agents whose native command files serve a separate surface while their CLI
|
|
57
|
+
* consumes the generated skill form. Keep this registry distinct from
|
|
58
|
+
* `shouldInstallCommandAsSkill`: these targets need both writes, not a format
|
|
59
|
+
* replacement.
|
|
60
|
+
*/
|
|
61
|
+
export const COMMAND_SKILL_DUAL_WRITE_TARGETS = {
|
|
62
|
+
cursor: true,
|
|
63
|
+
};
|
|
64
|
+
export function shouldAlsoInstallCommandAsSkill(agent, version) {
|
|
65
|
+
return COMMAND_SKILL_DUAL_WRITE_TARGETS[agent] === true
|
|
66
|
+
&& supports(agent, 'commands', version).ok
|
|
67
|
+
&& supports(agent, 'skills', version).ok;
|
|
68
|
+
}
|
|
55
69
|
export function commandSkillName(commandName) {
|
|
56
70
|
return commandName;
|
|
57
71
|
}
|
package/dist/lib/commands.js
CHANGED
|
@@ -15,7 +15,7 @@ import { markdownToToml } from './convert.js';
|
|
|
15
15
|
import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir, getTrashCommandsDir } from './state.js';
|
|
16
16
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
17
17
|
import { discoverPlugins } from './plugins.js';
|
|
18
|
-
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
18
|
+
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
19
19
|
import { installGooseCommandToVersion, listGooseCommandsInVersion, gooseCommandMatches, removeGooseCommandFromVersion, } from './goose-commands.js';
|
|
20
20
|
function compareVersions(a, b) {
|
|
21
21
|
const aParts = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
@@ -211,6 +211,14 @@ export function installCommand(sourcePath, agentId, commandName, method = 'symli
|
|
|
211
211
|
const agent = AGENTS[agentId];
|
|
212
212
|
ensureCommandsDir(agentId);
|
|
213
213
|
const home = getEffectiveHome(agentId);
|
|
214
|
+
const installVersion = pinnedVersion ?? listInstalledVersions(agentId)[0] ?? '';
|
|
215
|
+
const alsoInstallAsSkill = shouldAlsoInstallCommandAsSkill(agentId, installVersion);
|
|
216
|
+
if (alsoInstallAsSkill) {
|
|
217
|
+
const installed = installCommandSkillToVersion(path.join(home, agentConfigDirName(agentId)), commandName, sourcePath, [getSkillsDir(), ...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills'))]);
|
|
218
|
+
if (!installed.success) {
|
|
219
|
+
return { path: '', method: 'copy', error: installed.error, warnings: validation.warnings };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
214
222
|
// Goose: a slash command is a recipe YAML registered in config.yaml, not a
|
|
215
223
|
// native command file under commandsSubdir.
|
|
216
224
|
if (agentId === 'goose') {
|
|
@@ -401,6 +409,11 @@ export function installCommandToVersion(agent, version, commandName, method = 'c
|
|
|
401
409
|
...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills')),
|
|
402
410
|
]);
|
|
403
411
|
}
|
|
412
|
+
if (shouldAlsoInstallCommandAsSkill(agent, version)) {
|
|
413
|
+
const installed = installCommandSkillToVersion(agentDir, commandName, sourcePath, [getSkillsDir(), ...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills'))]);
|
|
414
|
+
if (!installed.success)
|
|
415
|
+
return installed;
|
|
416
|
+
}
|
|
404
417
|
// Goose: a slash command is a recipe YAML registered in config.yaml, not a
|
|
405
418
|
// native command file. Write the recipe + slash_commands entry.
|
|
406
419
|
if (agent === 'goose') {
|
|
@@ -441,6 +454,11 @@ export function removeCommandFromVersion(agent, version, commandName) {
|
|
|
441
454
|
if (shouldInstallCommandAsSkill(agent, version)) {
|
|
442
455
|
return removeCommandSkillFromVersion(agentDir, commandName);
|
|
443
456
|
}
|
|
457
|
+
if (shouldAlsoInstallCommandAsSkill(agent, version)) {
|
|
458
|
+
const removed = removeCommandSkillFromVersion(agentDir, commandName);
|
|
459
|
+
if (!removed.success)
|
|
460
|
+
return removed;
|
|
461
|
+
}
|
|
444
462
|
if (agent === 'goose') {
|
|
445
463
|
const trashDir = path.join(getTrashCommandsDir(), agent, version, commandName);
|
|
446
464
|
return removeGooseCommandFromVersion(versionHome, commandName, trashDir);
|
package/dist/lib/daemon.js
CHANGED
|
@@ -880,7 +880,7 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
880
880
|
<key>EnvironmentVariables</key>
|
|
881
881
|
<dict>
|
|
882
882
|
<key>PATH</key>
|
|
883
|
-
<string>${
|
|
883
|
+
<string>${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin', '/opt/homebrew/bin', `${os.homedir()}/.bun/bin`])}</string>
|
|
884
884
|
</dict>
|
|
885
885
|
</dict>
|
|
886
886
|
</plist>`;
|
|
@@ -909,7 +909,7 @@ Type=simple
|
|
|
909
909
|
ExecStart=${execStart}
|
|
910
910
|
Restart=always
|
|
911
911
|
RestartSec=10
|
|
912
|
-
Environment=PATH=${
|
|
912
|
+
Environment=PATH=${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin'])}
|
|
913
913
|
|
|
914
914
|
[Install]
|
|
915
915
|
WantedBy=default.target`;
|
|
@@ -1074,6 +1074,21 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
|
1074
1074
|
function daemonNodeBinDir() {
|
|
1075
1075
|
return path.dirname(process.execPath);
|
|
1076
1076
|
}
|
|
1077
|
+
/**
|
|
1078
|
+
* The full PATH value the daemon service manifest pins, in order: the Node runtime
|
|
1079
|
+
* dir (so the shim's shebang and any child resolve the exact installing Node — see
|
|
1080
|
+
* {@link daemonNodeBinDir}), then the directory of the `agents` shim itself, then
|
|
1081
|
+
* the platform's system dirs. The shim's own dir matters because a scheduled
|
|
1082
|
+
* `command` routine shells out to the bare name `agents`
|
|
1083
|
+
* (`/bin/sh -c 'agents watchdog --nudge'`): when the shim lives outside the Node
|
|
1084
|
+
* bin dir — a `~/.local/bin` global install, a separate npm prefix — a PATH
|
|
1085
|
+
* carrying only the Node dir resolves `agents` to nothing and every routine dies
|
|
1086
|
+
* with `exit 127`. Deduped across the whole list, so a Node/shim dir that already
|
|
1087
|
+
* appears among the system dirs (e.g. a `/usr/local/bin` install) never doubles.
|
|
1088
|
+
*/
|
|
1089
|
+
function daemonPathValue(agentsBin, systemDirs) {
|
|
1090
|
+
return [...new Set([daemonNodeBinDir(), path.dirname(agentsBin), ...systemDirs])].join(':');
|
|
1091
|
+
}
|
|
1077
1092
|
/**
|
|
1078
1093
|
* Build the argv to relaunch the `agents` CLI with the given subcommand args.
|
|
1079
1094
|
*
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import type { DuplicateVersionHook } from '../hooks.js';
|
|
3
|
+
import type { RcSecretFinding } from '../secrets/rc-hygiene.js';
|
|
4
|
+
import type { SyncStatusRow, OrphanRow } from '../drift.js';
|
|
5
|
+
import type { FetchStatusMarker } from '../auto-pull.js';
|
|
6
|
+
import type { VersionResourceReport } from '../doctor-diff.js';
|
|
7
|
+
import type { FleetDivergence, FleetVersionSignIn } from './fleet-divergence.js';
|
|
8
|
+
export type FindingSeverity = 'critical' | 'warning';
|
|
9
|
+
/** A machine-stable class for a finding — drives {@link remediationFor} and lets
|
|
10
|
+
* the JSON consumer group by kind. */
|
|
11
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "exec-policy", "stale-cli"];
|
|
12
|
+
/** A machine-stable class for a finding. Derived from the runtime list above so
|
|
13
|
+
* the rubric test can enumerate every kind. */
|
|
14
|
+
export type FindingKind = typeof ALL_FINDING_KINDS[number];
|
|
15
|
+
/** One prioritized finding, attributed to a device (and, when relevant, an agent
|
|
16
|
+
* version + account). `remediation` is the exact command/hint to fix it. */
|
|
17
|
+
export interface DoctorFinding {
|
|
18
|
+
severity: FindingSeverity;
|
|
19
|
+
kind: FindingKind;
|
|
20
|
+
/** The device this finding is about. */
|
|
21
|
+
device: string;
|
|
22
|
+
/** Agent id, when the finding is about a specific agent (else undefined). */
|
|
23
|
+
agent?: AgentId;
|
|
24
|
+
/** Version id, when about a specific installed version. Absent on a finding
|
|
25
|
+
* collapsed across versions — read {@link DoctorFinding.versions} instead. */
|
|
26
|
+
version?: string;
|
|
27
|
+
/** Set only on a finding collapsed across several versions of one agent (the
|
|
28
|
+
* same problem on each). The row renders `<agent> (N versions)` and the
|
|
29
|
+
* remediation widens to the agent-wide sweep. */
|
|
30
|
+
versions?: string[];
|
|
31
|
+
/** Human account label (email/org/opaque id), when known. */
|
|
32
|
+
account?: string | null;
|
|
33
|
+
/** One-line plain-English description of the problem. */
|
|
34
|
+
message: string;
|
|
35
|
+
/** Exact remediation command / hint. */
|
|
36
|
+
remediation: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The exact remediation for a finding. Login fixes are harness-native
|
|
40
|
+
* (`loginHint`); a per-version login is offered ONLY for agents that isolate the
|
|
41
|
+
* credential per home (`agents run <agent>@<version>` then log in) — for
|
|
42
|
+
* gemini/antigravity/droid/cursor the login is shared, so we say so instead of
|
|
43
|
+
* faking a per-version fix. Every other kind maps to its canonical command.
|
|
44
|
+
*/
|
|
45
|
+
export declare function remediationFor(finding: DoctorFinding): string;
|
|
46
|
+
export interface LocalFindingInputs {
|
|
47
|
+
device: string;
|
|
48
|
+
syncRows: SyncStatusRow[];
|
|
49
|
+
orphanRows: OrphanRow[];
|
|
50
|
+
repoBehind: FetchStatusMarker[];
|
|
51
|
+
/** Per-version resource reports (one per installed version) — the source of the
|
|
52
|
+
* missing-hook / missing-plugin / missing-resource / content-drift / unwired
|
|
53
|
+
* criticals+warnings. */
|
|
54
|
+
reports: VersionResourceReport[];
|
|
55
|
+
/** Per-version sign-in per agent id. */
|
|
56
|
+
signIn: Record<string, FleetVersionSignIn[]>;
|
|
57
|
+
/** Managed agents (installed versions) whose binary won't resolve. */
|
|
58
|
+
cliMissing?: AgentId[];
|
|
59
|
+
/** Host CLIs declared in a DotAgents repo's `cli/`: their install state on this
|
|
60
|
+
* box, plus any manifest the loader could not parse. Host-global (installed to
|
|
61
|
+
* PATH, never synced into a version home), so they are a machine-level
|
|
62
|
+
* finding, not a per-version one. */
|
|
63
|
+
hostClis?: {
|
|
64
|
+
statuses: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
installed: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
errors: Array<{
|
|
69
|
+
file: string;
|
|
70
|
+
reason: string;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
/** Hooks materialized into several version homes at once — identical copies are
|
|
74
|
+
* installation noise, differing ones are drift a stale gate can act on. */
|
|
75
|
+
duplicateHooks?: DuplicateVersionHook[];
|
|
76
|
+
/** Credential-shaped exports found in the user's shell rc files (RUSH-1968). */
|
|
77
|
+
rcSecrets?: RcSecretFinding[];
|
|
78
|
+
/** The effective PowerShell execution policy and the platform it was read on.
|
|
79
|
+
* Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
|
|
80
|
+
execPolicy?: {
|
|
81
|
+
platform: NodeJS.Platform;
|
|
82
|
+
policy: string | null;
|
|
83
|
+
};
|
|
84
|
+
/** `<agent>@<version>` keys whose home is an isolated copy. Their findings are
|
|
85
|
+
* never collapsed across versions: the agent-wide `agents doctor <agent> --fix`
|
|
86
|
+
* sweep deliberately skips isolated copies, so a collapsed row would print a
|
|
87
|
+
* remediation that does not fix them. */
|
|
88
|
+
isolatedVersions?: string[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fold this machine's signals into findings. Missing hooks/plugins and unwired
|
|
92
|
+
* hooks are CRITICAL; provable logouts are CRITICAL and unprovable ones WARNING;
|
|
93
|
+
* everything else (other missing kinds, drift, stale/never-synced, repo-behind,
|
|
94
|
+
* orphans) is a WARNING. Pure.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildLocalFindings(input: LocalFindingInputs): DoctorFinding[];
|
|
97
|
+
/**
|
|
98
|
+
* Fold findings that say the SAME thing about several versions of one agent into
|
|
99
|
+
* a single row carrying `versions`, and widen its remediation to the agent-wide
|
|
100
|
+
* sweep (`agents doctor claude --fix` heals every non-isolated version in one
|
|
101
|
+
* go). Five identical `plugin 'code' — mirror missing` rows, one per installed
|
|
102
|
+
* claude, is the same fact five times.
|
|
103
|
+
*
|
|
104
|
+
* Three things never merge, because for each of them the widened remediation
|
|
105
|
+
* would be wrong:
|
|
106
|
+
* - **Isolated copies** — the agent-wide sweep deliberately skips them
|
|
107
|
+
* (`runFix`), so a folded row would print a command that leaves one broken.
|
|
108
|
+
* - **Findings with no agent** (repo-behind, rc-secret-export, …) — their
|
|
109
|
+
* `version` field is an alias, not a version.
|
|
110
|
+
* - **Logouts** ({@link NEVER_COLLAPSED}) — a login is inherently per-version:
|
|
111
|
+
* the fix is `agents run <agent>@<version> -- login`, and there is no `@all`
|
|
112
|
+
* equivalent. Dropping `version` would fall back to the bare native hint,
|
|
113
|
+
* which the shim resolves to the DEFAULT version — logging into the wrong one
|
|
114
|
+
* and leaving the finding to reappear.
|
|
115
|
+
*
|
|
116
|
+
* Pure; input order is kept.
|
|
117
|
+
*/
|
|
118
|
+
export declare function collapseAcrossVersions(findings: DoctorFinding[], isolated: Set<string>): DoctorFinding[];
|
|
119
|
+
/**
|
|
120
|
+
* Map a device's per-version sign-in into logout findings: a PROVABLE logout is
|
|
121
|
+
* CRITICAL, an unprovable one is a hedged WARNING ("could not verify sign-in"),
|
|
122
|
+
* and a signed-in version yields nothing.
|
|
123
|
+
*
|
|
124
|
+
* An agent with no inspectable identity never appears at all — not even as the
|
|
125
|
+
* hedged warning: agents-cli knows no credential file for it, so "logged out" is
|
|
126
|
+
* unknowable and silence beats a false claim. Membership is
|
|
127
|
+
* `supportsAccountInspection` (`lib/agents.ts`) and is deliberately NOT listed
|
|
128
|
+
* here — agents move between the sets, and a copy of the list in prose becomes a
|
|
129
|
+
* lie the next time one does. Note the caller also requires
|
|
130
|
+
* `CredentialPresence.knownLocation`: the inspection set and the credential-path
|
|
131
|
+
* map move independently, so being inspectable is not on its own enough to call a
|
|
132
|
+
* logout provable. Pure.
|
|
133
|
+
*/
|
|
134
|
+
export declare function signInToFindings(device: string, signIn: Record<string, FleetVersionSignIn[]>): DoctorFinding[];
|
|
135
|
+
/**
|
|
136
|
+
* Map cross-device divergence (from {@link compareFleetInventories}) into
|
|
137
|
+
* warnings: an agent version present elsewhere but absent on a device is a
|
|
138
|
+
* version-skew warning; a diverged config repo is a repo-drift warning; a
|
|
139
|
+
* missing resource is a missing-resource warning. Baseline = the local machine.
|
|
140
|
+
* Only the *lagging* box is attributed (a `*-missing-local` finding is the
|
|
141
|
+
* baseline's gap). Pure.
|
|
142
|
+
*/
|
|
143
|
+
export declare function fleetDivergenceToFindings(divergences: FleetDivergence[], baseline: string): DoctorFinding[];
|
|
144
|
+
export interface RenderOptions {
|
|
145
|
+
/** Fleet mode (`--devices`): render the `─── by computer ───` header + one
|
|
146
|
+
* block per device. Single-machine mode collapses to one `▸ <machine>` block
|
|
147
|
+
* with no fleet header. */
|
|
148
|
+
fleet: boolean;
|
|
149
|
+
/** The baseline (local) machine name — tagged `· this machine`. */
|
|
150
|
+
baseline?: string;
|
|
151
|
+
/** Header line context: device count (fleet) or the local version string. */
|
|
152
|
+
header?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Render the two-part hybrid layout from a flat findings list. Pure — returns the
|
|
156
|
+
* lines so the exact output is snapshot-tested. Criticals across ALL devices go
|
|
157
|
+
* to the top section, worst-first; the per-computer section lists each device's
|
|
158
|
+
* warnings + a `✗ N critical (above)` marker, worst device first.
|
|
159
|
+
*/
|
|
160
|
+
export declare function renderFindings(findings: DoctorFinding[], accounts: Record<string, Record<string, FleetVersionSignIn[]>>, opts: RenderOptions): string[];
|
|
161
|
+
/**
|
|
162
|
+
* The compact accounts/versions line for one device: every installed version and
|
|
163
|
+
* its account, grouped by agent — green ✓ signed in, red ✗ provably logged out,
|
|
164
|
+
* gray ? unknown (see {@link badge}). e.g.
|
|
165
|
+
* `claude 2.1.170 ✓muqsit@gmail(Max) 2.1.999 ✓team(Team) · codex ✗ · grok ✓`
|
|
166
|
+
*/
|
|
167
|
+
export declare function renderAccountsLine(signIn: Record<string, FleetVersionSignIn[]>): string;
|