@phnx-labs/agents-cli 1.22.29 → 1.22.30
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 +82 -0
- package/README.md +39 -1
- 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/routines.js +29 -11
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +30 -15
- package/dist/commands/sessions-browser.js +6 -6
- package/dist/commands/sessions-favorite.d.ts +7 -7
- package/dist/commands/sessions-favorite.js +30 -30
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions.d.ts +12 -1
- package/dist/commands/sessions.js +259 -20
- 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 +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 -0
- package/dist/lib/secrets/agent.js +32 -2
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- 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/favorites.d.ts +2 -2
- package/dist/lib/session/favorites.js +2 -2
- 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
|
@@ -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[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-subsystem health record for the always-on daemon.
|
|
3
|
+
*
|
|
4
|
+
* Today a subsystem failure inside `runDaemon()` (daemon.ts) is a single
|
|
5
|
+
* `log('ERROR', ...)` line that scrolls out of the log file and is never
|
|
6
|
+
* surfaced anywhere else — `agents daemon status` has no way to answer "is the
|
|
7
|
+
* secrets broker actually healthy right now?" beyond "the daemon process is
|
|
8
|
+
* alive". This module gives every subsystem a small persisted record —
|
|
9
|
+
* {@link SubsystemHealth} — so `agents daemon status` / `agents daemon
|
|
10
|
+
* services` can report health, not just liveness (RUSH-2354).
|
|
11
|
+
*
|
|
12
|
+
* Scheduled routines get this for free once migrated onto `agents routines`
|
|
13
|
+
* (their run history already carries success/failure — `agents routines
|
|
14
|
+
* stats`). This module exists for the two subsystems that predate routines
|
|
15
|
+
* and have no run history of their own: the secrets broker and the browser
|
|
16
|
+
* IPC server.
|
|
17
|
+
*
|
|
18
|
+
* File-backed (one JSON object keyed by subsystem name) rather than in-memory
|
|
19
|
+
* because `agents daemon status` runs as a SEPARATE process from the daemon —
|
|
20
|
+
* it must read what the daemon last recorded, not maintain its own state.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { getDaemonDir } from './state.js';
|
|
25
|
+
const HEALTH_FILE = 'health.json';
|
|
26
|
+
/** Stable subsystem identifiers shared by the daemon (writer) and `agents daemon` (reader). */
|
|
27
|
+
export const SUBSYSTEM_SECRETS_BROKER = 'secrets-broker';
|
|
28
|
+
export const SUBSYSTEM_BROWSER_IPC = 'browser-ipc';
|
|
29
|
+
function getHealthPath() {
|
|
30
|
+
return path.join(getDaemonDir(), HEALTH_FILE);
|
|
31
|
+
}
|
|
32
|
+
function readAll() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(getHealthPath(), 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function writeAll(records) {
|
|
46
|
+
const healthPath = getHealthPath();
|
|
47
|
+
fs.mkdirSync(path.dirname(healthPath), { recursive: true });
|
|
48
|
+
fs.writeFileSync(healthPath, JSON.stringify(records), 'utf-8');
|
|
49
|
+
try {
|
|
50
|
+
fs.chmodSync(healthPath, 0o600);
|
|
51
|
+
}
|
|
52
|
+
catch { /* best effort */ }
|
|
53
|
+
}
|
|
54
|
+
function blankRecord(subsystem) {
|
|
55
|
+
return { subsystem, lastError: null, lastErrorAt: null, consecutiveFailures: 0, lastOkAt: null };
|
|
56
|
+
}
|
|
57
|
+
/** Record a successful subsystem check-in — clears the failure streak. */
|
|
58
|
+
export function recordSubsystemOk(subsystem, at = new Date().toISOString()) {
|
|
59
|
+
const all = readAll();
|
|
60
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
61
|
+
all[subsystem] = { ...existing, subsystem, consecutiveFailures: 0, lastOkAt: at };
|
|
62
|
+
writeAll(all);
|
|
63
|
+
}
|
|
64
|
+
/** Record a subsystem failure — bumps the consecutive-failure streak. */
|
|
65
|
+
export function recordSubsystemError(subsystem, error, at = new Date().toISOString()) {
|
|
66
|
+
const all = readAll();
|
|
67
|
+
const existing = all[subsystem] ?? blankRecord(subsystem);
|
|
68
|
+
all[subsystem] = {
|
|
69
|
+
...existing,
|
|
70
|
+
subsystem,
|
|
71
|
+
lastError: error,
|
|
72
|
+
lastErrorAt: at,
|
|
73
|
+
consecutiveFailures: existing.consecutiveFailures + 1,
|
|
74
|
+
};
|
|
75
|
+
writeAll(all);
|
|
76
|
+
}
|
|
77
|
+
/** Read one subsystem's health record, or null if it has never reported in. */
|
|
78
|
+
export function readSubsystemHealth(subsystem) {
|
|
79
|
+
return readAll()[subsystem] ?? null;
|
|
80
|
+
}
|
|
81
|
+
/** Read every subsystem's health record, sorted by subsystem name. */
|
|
82
|
+
export function readAllSubsystemHealth() {
|
|
83
|
+
return Object.values(readAll()).sort((a, b) => a.subsystem.localeCompare(b.subsystem));
|
|
84
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon housekeeping ticks — one-shot bodies invoked by system routines.
|
|
3
|
+
*
|
|
4
|
+
* RUSH-2353: these were ~15 hardcoded `setInterval` timers inside
|
|
5
|
+
* `runDaemon()` (daemon.ts) — a parallel, inferior reimplementation of the
|
|
6
|
+
* routines system (no declaration, no run history, no pause/disable, no
|
|
7
|
+
* device pin). Each function here is one migrated tick's *body*, unchanged in
|
|
8
|
+
* behavior, now invoked as a detached one-shot process by a system routine's
|
|
9
|
+
* `command:` (via the `agents __daemon-tick <name>` entrypoint in index.ts)
|
|
10
|
+
* instead of an in-process interval closure.
|
|
11
|
+
*
|
|
12
|
+
* Overlap protection that used to live in daemon.ts as a per-tick boolean
|
|
13
|
+
* flag (`watchdogInFlight`, `probingDevices`, ...) is now provided by the
|
|
14
|
+
* routine runner's launch claim (`withRoutineLaunchClaim` in runner.ts) — two
|
|
15
|
+
* fires of the same routine name never run concurrently.
|
|
16
|
+
*
|
|
17
|
+
* Output goes to `console.log`/`console.error`: each tick runs as a spawned
|
|
18
|
+
* child of `executeCommandJobDetached`, which redirects the child's stdout to
|
|
19
|
+
* the run's `stdout.log` — so this doubles as the run's log, readable via
|
|
20
|
+
* `agents routines runs <name>`.
|
|
21
|
+
*/
|
|
22
|
+
/** ~every 3 min. Mirrors the old WATCHDOG_TICK_MS. */
|
|
23
|
+
export declare function runWatchdogTick(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Device probe: refresh registered devices' reachability and detect newly
|
|
26
|
+
* appeared tailnet nodes, dropping a sentinel per pending device so the
|
|
27
|
+
* menu-bar helper can surface "NEW DEVICES -> Register / Ignore". Refresh
|
|
28
|
+
* mode never auto-registers a newcomer. A machine without tailscale is a
|
|
29
|
+
* clean no-op. ~every 3 min.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runDeviceProbeTick(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* tmux hook reconcile: retrofit the guarded `pane-died` hook onto managed
|
|
34
|
+
* `agents run` sessions a pre-fix binary left with the old unconditional
|
|
35
|
+
* hook. Non-destructive: set-hook only, never a kill or detach. ~every 5 min.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runTmuxReconcileTick(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Launch-health self-heal: probe that each agent's DEFAULT version actually
|
|
40
|
+
* LAUNCHES, and repair a gutted install. Never repoints the global default
|
|
41
|
+
* (allowDefaultSwitch: false) — a background default switch would be a
|
|
42
|
+
* silent logout. ~every 6h.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runLaunchHealthTick(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
47
|
+
* status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
|
|
48
|
+
* ~every 3 min.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runFleetCacheWarmTick(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Session-status cache warm (RUSH-2062): publish THIS host's local active
|
|
53
|
+
* sessions so menubar / Factory / watchdog / CLI share one warm snapshot.
|
|
54
|
+
* Publish-own only (no cross-host SSH). ~every 3 min.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runSessionCacheWarmTick(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Usage refresh: keep the usage cache the `agents run` router reads
|
|
59
|
+
* (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
60
|
+
* This host is the sole writer for its own local accounts. ~every 60s
|
|
61
|
+
* (USAGE_REFRESH_TICK_MS in usage-refresh.ts — keep in sync).
|
|
62
|
+
*/
|
|
63
|
+
export declare function runUsageRefreshTick(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Auto-dispatch: for any managed project that has opted in (autoDispatch:true
|
|
66
|
+
* + maxAgents>0 in ~/.agents/factory/projects.json), pick up Linear tickets
|
|
67
|
+
* delegated to an agent and still in Todo, and dispatch each through
|
|
68
|
+
* agents-cli's own cloud-provider layer. OFF unless a project opts in; no
|
|
69
|
+
* opted-in project or no LINEAR_API_KEY is a clean no-op. ~every 3 min.
|
|
70
|
+
*
|
|
71
|
+
* Migrated to a routine (RUSH-2353) so it inherits the `devices:` allowlist —
|
|
72
|
+
* pin with `agents routines devices auto-dispatch --set <one>` to fix the
|
|
73
|
+
* shared-input double-fire problem this job had hardcoded (every daemon on
|
|
74
|
+
* the fleet polled the same Linear queue with no coordination).
|
|
75
|
+
*/
|
|
76
|
+
export declare function runAutoDispatchTick(): Promise<void>;
|
|
77
|
+
/** Registry: routine-facing name -> tick body. Keys match the shipped routine YAML names. */
|
|
78
|
+
export declare const DAEMON_TICKS: Record<string, () => Promise<void>>;
|
|
79
|
+
export declare const DAEMON_TICK_ROUTINE_NAMES: readonly string[];
|
|
80
|
+
/** Run one named tick, or throw for an unknown name (fails the routine run loud). */
|
|
81
|
+
export declare function runDaemonTick(name: string): Promise<void>;
|