@phnx-labs/agents-cli 1.22.24 → 1.22.25
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 +167 -1
- package/README.md +14 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +100 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +24 -2
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/teams.js +3 -2
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +36 -1
- package/dist/lib/agents.js +126 -21
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/runner.js +5 -5
- 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/bundles.js +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +15 -0
- package/dist/lib/startup/command-registry.js +42 -0
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { isAlive, killTree } from './platform/index.js';
|
|
5
|
+
import { getRunsDir } from './state.js';
|
|
6
|
+
function processMatchesRun(meta) {
|
|
7
|
+
if (!meta.pid || !meta.spawnedAt)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
if (process.platform === 'win32') {
|
|
11
|
+
const startedAt = execFileSync('powershell.exe', [
|
|
12
|
+
'-NoProfile',
|
|
13
|
+
'-NonInteractive',
|
|
14
|
+
'-Command',
|
|
15
|
+
`(Get-Process -Id ${meta.pid} -ErrorAction Stop).StartTime.ToUniversalTime().ToString("o")`,
|
|
16
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
|
|
17
|
+
const processStart = Date.parse(startedAt);
|
|
18
|
+
return Number.isFinite(processStart) && Math.abs(processStart - meta.spawnedAt) < 30_000;
|
|
19
|
+
}
|
|
20
|
+
const elapsed = execFileSync('ps', ['-p', String(meta.pid), '-o', 'etime='], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
23
|
+
}).trim();
|
|
24
|
+
if (!elapsed)
|
|
25
|
+
return false;
|
|
26
|
+
const fields = elapsed.replace(/-/g, ':').split(':').reverse();
|
|
27
|
+
const seconds = Number(fields[0] ?? 0)
|
|
28
|
+
+ Number(fields[1] ?? 0) * 60
|
|
29
|
+
+ Number(fields[2] ?? 0) * 3600
|
|
30
|
+
+ Number(fields[3] ?? 0) * 86400;
|
|
31
|
+
return Math.abs((Date.now() - seconds * 1000) - meta.spawnedAt) < 30_000;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Reap process groups whose durable run record is already terminal. */
|
|
38
|
+
export function reapTerminalRoutineProcesses(opts = {}) {
|
|
39
|
+
const runsDir = opts.runsDir ?? getRunsDir();
|
|
40
|
+
const alive = opts.alive ?? isAlive;
|
|
41
|
+
const owns = opts.owns ?? processMatchesRun;
|
|
42
|
+
const terminate = opts.terminate ?? ((pid) => killTree(process.platform === 'win32' ? pid : -pid));
|
|
43
|
+
if (!fs.existsSync(runsDir))
|
|
44
|
+
return [];
|
|
45
|
+
const reaped = [];
|
|
46
|
+
for (const job of fs.readdirSync(runsDir, { withFileTypes: true })) {
|
|
47
|
+
if (!job.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
const jobDir = path.join(runsDir, job.name);
|
|
50
|
+
for (const run of fs.readdirSync(jobDir, { withFileTypes: true })) {
|
|
51
|
+
if (!run.isDirectory())
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const meta = JSON.parse(fs.readFileSync(path.join(jobDir, run.name, 'meta.json'), 'utf-8'));
|
|
55
|
+
if (!['failed', 'timeout'].includes(meta.status) || !meta.pid || meta.hostTaskId || meta.cloudTaskId)
|
|
56
|
+
continue;
|
|
57
|
+
const completedAt = Date.parse(meta.completedAt ?? '');
|
|
58
|
+
if (!Number.isFinite(completedAt) || Date.now() - completedAt < 5_000)
|
|
59
|
+
continue;
|
|
60
|
+
if (!alive(meta.pid))
|
|
61
|
+
continue;
|
|
62
|
+
if (!owns(meta))
|
|
63
|
+
continue;
|
|
64
|
+
terminate(meta.pid);
|
|
65
|
+
reaped.push(meta.pid);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Corrupt or concurrently-replaced records are left untouched.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return reaped;
|
|
73
|
+
}
|
package/dist/lib/runner.js
CHANGED
|
@@ -52,7 +52,7 @@ function activeRoutineRun(config) {
|
|
|
52
52
|
const runs = listRuns(config.name);
|
|
53
53
|
for (let i = runs.length - 1; i >= 0; i--) {
|
|
54
54
|
const run = runs[i];
|
|
55
|
-
if (
|
|
55
|
+
if (!['running', 'failed', 'timeout'].includes(run.status))
|
|
56
56
|
continue;
|
|
57
57
|
if (run.pid && isPidOurs(run.pid, run.spawnedAt))
|
|
58
58
|
return run;
|
|
@@ -235,10 +235,10 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
235
235
|
appendModelAndReasoning(cmd, config);
|
|
236
236
|
}
|
|
237
237
|
if (config.agent === 'cursor') {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
238
|
+
if (mode === 'plan') {
|
|
239
|
+
cmd.push('--plan');
|
|
240
|
+
}
|
|
241
|
+
else if (mode === 'skip') {
|
|
242
242
|
cmd.push('-f');
|
|
243
243
|
}
|
|
244
244
|
else {
|
|
Binary file
|
|
Binary file
|
|
@@ -55,19 +55,7 @@ const keychainStore = {
|
|
|
55
55
|
const fileItemStore = {
|
|
56
56
|
has: (item) => fileStore.has(item),
|
|
57
57
|
get: (item) => fileStore.get(item),
|
|
58
|
-
getBatch: (items) =>
|
|
59
|
-
const out = new Map();
|
|
60
|
-
for (const item of items) {
|
|
61
|
-
try {
|
|
62
|
-
out.set(item, fileStore.get(item));
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// Missing/undecryptable item — absent from the map, mirroring
|
|
66
|
-
// getKeychainTokens (caller decides whether that's an error).
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
},
|
|
58
|
+
getBatch: (items) => fileStore.getBatch(items),
|
|
71
59
|
set: (item, value) => fileStore.set(item, value),
|
|
72
60
|
setBatch: (items) => {
|
|
73
61
|
for (const [item, value] of items) {
|
|
@@ -1340,13 +1328,6 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1340
1328
|
: store.getBatch([...new Set([metaItem, ...secretItems])]);
|
|
1341
1329
|
const json = fetched.get(metaItem);
|
|
1342
1330
|
if (json === undefined) {
|
|
1343
|
-
// For a file-backed bundle the metadata item is on disk (that's how
|
|
1344
|
-
// bundleBackend resolved to 'file'); a missing decrypt means the wrong
|
|
1345
|
-
// passphrase, not a missing bundle. getBatch swallowed the decrypt error,
|
|
1346
|
-
// so distinguish here rather than report a misleading "not found".
|
|
1347
|
-
if (backend === 'file' && fileStore.has(metaItem)) {
|
|
1348
|
-
throw new Error(`Bundle '${name}': failed to decrypt — wrong AGENTS_SECRETS_PASSPHRASE or tampered file store.`);
|
|
1349
|
-
}
|
|
1350
1331
|
if (vaultExists() && !getVaultSession().loggedIn) {
|
|
1351
1332
|
throw new Error(`Synced secrets are locked. Run: agents login`);
|
|
1352
1333
|
}
|
|
@@ -53,6 +53,7 @@ export declare function encryptForFallback(plaintext: string, passphrase: string
|
|
|
53
53
|
export declare function decryptForFallback(enc: EncFile, passphrase: string): string;
|
|
54
54
|
declare function fileHas(item: string): boolean;
|
|
55
55
|
declare function fileGet(item: string): string;
|
|
56
|
+
declare function fileGetBatch(items: string[]): Map<string, string>;
|
|
56
57
|
declare function fileSet(item: string, value: string): void;
|
|
57
58
|
declare function fileDelete(item: string): boolean;
|
|
58
59
|
declare function fileList(prefix: string): string[];
|
|
@@ -63,6 +64,7 @@ export declare function fileStoreHasItems(): boolean;
|
|
|
63
64
|
export declare const fileStore: {
|
|
64
65
|
has: typeof fileHas;
|
|
65
66
|
get: typeof fileGet;
|
|
67
|
+
getBatch: typeof fileGetBatch;
|
|
66
68
|
set: typeof fileSet;
|
|
67
69
|
delete: typeof fileDelete;
|
|
68
70
|
list: typeof fileList;
|
|
@@ -237,6 +237,18 @@ function fileGet(item) {
|
|
|
237
237
|
throw new Error(`Failed to decrypt '${item}'. Wrong AGENTS_SECRETS_PASSPHRASE or tampered file.`);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
+
function fileGetBatch(items) {
|
|
241
|
+
const out = new Map();
|
|
242
|
+
for (const item of items) {
|
|
243
|
+
// A missing file is an absent item. Any error reading an existing file,
|
|
244
|
+
// especially an AES-GCM authentication failure, is a broken store and must
|
|
245
|
+
// stop the caller rather than silently produce an incomplete environment.
|
|
246
|
+
if (!fileHas(item))
|
|
247
|
+
continue;
|
|
248
|
+
out.set(item, fileGet(item));
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
240
252
|
function fileSet(item, value) {
|
|
241
253
|
ensureFileDir();
|
|
242
254
|
// Under the store lock: a write must not interleave with a rotation's swap.
|
|
@@ -283,6 +295,7 @@ export function fileStoreHasItems() {
|
|
|
283
295
|
export const fileStore = {
|
|
284
296
|
has: fileHas,
|
|
285
297
|
get: fileGet,
|
|
298
|
+
getBatch: fileGetBatch,
|
|
286
299
|
set: fileSet,
|
|
287
300
|
delete: fileDelete,
|
|
288
301
|
list: fileList,
|
|
@@ -32,6 +32,20 @@ export interface RcSecretFinding {
|
|
|
32
32
|
/** The file-store master passphrase gets called out separately — it is the highest-severity case. */
|
|
33
33
|
isMasterPassphrase: boolean;
|
|
34
34
|
}
|
|
35
|
+
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
36
|
+
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
37
|
+
export declare const MASTER_PASSPHRASE = "AGENTS_SECRETS_PASSPHRASE";
|
|
38
|
+
/**
|
|
39
|
+
* True when the file-store master key is live in THIS process's environment.
|
|
40
|
+
*
|
|
41
|
+
* The scanner above reads FILES, and that leaves a hole: a value inherited by a
|
|
42
|
+
* long-lived process outlives the rc line that set it, so deleting the export
|
|
43
|
+
* makes `scanRcExports` report clean while every shell, editor and agent started
|
|
44
|
+
* beforehand still carries the key and passes it to everything it spawns.
|
|
45
|
+
* Returns a boolean — never the value — so a finding built from it can be
|
|
46
|
+
* printed, logged, or shipped without leaking the secret.
|
|
47
|
+
*/
|
|
48
|
+
export declare function masterPassphraseInEnv(): boolean;
|
|
35
49
|
/** True if a variable name looks like it holds a credential value. */
|
|
36
50
|
export declare function isCredentialName(name: string): boolean;
|
|
37
51
|
/**
|
|
@@ -33,7 +33,20 @@ export const RC_FILENAMES = [
|
|
|
33
33
|
];
|
|
34
34
|
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
35
35
|
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
36
|
-
const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
|
|
36
|
+
export const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
|
|
37
|
+
/**
|
|
38
|
+
* True when the file-store master key is live in THIS process's environment.
|
|
39
|
+
*
|
|
40
|
+
* The scanner above reads FILES, and that leaves a hole: a value inherited by a
|
|
41
|
+
* long-lived process outlives the rc line that set it, so deleting the export
|
|
42
|
+
* makes `scanRcExports` report clean while every shell, editor and agent started
|
|
43
|
+
* beforehand still carries the key and passes it to everything it spawns.
|
|
44
|
+
* Returns a boolean — never the value — so a finding built from it can be
|
|
45
|
+
* printed, logged, or shipped without leaking the secret.
|
|
46
|
+
*/
|
|
47
|
+
export function masterPassphraseInEnv() {
|
|
48
|
+
return (process.env[MASTER_PASSPHRASE] ?? '').length > 0;
|
|
49
|
+
}
|
|
37
50
|
/**
|
|
38
51
|
* Last `_`-delimited segment values that mark a variable as credential-shaped.
|
|
39
52
|
* Matched against the FINAL segment (segment equality, not substring) so
|
|
@@ -126,6 +126,10 @@ export interface ActiveSession {
|
|
|
126
126
|
*/
|
|
127
127
|
lastActivityMs?: number;
|
|
128
128
|
status: ActiveStatus;
|
|
129
|
+
/** Indexed launch origin, backfilled by the sessions command for JSON consumers. */
|
|
130
|
+
origin?: 'cli' | 'routine';
|
|
131
|
+
/** Routine definition name when origin is `routine`. */
|
|
132
|
+
routineName?: string;
|
|
129
133
|
/**
|
|
130
134
|
* Foreground/background presence for the detach/attach model:
|
|
131
135
|
* `attached` — live interactive TUI you're watching;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -12,7 +12,7 @@ import Database from '../sqlite.js';
|
|
|
12
12
|
import { parseSession } from './parse.js';
|
|
13
13
|
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
14
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
15
|
-
import { query as queryEvents } from '../events.js';
|
|
15
|
+
import { query as queryEvents, queryToolUsageForSessions } from '../events.js';
|
|
16
16
|
import { machineForSessionFile } from './origin-machine.js';
|
|
17
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
18
|
import { toolCallsFromEvents } from './tool-calls.js';
|
|
@@ -1648,6 +1648,12 @@ export function upsertSessionsBatch(entries) {
|
|
|
1648
1648
|
}
|
|
1649
1649
|
});
|
|
1650
1650
|
const writtenEntries = [];
|
|
1651
|
+
// Pre-compute browser/computer usage for all sessions outside the write
|
|
1652
|
+
// transaction. detectToolUsage scans all event log files (O(files) I/O
|
|
1653
|
+
// per call) and holding the SQLite write lock during that scan is what
|
|
1654
|
+
// causes the "DB locked" errors (RUSH-2006). One pass for the whole batch
|
|
1655
|
+
// costs O(files) total instead of O(N × files) inside the lock.
|
|
1656
|
+
const toolUsageBySession = queryToolUsageForSessions(new Set(enrichedEntries.map(e => e.meta.id)));
|
|
1651
1657
|
const txn = db.transaction((items) => {
|
|
1652
1658
|
// Re-read the ledger now that we hold the write lock. Any file committed
|
|
1653
1659
|
// by a concurrent process since our pre-scan is visible here.
|
|
@@ -1678,7 +1684,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
1678
1684
|
// back when the error escapes `fn`, so catching + skipping here leaves the txn valid
|
|
1679
1685
|
// and committable. We deliberately do NOT stamp the ledger for a skipped row, so the
|
|
1680
1686
|
// next scan re-tries it (self-healing once the underlying parser is fixed).
|
|
1681
|
-
const toolUsage =
|
|
1687
|
+
const toolUsage = toolUsageBySession.get(meta.id) ?? { usedBrowser: false, usedComputer: false };
|
|
1682
1688
|
// claude/codex skip enrichCachedSessionMeta above (preserving their
|
|
1683
1689
|
// resumable-parse optimization) — write their pre-computed
|
|
1684
1690
|
// skillsUsed/slashCommandsUsed (folded incrementally by discover.ts's
|
|
@@ -81,6 +81,8 @@ export interface GatherRemoteListOptions {
|
|
|
81
81
|
* know whether the match is unique or conflicting.
|
|
82
82
|
*/
|
|
83
83
|
isDefinitive?: (session: SessionMeta, machine: string) => boolean;
|
|
84
|
+
/** Per-peer deadline for slower indexed browse queries. */
|
|
85
|
+
timeoutMs?: number;
|
|
84
86
|
}
|
|
85
87
|
export declare function gatherRemoteList(forwardedArgs: string[], hosts?: string[], opts?: GatherRemoteListOptions): Promise<RemoteListResult>;
|
|
86
88
|
export interface RemoteToolSearchResult {
|
|
@@ -197,6 +197,7 @@ export async function gatherRemoteList(forwardedArgs, hosts, opts) {
|
|
|
197
197
|
args: forwardedArgs,
|
|
198
198
|
noFanoutEnv: NO_FANOUT_ENV,
|
|
199
199
|
hosts,
|
|
200
|
+
timeoutMs: opts?.timeoutMs,
|
|
200
201
|
earlyExit: opts?.isDefinitive ? { isDefinitive: opts.isDefinitive } : undefined,
|
|
201
202
|
parse: (stdout, machine) => parseRemoteListPayload(stdout, machine, safeResolver),
|
|
202
203
|
});
|
|
@@ -4,11 +4,11 @@ import type { ActiveSession } from './active.js';
|
|
|
4
4
|
* Short on purpose: live status (running/idle/waiting) must not go stale.
|
|
5
5
|
* The daemon warm tick uses the same cadence (see {@link SESSION_CACHE_WARM_INTERVAL_MS}).
|
|
6
6
|
*/
|
|
7
|
-
export declare const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS
|
|
7
|
+
export declare const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS: number;
|
|
8
8
|
/** Daemon warm interval — keep in sync with the setInterval in `lib/daemon.ts`. */
|
|
9
|
-
export declare const SESSION_CACHE_WARM_INTERVAL_MS
|
|
10
|
-
/** Kick off the first warm
|
|
11
|
-
export declare const SESSION_CACHE_WARM_KICKOFF_MS =
|
|
9
|
+
export declare const SESSION_CACHE_WARM_INTERVAL_MS: number;
|
|
10
|
+
/** Kick off the first warm 30s after daemon start (staggered off other ticks). */
|
|
11
|
+
export declare const SESSION_CACHE_WARM_KICKOFF_MS = 30000;
|
|
12
12
|
/** Snapshot scope: this host only, or a fleet-wide merge written by a reader. */
|
|
13
13
|
export type ActiveCacheScope = 'local' | 'fleet';
|
|
14
14
|
export interface ActiveSessionsSnapshot {
|
|
@@ -34,11 +34,11 @@ const IMMUTABLE_FILE = '.active-session-immutable.json';
|
|
|
34
34
|
* Short on purpose: live status (running/idle/waiting) must not go stale.
|
|
35
35
|
* The daemon warm tick uses the same cadence (see {@link SESSION_CACHE_WARM_INTERVAL_MS}).
|
|
36
36
|
*/
|
|
37
|
-
export const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS =
|
|
37
|
+
export const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS = 4 * 60_000;
|
|
38
38
|
/** Daemon warm interval — keep in sync with the setInterval in `lib/daemon.ts`. */
|
|
39
|
-
export const SESSION_CACHE_WARM_INTERVAL_MS =
|
|
40
|
-
/** Kick off the first warm
|
|
41
|
-
export const SESSION_CACHE_WARM_KICKOFF_MS =
|
|
39
|
+
export const SESSION_CACHE_WARM_INTERVAL_MS = 3 * 60_000;
|
|
40
|
+
/** Kick off the first warm 30s after daemon start (staggered off other ticks). */
|
|
41
|
+
export const SESSION_CACHE_WARM_KICKOFF_MS = 30_000;
|
|
42
42
|
/** Keys stored in the immutable memo (transcript-stable). */
|
|
43
43
|
export const IMMUTABLE_FIELD_KEYS = [
|
|
44
44
|
'topic',
|
package/dist/lib/shims.js
CHANGED
|
@@ -584,6 +584,18 @@ elif [ "$AGENT" = "muse" ]; then
|
|
|
584
584
|
esac
|
|
585
585
|
fi
|
|
586
586
|
fi
|
|
587
|
+
elif [ "$AGENT" = "warp" ]; then
|
|
588
|
+
# Warp Agent CLI installs a global, self-updating oz binary (brew cask on
|
|
589
|
+
# macOS, the oz-stable apt|yum|pacman package on Linux) -- a platform/package
|
|
590
|
+
# specific location, not ~/.local/bin -- so resolve it from PATH with the same
|
|
591
|
+
# shims-dir re-exec guard as droid/muse.
|
|
592
|
+
BINARY=$(adopted_original_bin || echo "")
|
|
593
|
+
if [ -z "$BINARY" ]; then
|
|
594
|
+
BINARY=$(command -v oz 2>/dev/null || echo "")
|
|
595
|
+
case "$(readlink -f "$BINARY" 2>/dev/null)" in
|
|
596
|
+
"$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
|
|
597
|
+
esac
|
|
598
|
+
fi
|
|
587
599
|
else
|
|
588
600
|
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
589
601
|
fi
|
|
@@ -1041,7 +1053,15 @@ else
|
|
|
1041
1053
|
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1042
1054
|
esac
|
|
1043
1055
|
fi`
|
|
1044
|
-
:
|
|
1056
|
+
: agent === 'warp'
|
|
1057
|
+
? `# Warp Agent CLI installs a global self-updating \`oz\` binary (brew cask on
|
|
1058
|
+
# macOS, oz-stable apt|yum|pacman on Linux) — a platform-specific location, not
|
|
1059
|
+
# ~/.local/bin — so resolve it from PATH, refusing anything under our shims dir.
|
|
1060
|
+
BINARY=$(command -v oz 2>/dev/null || echo "")
|
|
1061
|
+
case "$BINARY" in
|
|
1062
|
+
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1063
|
+
esac`
|
|
1064
|
+
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
1045
1065
|
return `#!/bin/bash
|
|
1046
1066
|
# Auto-generated by agents-cli - do not edit
|
|
1047
1067
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
package/dist/lib/signin-badge.js
CHANGED
|
@@ -24,6 +24,8 @@ export function loginHint(agentId) {
|
|
|
24
24
|
return `${cli}, then /login`;
|
|
25
25
|
case 'codex':
|
|
26
26
|
case 'grok':
|
|
27
|
+
// Warp Agent CLI: `oz login` opens a browser sign-in (or set WARP_API_KEY).
|
|
28
|
+
case 'warp':
|
|
27
29
|
return `${cli} login`;
|
|
28
30
|
case 'opencode':
|
|
29
31
|
return `${cli} auth login`;
|
|
@@ -131,3 +131,18 @@ export declare const LAZY_COMMAND_NAMES: ReadonlySet<string>;
|
|
|
131
131
|
* are handled directly in src/index.ts.
|
|
132
132
|
*/
|
|
133
133
|
export declare const COMMAND_LOADERS: Record<string, ModuleLoader[]>;
|
|
134
|
+
/**
|
|
135
|
+
* Every top-level command name the CLI answers to — the loader table plus the
|
|
136
|
+
* inline aliases/tombstones above. This is the "does this command exist?"
|
|
137
|
+
* predicate for code that runs BEFORE commander parses, most importantly the
|
|
138
|
+
* `--host`/`--device` router (lib/hosts/passthrough.ts): without it a typo'd
|
|
139
|
+
* command carrying `--host` reported a flag-support error instead of
|
|
140
|
+
* `unknown command` (RUSH-2022).
|
|
141
|
+
*
|
|
142
|
+
* Commander sub-aliases (`sessions ls`, `teams rm`, …) are deliberately absent —
|
|
143
|
+
* this set is top-level only. `command-registry.test.ts` pins it against the real
|
|
144
|
+
* registered command tree so a new command can never drift out of it.
|
|
145
|
+
*/
|
|
146
|
+
export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
|
|
147
|
+
/** Whether `name` is a top-level command this CLI registers. See {@link KNOWN_TOP_LEVEL_COMMANDS}. */
|
|
148
|
+
export declare function isKnownTopLevelCommand(name: string): boolean;
|
|
@@ -146,6 +146,11 @@ export const COMMAND_LOADERS = {
|
|
|
146
146
|
registry: [loadPackages],
|
|
147
147
|
search: [loadPackages],
|
|
148
148
|
install: [loadPackages],
|
|
149
|
+
// packages.ts also registers `publish` at top level (commands/packages.ts:435).
|
|
150
|
+
// It was missing here, so `agents publish` only worked via the unknown-command
|
|
151
|
+
// fallback that registers the whole tree — and the --host router could not see
|
|
152
|
+
// it as a real command at all (RUSH-2022).
|
|
153
|
+
publish: [loadPackages],
|
|
149
154
|
routines: [loadRoutines],
|
|
150
155
|
monitors: [loadMonitors],
|
|
151
156
|
projects: [loadProjects],
|
|
@@ -231,3 +236,40 @@ export const COMMAND_LOADERS = {
|
|
|
231
236
|
funnel: [loadFunnel],
|
|
232
237
|
humans: [loadHumans],
|
|
233
238
|
};
|
|
239
|
+
/**
|
|
240
|
+
* Top-level names that {@link COMMAND_LOADERS} does not carry because they are
|
|
241
|
+
* registered inline in src/index.ts — closures over entry-point state (the
|
|
242
|
+
* deprecated aliases and tombstones) plus the internal/upgrade commands. They are
|
|
243
|
+
* real commands, so anything that asks "does this command exist?" must count them.
|
|
244
|
+
*/
|
|
245
|
+
const INLINE_COMMAND_NAMES = [
|
|
246
|
+
'perms', // deprecated alias -> permissions
|
|
247
|
+
'exec', // deprecated alias -> run
|
|
248
|
+
'jobs', // deprecated alias -> routines
|
|
249
|
+
'cron', // deprecated alias -> routines
|
|
250
|
+
'check', // tombstone -> doctor --check
|
|
251
|
+
'resources', // tombstone -> view --merged
|
|
252
|
+
'hq', // tombstone
|
|
253
|
+
'_internal',
|
|
254
|
+
'upgrade',
|
|
255
|
+
];
|
|
256
|
+
/**
|
|
257
|
+
* Every top-level command name the CLI answers to — the loader table plus the
|
|
258
|
+
* inline aliases/tombstones above. This is the "does this command exist?"
|
|
259
|
+
* predicate for code that runs BEFORE commander parses, most importantly the
|
|
260
|
+
* `--host`/`--device` router (lib/hosts/passthrough.ts): without it a typo'd
|
|
261
|
+
* command carrying `--host` reported a flag-support error instead of
|
|
262
|
+
* `unknown command` (RUSH-2022).
|
|
263
|
+
*
|
|
264
|
+
* Commander sub-aliases (`sessions ls`, `teams rm`, …) are deliberately absent —
|
|
265
|
+
* this set is top-level only. `command-registry.test.ts` pins it against the real
|
|
266
|
+
* registered command tree so a new command can never drift out of it.
|
|
267
|
+
*/
|
|
268
|
+
export const KNOWN_TOP_LEVEL_COMMANDS = new Set([
|
|
269
|
+
...Object.keys(COMMAND_LOADERS),
|
|
270
|
+
...INLINE_COMMAND_NAMES,
|
|
271
|
+
]);
|
|
272
|
+
/** Whether `name` is a top-level command this CLI registers. See {@link KNOWN_TOP_LEVEL_COMMANDS}. */
|
|
273
|
+
export function isKnownTopLevelCommand(name) {
|
|
274
|
+
return KNOWN_TOP_LEVEL_COMMANDS.has(name);
|
|
275
|
+
}
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -197,7 +197,7 @@ export function buildTeammateSpawnEnv(envOverrides) {
|
|
|
197
197
|
*/
|
|
198
198
|
export { captureProcessStartTime };
|
|
199
199
|
/** Agent types the team runner supports. */
|
|
200
|
-
const TEAM_AGENT_TYPES = ['codex', 'cursor', 'claude', 'opencode', 'grok', 'antigravity', 'kimi', 'droid'];
|
|
200
|
+
const TEAM_AGENT_TYPES = ['codex', 'cursor', 'claude', 'opencode', 'grok', 'antigravity', 'kimi', 'droid', 'warp'];
|
|
201
201
|
// Suffix appended to all prompts to ensure agents provide a summary
|
|
202
202
|
const PROMPT_SUFFIX = `
|
|
203
203
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Supported agent CLI types for team spawning. */
|
|
2
|
-
export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity' | 'kimi' | 'droid';
|
|
2
|
+
export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity' | 'kimi' | 'droid' | 'warp';
|
|
3
3
|
/** Normalize a raw JSON event from any agent type into an array of unified event objects. */
|
|
4
4
|
export declare function normalizeEvents(agentType: AgentType, raw: any): any[];
|
|
5
5
|
/** Normalize a raw JSON event, returning only the first unified event (convenience wrapper). */
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { CloudProviderId } from './cloud/types.js';
|
|
9
9
|
import type { FeedBroadcastConfig } from './feed-broadcast.js';
|
|
10
10
|
/** Unique identifier for a current or legacy AI coding agent. */
|
|
11
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi' | 'muse';
|
|
11
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi' | 'muse' | 'warp';
|
|
12
12
|
/** How `agents run <agent>` chooses an installed version when none is pinned. */
|
|
13
13
|
export type RunStrategy = 'pinned' | 'available' | 'balanced';
|
|
14
14
|
export type RunEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
package/dist/lib/versions.js
CHANGED
|
@@ -928,6 +928,21 @@ export function getBinaryPath(agent, version) {
|
|
|
928
928
|
? path.join(getHomeDir(), 'bin', 'muse.exe')
|
|
929
929
|
: path.join(getHomeDir(), '.local', 'bin', 'muse');
|
|
930
930
|
}
|
|
931
|
+
if (agent === 'warp') {
|
|
932
|
+
// Warp Agent CLI installs a single global, self-updating `oz` binary — brew
|
|
933
|
+
// cask on macOS, the `oz-stable` apt|yum|pacman package on Linux — so, unlike
|
|
934
|
+
// droid/muse (which land at ~/.local/bin), its install location is
|
|
935
|
+
// platform/package specific. Resolve the real binary on PATH (findInPath
|
|
936
|
+
// skips our own shims dir) so isVersionInstalled / agents view agree with
|
|
937
|
+
// what executes. When oz is not installed, fall back to a deterministic path
|
|
938
|
+
// that won't exist, so isVersionInstalled reports uninstalled honestly.
|
|
939
|
+
const onPath = findInPath('oz');
|
|
940
|
+
if (onPath)
|
|
941
|
+
return onPath;
|
|
942
|
+
return IS_WINDOWS
|
|
943
|
+
? path.join(getHomeDir(), 'bin', 'oz.exe')
|
|
944
|
+
: '/opt/warpdotdev/oz-stable/oz';
|
|
945
|
+
}
|
|
931
946
|
const versionDir = getVersionDir(agent, version);
|
|
932
947
|
return path.join(versionDir, 'node_modules', '.bin', agentConfig.cliCommand);
|
|
933
948
|
}
|
|
@@ -1539,7 +1554,7 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1539
1554
|
// exec itself forever. So we skip the resolver-backed agents here AND, for
|
|
1540
1555
|
// everyone else, filter the shims dir out of the `which` candidates so the
|
|
1541
1556
|
// same race can't bite a non-special-cased installScript agent.
|
|
1542
|
-
if (agent !== 'grok' && agent !== 'droid' && agent !== 'muse') {
|
|
1557
|
+
if (agent !== 'grok' && agent !== 'droid' && agent !== 'muse' && agent !== 'warp') {
|
|
1543
1558
|
// findInPath is a pure-Node PATH scan that already skips our own shims
|
|
1544
1559
|
// dir — so it returns the genuine install, never our dispatcher shim
|
|
1545
1560
|
// (which sits ahead of ~/.local/bin on PATH and would otherwise be
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ActiveSession } from '../session/active.js';
|
|
2
|
+
import { type WatchdogThresholds, type WatchdogTickResult } from './runner.js';
|
|
3
|
+
export interface WatchdogPassOptions {
|
|
4
|
+
nudge: boolean;
|
|
5
|
+
nudgeText?: string;
|
|
6
|
+
smart?: boolean;
|
|
7
|
+
smartAgent?: string;
|
|
8
|
+
thresholds?: WatchdogThresholds;
|
|
9
|
+
allowGhosttyFocus?: boolean;
|
|
10
|
+
sessions?: ActiveSession[];
|
|
11
|
+
stateDir?: string;
|
|
12
|
+
mailboxGc?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadWatchdogSessions(): Promise<ActiveSession[]>;
|
|
15
|
+
export declare function runWatchdogMailboxGc(sessions: ActiveSession[]): void;
|
|
16
|
+
/** Execute one watchdog pass from the daemon or the explicit CLI command. */
|
|
17
|
+
export declare function runWatchdogPass(opts: WatchdogPassOptions): Promise<WatchdogTickResult>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { gcMailbox } from '../mailbox-gc.js';
|
|
3
|
+
import { mailboxIdForActiveSession } from '../mailbox-target.js';
|
|
4
|
+
import { getActiveSessions } from '../session/active.js';
|
|
5
|
+
import { loadLocalActiveSessions } from '../session/session-cache.js';
|
|
6
|
+
import { getRuntimeStateDir } from '../state.js';
|
|
7
|
+
import { runWatchdogTick } from './runner.js';
|
|
8
|
+
export async function loadWatchdogSessions() {
|
|
9
|
+
const loaded = await loadLocalActiveSessions({
|
|
10
|
+
gather: () => getActiveSessions({ localOnly: true }),
|
|
11
|
+
});
|
|
12
|
+
return loaded.sessions;
|
|
13
|
+
}
|
|
14
|
+
export function runWatchdogMailboxGc(sessions) {
|
|
15
|
+
const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => Boolean(id)));
|
|
16
|
+
try {
|
|
17
|
+
gcMailbox(activeBoxIds);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Housekeeping is retried by the next daemon tick.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Execute one watchdog pass from the daemon or the explicit CLI command. */
|
|
24
|
+
export async function runWatchdogPass(opts) {
|
|
25
|
+
const sessions = opts.sessions ?? await loadWatchdogSessions();
|
|
26
|
+
const result = await runWatchdogTick({
|
|
27
|
+
nudge: opts.nudge,
|
|
28
|
+
nudgeText: opts.nudgeText,
|
|
29
|
+
smart: opts.smart,
|
|
30
|
+
smartAgent: opts.smartAgent,
|
|
31
|
+
thresholds: opts.thresholds,
|
|
32
|
+
allowGhosttyFocus: opts.allowGhosttyFocus,
|
|
33
|
+
stateDir: opts.stateDir ?? path.join(getRuntimeStateDir(), 'watchdog'),
|
|
34
|
+
sessions,
|
|
35
|
+
});
|
|
36
|
+
if (opts.mailboxGc !== false)
|
|
37
|
+
runWatchdogMailboxGc(sessions);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.25",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|