@phnx-labs/agents-cli 1.22.29 → 1.22.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +44 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +86 -105
- package/dist/commands/sessions-bookmark.d.ts +20 -0
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +42 -42
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +21 -8
- package/dist/commands/sessions.js +328 -74
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +13 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -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/agent.d.ts +19 -1
- package/dist/lib/secrets/agent.js +32 -6
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +15 -15
- package/dist/lib/session/{favorites.js → bookmarks.js} +23 -23
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
- package/dist/commands/sessions-favorite.d.ts +0 -20
package/dist/commands/view.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ import { type ProfileSummary } from '../lib/profiles.js';
|
|
|
15
15
|
import { type ByokUsageResult } from '../lib/byok-usage.js';
|
|
16
16
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
17
17
|
export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
18
|
+
export interface AccountOrderedVersion {
|
|
19
|
+
version: string;
|
|
20
|
+
email: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Human `agents view` row order: selected default first, then email-bearing
|
|
24
|
+
* accounts alphabetically, then installs whose account has no email. Version
|
|
25
|
+
* descending is the deterministic tie-breaker and preserves the old order for
|
|
26
|
+
* every non-email harness.
|
|
27
|
+
*/
|
|
28
|
+
export declare function compareAccountOrderedVersions(a: AccountOrderedVersion, b: AccountOrderedVersion, globalDefault: string | null): number;
|
|
18
29
|
/**
|
|
19
30
|
* Join fixed view columns with a consistent two-space gutter. Empty trailing
|
|
20
31
|
* columns are dropped so a row without an auth chip does not grow a dangling
|
package/dist/commands/view.js
CHANGED
|
@@ -33,6 +33,30 @@ import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js
|
|
|
33
33
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
34
34
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
35
35
|
export const accountColumnLabel = accountDisplayLabel;
|
|
36
|
+
/**
|
|
37
|
+
* Human `agents view` row order: selected default first, then email-bearing
|
|
38
|
+
* accounts alphabetically, then installs whose account has no email. Version
|
|
39
|
+
* descending is the deterministic tie-breaker and preserves the old order for
|
|
40
|
+
* every non-email harness.
|
|
41
|
+
*/
|
|
42
|
+
export function compareAccountOrderedVersions(a, b, globalDefault) {
|
|
43
|
+
const aIsDefault = a.version === globalDefault;
|
|
44
|
+
const bIsDefault = b.version === globalDefault;
|
|
45
|
+
if (aIsDefault !== bIsDefault)
|
|
46
|
+
return aIsDefault ? -1 : 1;
|
|
47
|
+
const aEmail = a.email?.toLowerCase() ?? null;
|
|
48
|
+
const bEmail = b.email?.toLowerCase() ?? null;
|
|
49
|
+
if (aEmail !== null && bEmail === null)
|
|
50
|
+
return -1;
|
|
51
|
+
if (aEmail === null && bEmail !== null)
|
|
52
|
+
return 1;
|
|
53
|
+
if (aEmail !== null && bEmail !== null) {
|
|
54
|
+
const emailOrder = aEmail.localeCompare(bEmail);
|
|
55
|
+
if (emailOrder !== 0)
|
|
56
|
+
return emailOrder;
|
|
57
|
+
}
|
|
58
|
+
return compareVersions(b.version, a.version);
|
|
59
|
+
}
|
|
36
60
|
/**
|
|
37
61
|
* Overview (`agents view` with no agent filter) caps compact usage windows so
|
|
38
62
|
* multi-meter agents (Antigravity's four model quotas, Droid's three buckets)
|
|
@@ -56,15 +80,12 @@ export function joinViewColumns(cols) {
|
|
|
56
80
|
return cols.slice(0, end).join(' ');
|
|
57
81
|
}
|
|
58
82
|
/**
|
|
59
|
-
* Custom harnesses (the `~/.agents/profiles/*.yml` bundles), sorted by name
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* malformed file.
|
|
83
|
+
* Custom harnesses (the `~/.agents/profiles/*.yml` bundles), sorted by name.
|
|
84
|
+
* YAMLs that fail validation are silently skipped by `listProfiles`, so this
|
|
85
|
+
* never throws on a malformed file.
|
|
63
86
|
*/
|
|
64
|
-
function getHarnesses(
|
|
65
|
-
return listProfiles()
|
|
66
|
-
.filter((profile) => !filterAgentId || profile.host.agent === filterAgentId)
|
|
67
|
-
.map(profileSummary);
|
|
87
|
+
function getHarnesses() {
|
|
88
|
+
return listProfiles().map(profileSummary);
|
|
68
89
|
}
|
|
69
90
|
/** Version-first label: "<version> (forked from <host>[, tracks default])" */
|
|
70
91
|
function harnessVersionLabel(harness, globalDefault) {
|
|
@@ -307,7 +328,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
307
328
|
.filter((agentId) => !hasNonIsolatedVersion(agentId))
|
|
308
329
|
.map(async (agentId) => [agentId, await getUnmanagedCliState(agentId)])));
|
|
309
330
|
const showPaths = !!filterAgentId;
|
|
310
|
-
|
|
331
|
+
// A filtered native view is about that native harness's installed versions.
|
|
332
|
+
// Custom forks are standalone agent types and only belong in the overview.
|
|
333
|
+
const harnesses = filterAgentId ? [] : getHarnesses();
|
|
311
334
|
// Auto-heal stale versioned aliases. Pre-v2 aliases (e.g. pre-CLAUDE_CONFIG_DIR
|
|
312
335
|
// claude shims) silently route login through the default version's symlinked
|
|
313
336
|
// home, so `agents view` would never reflect the right account. Regenerate on
|
|
@@ -524,14 +547,16 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
524
547
|
? chalk.yellow(' (no default)')
|
|
525
548
|
: '';
|
|
526
549
|
console.log(` ${chalk.bold(agentLabel(agentId))}${strategyLabel}${noDefaultLabel}`);
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
})
|
|
550
|
+
// Account information is already loaded above. Keep the selected default
|
|
551
|
+
// first, then make multi-account installs scannable by email. Harnesses
|
|
552
|
+
// without email identities retain their prior version-descending order.
|
|
553
|
+
const sortedVersions = versions
|
|
554
|
+
.map((version) => ({
|
|
555
|
+
version,
|
|
556
|
+
email: infoMap.get(`${agentId}:${version}`)?.email ?? null,
|
|
557
|
+
}))
|
|
558
|
+
.sort((a, b) => compareAccountOrderedVersions(a, b, globalDefault))
|
|
559
|
+
.map(({ version }) => version);
|
|
535
560
|
for (const version of sortedVersions) {
|
|
536
561
|
const isDefault = version === globalDefault;
|
|
537
562
|
const isolated = !isDefault && isVersionIsolated(agentId, version);
|
|
@@ -1273,7 +1298,9 @@ export async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1273
1298
|
else
|
|
1274
1299
|
byAgent.set(agentId, [entry]);
|
|
1275
1300
|
}
|
|
1276
|
-
|
|
1301
|
+
// Keep filtered native JSON consistent with the text view: custom forks are
|
|
1302
|
+
// not children of the native harness they execute through.
|
|
1303
|
+
const harnesses = filterAgentId ? [] : getHarnesses();
|
|
1277
1304
|
const out = [];
|
|
1278
1305
|
for (const agentId of agentsToShow) {
|
|
1279
1306
|
const versions = byAgent.get(agentId) ?? [];
|
|
@@ -1544,19 +1571,19 @@ export async function viewAction(agentArg, options) {
|
|
|
1544
1571
|
// Parse agent@version syntax
|
|
1545
1572
|
const parts = agentArg.split('@');
|
|
1546
1573
|
const agentName = parts[0];
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
if (
|
|
1552
|
-
|
|
1553
|
-
if (json) {
|
|
1554
|
-
console.log(JSON.stringify(harness, null, 2));
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
renderHarnessDetail(agentName);
|
|
1574
|
+
// Match run resolution: an exact custom harness name wins over a native id or
|
|
1575
|
+
// alias with the same spelling, so every fork remains independently viewable.
|
|
1576
|
+
if (profileExists(agentName)) {
|
|
1577
|
+
const harness = profileSummary(readProfile(agentName));
|
|
1578
|
+
if (json) {
|
|
1579
|
+
console.log(JSON.stringify(harness, null, 2));
|
|
1558
1580
|
return;
|
|
1559
1581
|
}
|
|
1582
|
+
renderHarnessDetail(agentName);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const agentId = resolveAgentName(agentName);
|
|
1586
|
+
if (!agentId) {
|
|
1560
1587
|
if (json) {
|
|
1561
1588
|
console.log(JSON.stringify({ error: formatAgentError(agentName) }));
|
|
1562
1589
|
process.exit(1);
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
|
|
|
94
94
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
95
95
|
// first byte of output), the registry maps a command name to a thunk that
|
|
96
96
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
97
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
97
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadDaemon, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadAccounts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
@@ -120,6 +120,22 @@ if (process.argv[2] === '__daemon-run') {
|
|
|
120
120
|
await runDaemon();
|
|
121
121
|
process.exit(process.exitCode ?? 0);
|
|
122
122
|
}
|
|
123
|
+
// One-shot invocation of a migrated daemon housekeeping tick (RUSH-2353). The
|
|
124
|
+
// shipped system routines (`watchdog`, `device-probe`, `fleet-cache-warm`, ...)
|
|
125
|
+
// run this as their `command:` instead of the daemon holding a setInterval —
|
|
126
|
+
// same tick body, now scheduled/tracked/pinnable through the routines system.
|
|
127
|
+
if (process.argv[2] === '__daemon-tick') {
|
|
128
|
+
const name = process.argv[3] || '';
|
|
129
|
+
const { runDaemonTick } = await import('./lib/daemon-ticks.js');
|
|
130
|
+
try {
|
|
131
|
+
await runDaemonTick(name);
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
process.stderr.write(`[agents] daemon tick '${name}' failed: ${err.message}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
123
139
|
// White-label: the shim for a brand (e.g. `jack`) exports AGENTS_BRAND, so the
|
|
124
140
|
// CLI presents its own name/help/errors as the brand. Unbranded (AGENTS_BRAND
|
|
125
141
|
// unset) resolves to 'agents' and everything below is byte-identical to before.
|
|
@@ -303,6 +319,7 @@ Run and dispatch:
|
|
|
303
319
|
defaults Configure run defaults by agent/version selector
|
|
304
320
|
teams Coordinate multiple agents on shared work
|
|
305
321
|
routines Run agents on a cron schedule (scheduler auto-starts)
|
|
322
|
+
daemon Runtime status/control for the always-on daemon (secrets broker, browser IPC, scheduler)
|
|
306
323
|
webhook Receive signed GitHub/Linear webhooks for trigger routines
|
|
307
324
|
funnel Expose a webhook receiver through Tailscale Funnel
|
|
308
325
|
sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
|
|
@@ -929,6 +946,7 @@ async function registerAllEagerCommands() {
|
|
|
929
946
|
await reg(loadExport);
|
|
930
947
|
await reg(loadPackages);
|
|
931
948
|
await reg(loadRoutines);
|
|
949
|
+
await reg(loadDaemon);
|
|
932
950
|
await reg(loadMonitors);
|
|
933
951
|
await reg(loadProjects);
|
|
934
952
|
await reg(loadRun);
|
|
@@ -980,6 +998,7 @@ async function registerAllEagerCommands() {
|
|
|
980
998
|
await reg(loadWebhook);
|
|
981
999
|
await reg(loadFunnel);
|
|
982
1000
|
await reg(loadHumans);
|
|
1001
|
+
await reg(loadAccounts);
|
|
983
1002
|
registerHqTombstoneCommand(program);
|
|
984
1003
|
await reg(loadFeed);
|
|
985
1004
|
await reg(loadMailboxes);
|
|
@@ -1060,6 +1079,22 @@ program.on('command:*', (operands) => {
|
|
|
1060
1079
|
// and the doc flags (--version/--help/-h) drive both the registration strategy
|
|
1061
1080
|
// and whether the update check + background sync run at all.
|
|
1062
1081
|
const passedArgs = process.argv.slice(2);
|
|
1082
|
+
// Commander owns `--version` on the root command and otherwise intercepts it
|
|
1083
|
+
// even after `sessions`, before the subcommand can parse its version filter.
|
|
1084
|
+
// Rewrite only that value-taking nested form; bare `agents --version` and every
|
|
1085
|
+
// other command retain the root documentation flag unchanged.
|
|
1086
|
+
if (passedArgs[0] === 'sessions') {
|
|
1087
|
+
const nestedVersionIndex = passedArgs.indexOf('--version', 1);
|
|
1088
|
+
if (nestedVersionIndex >= 0) {
|
|
1089
|
+
const nestedVersion = passedArgs[nestedVersionIndex + 1];
|
|
1090
|
+
if (!nestedVersion || nestedVersion.startsWith('-')) {
|
|
1091
|
+
console.error("error: option '--version <version>' argument missing");
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
passedArgs[nestedVersionIndex] = '--session-version';
|
|
1095
|
+
process.argv[nestedVersionIndex + 2] = '--session-version';
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1063
1098
|
const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
|
|
1064
1099
|
const verboseStartup = passedArgs.includes('--verbose');
|
|
1065
1100
|
// Help and version output are pure documentation — they must never gate on
|
|
@@ -1212,7 +1247,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
1212
1247
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
1213
1248
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
1214
1249
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
1215
|
-
const sentinelValue = '
|
|
1250
|
+
const sentinelValue = 'v18';
|
|
1216
1251
|
let needRun = true;
|
|
1217
1252
|
try {
|
|
1218
1253
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentId } from './types.js';
|
|
2
|
+
export interface AccountLabel {
|
|
3
|
+
agent: AgentId;
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AccountLabelsDocument {
|
|
7
|
+
labels: Record<string, AccountLabel>;
|
|
8
|
+
}
|
|
9
|
+
export interface DiscoveredAccount {
|
|
10
|
+
agent: AgentId;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
display: string;
|
|
13
|
+
versions: string[];
|
|
14
|
+
label: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function identityFingerprint(agent: string, accountKey: string): string;
|
|
17
|
+
export declare function accountLabelsPath(base?: string): string;
|
|
18
|
+
export declare function readAccountLabels(base?: string): AccountLabelsDocument;
|
|
19
|
+
export declare function nameAccount(label: string, agent: AgentId, fingerprint: string, base?: string): void;
|
|
20
|
+
export declare function renameAccountLabel(oldLabel: string, newLabel: string, base?: string): void;
|
|
21
|
+
export declare function removeAccountLabel(label: string, base?: string): void;
|
|
22
|
+
export declare function labelForFingerprint(agent: AgentId, fingerprint: string, doc?: AccountLabelsDocument): string | null;
|
|
23
|
+
export declare function discoverAccounts(agentIds?: readonly AgentId[]): Promise<DiscoveredAccount[]>;
|
|
24
|
+
export declare function resolveAccountLabel(agent: AgentId, label: string): Promise<string>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as yaml from 'yaml';
|
|
5
|
+
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
6
|
+
import { getUserAgentsDir } from './state.js';
|
|
7
|
+
import { ACCOUNT_INSPECTION_AGENT_IDS } from './agents.js';
|
|
8
|
+
import { collectRunCandidates, pickBalancedCandidate } from './rotate.js';
|
|
9
|
+
function emptyDocument() { return { labels: {} }; }
|
|
10
|
+
export function identityFingerprint(agent, accountKey) { return crypto.createHash('sha256').update(`${agent}\0${accountKey}`).digest('hex'); }
|
|
11
|
+
export function accountLabelsPath(base = getUserAgentsDir()) { return path.join(base, 'accounts.yaml'); }
|
|
12
|
+
export function readAccountLabels(base = getUserAgentsDir()) {
|
|
13
|
+
const file = accountLabelsPath(base);
|
|
14
|
+
if (!fs.existsSync(file))
|
|
15
|
+
return emptyDocument();
|
|
16
|
+
const value = yaml.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
18
|
+
throw new Error(`Account labels corrupted at ${file}: expected a YAML map.`);
|
|
19
|
+
const doc = value;
|
|
20
|
+
doc.labels ??= {};
|
|
21
|
+
return doc;
|
|
22
|
+
}
|
|
23
|
+
function writeAccountLabels(doc, base = getUserAgentsDir()) { const file = accountLabelsPath(base); fs.mkdirSync(path.dirname(file), { recursive: true }); atomicWriteFileSync(file, yaml.stringify(doc)); }
|
|
24
|
+
function assertLabel(label) { if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(label))
|
|
25
|
+
throw new Error('Label must start with a letter or number and contain only letters, numbers, dot, underscore, or dash.'); }
|
|
26
|
+
export function nameAccount(label, agent, fingerprint, base = getUserAgentsDir()) {
|
|
27
|
+
assertLabel(label);
|
|
28
|
+
const doc = readAccountLabels(base);
|
|
29
|
+
for (const [other, account] of Object.entries(doc.labels))
|
|
30
|
+
if (other !== label && account.agent === agent && account.fingerprint === fingerprint)
|
|
31
|
+
throw new Error(`This ${agent} account is already named '${other}'.`);
|
|
32
|
+
if (doc.labels[label] && (doc.labels[label].agent !== agent || doc.labels[label].fingerprint !== fingerprint))
|
|
33
|
+
throw new Error(`Account label '${label}' already names another account.`);
|
|
34
|
+
doc.labels[label] = { agent, fingerprint };
|
|
35
|
+
writeAccountLabels(doc, base);
|
|
36
|
+
}
|
|
37
|
+
export function renameAccountLabel(oldLabel, newLabel, base = getUserAgentsDir()) { assertLabel(newLabel); const doc = readAccountLabels(base); if (!doc.labels[oldLabel])
|
|
38
|
+
throw new Error(`Unknown account label '${oldLabel}'.`); if (doc.labels[newLabel])
|
|
39
|
+
throw new Error(`Account label '${newLabel}' already exists.`); doc.labels[newLabel] = doc.labels[oldLabel]; delete doc.labels[oldLabel]; writeAccountLabels(doc, base); }
|
|
40
|
+
export function removeAccountLabel(label, base = getUserAgentsDir()) { const doc = readAccountLabels(base); if (!doc.labels[label])
|
|
41
|
+
throw new Error(`Unknown account label '${label}'.`); delete doc.labels[label]; writeAccountLabels(doc, base); }
|
|
42
|
+
export function labelForFingerprint(agent, fingerprint, doc = readAccountLabels()) { return Object.entries(doc.labels).find(([, account]) => account.agent === agent && account.fingerprint === fingerprint)?.[0] ?? null; }
|
|
43
|
+
export async function discoverAccounts(agentIds = ACCOUNT_INSPECTION_AGENT_IDS) {
|
|
44
|
+
const labels = readAccountLabels();
|
|
45
|
+
const grouped = new Map();
|
|
46
|
+
await Promise.all(agentIds.map(async (agent) => {
|
|
47
|
+
for (const candidate of await collectRunCandidates(agent)) {
|
|
48
|
+
if (!candidate.signedIn || !candidate.accountKey)
|
|
49
|
+
continue;
|
|
50
|
+
const fingerprint = identityFingerprint(agent, candidate.accountKey);
|
|
51
|
+
const key = `${agent}:${fingerprint}`;
|
|
52
|
+
const existing = grouped.get(key);
|
|
53
|
+
if (existing)
|
|
54
|
+
existing.versions.push(candidate.version);
|
|
55
|
+
else
|
|
56
|
+
grouped.set(key, { agent, fingerprint, display: candidate.accountLabel || 'signed-in account', versions: [candidate.version], label: labelForFingerprint(agent, fingerprint, labels) });
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
return [...grouped.values()].sort((a, b) => a.agent.localeCompare(b.agent) || a.display.localeCompare(b.display));
|
|
60
|
+
}
|
|
61
|
+
export async function resolveAccountLabel(agent, label) {
|
|
62
|
+
const account = readAccountLabels().labels[label];
|
|
63
|
+
if (!account)
|
|
64
|
+
throw new Error(`Unknown account label '${label}'.`);
|
|
65
|
+
if (account.agent !== agent)
|
|
66
|
+
throw new Error(`Account label '${label}' names a ${account.agent} account, not ${agent}.`);
|
|
67
|
+
const candidates = (await collectRunCandidates(agent)).filter(candidate => candidate.accountKey && identityFingerprint(agent, candidate.accountKey) === account.fingerprint);
|
|
68
|
+
const result = pickBalancedCandidate(candidates);
|
|
69
|
+
if (!result)
|
|
70
|
+
throw new Error(`No healthy installed ${agent} version is currently signed into account '${label}'.`);
|
|
71
|
+
return result.picked.version;
|
|
72
|
+
}
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -23,7 +23,18 @@ export declare const GEMINI_HOOKS_MIN_VERSION = "0.26.0";
|
|
|
23
23
|
* dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
|
|
24
24
|
* section, even though the user had nothing to import.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
interface NativeBinaryResolutionOptions {
|
|
27
|
+
shimsDir?: string;
|
|
28
|
+
historyDir?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a PATH candidate to the immutable native executable agents-cli may
|
|
32
|
+
* safely register. An adopted launcher can live outside the shims directory
|
|
33
|
+
* while resolving back into it; in that case its durable adoption record is
|
|
34
|
+
* the source of truth for the original executable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveNativeBinaryPath(command: string, candidate: string, options?: NativeBinaryResolutionOptions): string | null;
|
|
37
|
+
export declare function findInPath(command: string, options?: NativeBinaryResolutionOptions): string | null;
|
|
27
38
|
/**
|
|
28
39
|
* Master registry of all supported agents keyed by AgentId.
|
|
29
40
|
*
|
|
@@ -289,6 +300,26 @@ export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
|
|
|
289
300
|
} | null;
|
|
290
301
|
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
291
302
|
export declare function __resetAntigravityKeychainCacheForTest(): void;
|
|
303
|
+
/**
|
|
304
|
+
* OpenCode's account identity: the sorted, "+"-joined list of provider ids
|
|
305
|
+
* that hold a valid credential in `auth.json` (e.g. `"anthropic+muse-spark"`).
|
|
306
|
+
* `auth.json` carries no email/identity claim (see `isValidOpenCodeCredential`),
|
|
307
|
+
* so this join is the closest thing to "which account is this" available — the
|
|
308
|
+
* same value `agents view`/`agents doctor` show for OpenCode's signed-in state.
|
|
309
|
+
*
|
|
310
|
+
* This is the ONLY correct source for an OpenCode "account". OpenCode's SQLite
|
|
311
|
+
* `opencode.db` also carries `account`/`account_state`/`control_account` tables,
|
|
312
|
+
* but on a real, actively-used install (yosemite-s1, 1.16.0, 35 applied
|
|
313
|
+
* migrations) all three are permanently empty — no migration ever populates
|
|
314
|
+
* them, and no session has ever written a row. Reading from them instead of
|
|
315
|
+
* `auth.json` always yields `undefined`, credential or not; `session/discover.ts`
|
|
316
|
+
* uses this function rather than duplicating a sqlite lookup against those
|
|
317
|
+
* dead tables.
|
|
318
|
+
*
|
|
319
|
+
* Sync (`fs.readFileSync`), no network. Returns undefined when `auth.json` is
|
|
320
|
+
* missing, unreadable, or carries no valid credential.
|
|
321
|
+
*/
|
|
322
|
+
export declare function resolveOpenCodeAccountId(base: string): string | undefined;
|
|
292
323
|
/**
|
|
293
324
|
* Whether a Claude version home's credential file is present but carries no
|
|
294
325
|
* token — the "must have a real credential" floor (see
|
package/dist/lib/agents.js
CHANGED
|
@@ -21,7 +21,7 @@ import chalk from 'chalk';
|
|
|
21
21
|
import { execFileShellSpec } from './platform/index.js';
|
|
22
22
|
import { latestFileMtimeMs } from './fs-walk.js';
|
|
23
23
|
import { damerauLevenshtein } from './fuzzy.js';
|
|
24
|
-
import { getCacheDir, getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
|
|
24
|
+
import { getCacheDir, getVersionsDir, getShimsDir, getHistoryDir, getCliVersionCachePath } from './state.js';
|
|
25
25
|
import { resolveVersion, getVersionHomePath, getBinaryPath } from './versions.js';
|
|
26
26
|
import { supports } from './capabilities.js';
|
|
27
27
|
const execFileAsync = promisify(execFile);
|
|
@@ -61,20 +61,57 @@ function saveCliVersionCache() {
|
|
|
61
61
|
/* best-effort cache persist */
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
function pathIsWithin(candidate, directory) {
|
|
65
|
+
const relative = path.relative(directory, candidate);
|
|
66
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
67
|
+
}
|
|
64
68
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* false positive where agents with NO real binary on the host (e.g. a
|
|
70
|
-
* never-installed Cursor whose only PATH entry was our `cursor-agent` shim
|
|
71
|
-
* dispatcher) showed up under `agents view`'s "Not Managed by Agents CLI"
|
|
72
|
-
* section, even though the user had nothing to import.
|
|
69
|
+
* Resolve a PATH candidate to the immutable native executable agents-cli may
|
|
70
|
+
* safely register. An adopted launcher can live outside the shims directory
|
|
71
|
+
* while resolving back into it; in that case its durable adoption record is
|
|
72
|
+
* the source of truth for the original executable.
|
|
73
73
|
*/
|
|
74
|
-
export function
|
|
74
|
+
export function resolveNativeBinaryPath(command, candidate, options = {}) {
|
|
75
|
+
const shimsDir = options.shimsDir ?? getShimsDir();
|
|
76
|
+
const historyDir = options.historyDir ?? getHistoryDir();
|
|
77
|
+
let canonicalCandidate;
|
|
78
|
+
try {
|
|
79
|
+
canonicalCandidate = fs.realpathSync(candidate);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
let canonicalShimsDir = path.resolve(shimsDir);
|
|
85
|
+
try {
|
|
86
|
+
canonicalShimsDir = fs.realpathSync(shimsDir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* An absent shims dir cannot make an existing native binary invalid. */
|
|
90
|
+
}
|
|
91
|
+
if (!pathIsWithin(canonicalCandidate, canonicalShimsDir))
|
|
92
|
+
return canonicalCandidate;
|
|
93
|
+
const recordPath = path.join(historyDir, 'adopted-launchers', command);
|
|
94
|
+
try {
|
|
95
|
+
const [original] = fs.readFileSync(recordPath, 'utf-8').split(/\r?\n/, 1);
|
|
96
|
+
if (!original)
|
|
97
|
+
return null;
|
|
98
|
+
const canonicalOriginal = fs.realpathSync(original);
|
|
99
|
+
if (pathIsWithin(canonicalOriginal, canonicalShimsDir))
|
|
100
|
+
return null;
|
|
101
|
+
const stat = fs.statSync(canonicalOriginal);
|
|
102
|
+
if (!stat.isFile())
|
|
103
|
+
return null;
|
|
104
|
+
fs.accessSync(canonicalOriginal, fs.constants.X_OK);
|
|
105
|
+
return canonicalOriginal;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function findInPath(command, options = {}) {
|
|
75
112
|
const pathEnv = process.env.PATH || '';
|
|
76
113
|
const pathExt = process.platform === 'win32' ? (process.env.PATHEXT || '').split(';') : [''];
|
|
77
|
-
const shimsDir = getShimsDir();
|
|
114
|
+
const shimsDir = options.shimsDir ?? getShimsDir();
|
|
78
115
|
for (const dir of pathEnv.split(path.delimiter)) {
|
|
79
116
|
if (!dir)
|
|
80
117
|
continue;
|
|
@@ -84,8 +121,11 @@ export function findInPath(command) {
|
|
|
84
121
|
const full = path.join(dir, command + ext);
|
|
85
122
|
try {
|
|
86
123
|
const stat = fs.statSync(full);
|
|
87
|
-
if (stat.isFile())
|
|
88
|
-
|
|
124
|
+
if (!stat.isFile())
|
|
125
|
+
continue;
|
|
126
|
+
const native = resolveNativeBinaryPath(command, full, options);
|
|
127
|
+
if (native)
|
|
128
|
+
return native;
|
|
89
129
|
}
|
|
90
130
|
catch {
|
|
91
131
|
/* not in this dir */
|
|
@@ -305,9 +345,8 @@ export const AGENTS = {
|
|
|
305
345
|
// + warns for pre-2.4 installs); the direct `subagents add --agents cursor` path
|
|
306
346
|
// writes unconditionally, same as the other since-gated agents.
|
|
307
347
|
// See transformSubagentForCursor / https://cursor.com/docs/subagents.
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], interactiveRepl: false }, // allowlist: ~/.cursor/cli-config.json
|
|
348
|
+
// Current cursor-agent builds open their interactive TUI with no argv.
|
|
349
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], interactiveRepl: true }, // allowlist: ~/.cursor/cli-config.json
|
|
311
350
|
},
|
|
312
351
|
opencode: {
|
|
313
352
|
id: 'opencode',
|
|
@@ -1602,6 +1641,43 @@ function isValidOpenCodeCredential(value) {
|
|
|
1602
1641
|
default: return false;
|
|
1603
1642
|
}
|
|
1604
1643
|
}
|
|
1644
|
+
/**
|
|
1645
|
+
* OpenCode's account identity: the sorted, "+"-joined list of provider ids
|
|
1646
|
+
* that hold a valid credential in `auth.json` (e.g. `"anthropic+muse-spark"`).
|
|
1647
|
+
* `auth.json` carries no email/identity claim (see `isValidOpenCodeCredential`),
|
|
1648
|
+
* so this join is the closest thing to "which account is this" available — the
|
|
1649
|
+
* same value `agents view`/`agents doctor` show for OpenCode's signed-in state.
|
|
1650
|
+
*
|
|
1651
|
+
* This is the ONLY correct source for an OpenCode "account". OpenCode's SQLite
|
|
1652
|
+
* `opencode.db` also carries `account`/`account_state`/`control_account` tables,
|
|
1653
|
+
* but on a real, actively-used install (yosemite-s1, 1.16.0, 35 applied
|
|
1654
|
+
* migrations) all three are permanently empty — no migration ever populates
|
|
1655
|
+
* them, and no session has ever written a row. Reading from them instead of
|
|
1656
|
+
* `auth.json` always yields `undefined`, credential or not; `session/discover.ts`
|
|
1657
|
+
* uses this function rather than duplicating a sqlite lookup against those
|
|
1658
|
+
* dead tables.
|
|
1659
|
+
*
|
|
1660
|
+
* Sync (`fs.readFileSync`), no network. Returns undefined when `auth.json` is
|
|
1661
|
+
* missing, unreadable, or carries no valid credential.
|
|
1662
|
+
*/
|
|
1663
|
+
export function resolveOpenCodeAccountId(base) {
|
|
1664
|
+
const authPath = resolveOpenCodeAuthPath(base);
|
|
1665
|
+
if (!authPath)
|
|
1666
|
+
return undefined;
|
|
1667
|
+
try {
|
|
1668
|
+
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
1669
|
+
if (!data || typeof data !== 'object')
|
|
1670
|
+
return undefined;
|
|
1671
|
+
const providers = Object.entries(data)
|
|
1672
|
+
.filter(([, cred]) => isValidOpenCodeCredential(cred))
|
|
1673
|
+
.map(([id]) => id)
|
|
1674
|
+
.sort();
|
|
1675
|
+
return providers.length ? providers.join('+') : undefined;
|
|
1676
|
+
}
|
|
1677
|
+
catch {
|
|
1678
|
+
return undefined;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1605
1681
|
/**
|
|
1606
1682
|
* Whether a Muse Code `~/.config/muse/auth.json` document holds any usable
|
|
1607
1683
|
* access token. Live shape from `muse login` (device OAuth, Muse Code 0.1.0):
|
|
@@ -2020,22 +2096,11 @@ export async function getAccountInfo(agentId, home) {
|
|
|
2020
2096
|
// themselves. The user's complaint was the row read "not signed in"
|
|
2021
2097
|
// despite a live login; a valid provider entry now shows e.g.
|
|
2022
2098
|
// "id:muse-spark" so they can see exactly which provider is configured.
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
if (!data || typeof data !== 'object')
|
|
2028
|
-
return { ...empty, lastActive };
|
|
2029
|
-
const providers = Object.entries(data)
|
|
2030
|
-
.filter(([, cred]) => isValidOpenCodeCredential(cred))
|
|
2031
|
-
.map(([id]) => id)
|
|
2032
|
-
.sort();
|
|
2033
|
-
if (providers.length === 0)
|
|
2099
|
+
// resolveOpenCodeAccountId is the single source of truth for this join —
|
|
2100
|
+
// session/discover.ts reuses it for the indexed `account` field.
|
|
2101
|
+
const accountId = resolveOpenCodeAccountId(base);
|
|
2102
|
+
if (!accountId)
|
|
2034
2103
|
return { ...empty, lastActive };
|
|
2035
|
-
// Provider ids are config keys (e.g. "anthropic", "muse-spark"), not
|
|
2036
|
-
// secrets. Join them into a stable, human-readable account label +
|
|
2037
|
-
// identity key for usage dedup.
|
|
2038
|
-
const accountId = providers.join('+');
|
|
2039
2104
|
const accountKey = buildIdentityKey(agentId, [['providers', accountId]]);
|
|
2040
2105
|
return { ...empty, signedIn: true, accountId, accountKey, lastActive };
|
|
2041
2106
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
|
|
2
|
+
export declare const SUBSYSTEM_SECRETS_BROKER = "secrets-broker";
|
|
3
|
+
export declare const SUBSYSTEM_BROWSER_IPC = "browser-ipc";
|
|
4
|
+
/** One subsystem's health as of the last time it reported in. */
|
|
5
|
+
export interface SubsystemHealth {
|
|
6
|
+
/** Stable identifier, e.g. 'secrets-broker', 'browser-ipc'. */
|
|
7
|
+
subsystem: string;
|
|
8
|
+
/** Most recent error message, or null if it has never failed. */
|
|
9
|
+
lastError: string | null;
|
|
10
|
+
/** ISO timestamp of the most recent error, or null. */
|
|
11
|
+
lastErrorAt: string | null;
|
|
12
|
+
/** Consecutive failures since the last success (0 when currently healthy). */
|
|
13
|
+
consecutiveFailures: number;
|
|
14
|
+
/** ISO timestamp of the most recent success, or null if it has never succeeded. */
|
|
15
|
+
lastOkAt: string | null;
|
|
16
|
+
}
|
|
17
|
+
/** Record a successful subsystem check-in — clears the failure streak. */
|
|
18
|
+
export declare function recordSubsystemOk(subsystem: string, at?: string): void;
|
|
19
|
+
/** Record a subsystem failure — bumps the consecutive-failure streak. */
|
|
20
|
+
export declare function recordSubsystemError(subsystem: string, error: string, at?: string): void;
|
|
21
|
+
/** Read one subsystem's health record, or null if it has never reported in. */
|
|
22
|
+
export declare function readSubsystemHealth(subsystem: string): SubsystemHealth | null;
|
|
23
|
+
/** Read every subsystem's health record, sorted by subsystem name. */
|
|
24
|
+
export declare function readAllSubsystemHealth(): SubsystemHealth[];
|