@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +167 -1
  2. package/README.md +14 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.js +9 -5
  5. package/dist/commands/doctor.d.ts +24 -0
  6. package/dist/commands/doctor.js +100 -9
  7. package/dist/commands/exec.js +19 -16
  8. package/dist/commands/feed.d.ts +5 -0
  9. package/dist/commands/feed.js +21 -1
  10. package/dist/commands/focus.js +2 -2
  11. package/dist/commands/menubar.js +8 -0
  12. package/dist/commands/routines.d.ts +3 -0
  13. package/dist/commands/routines.js +70 -47
  14. package/dist/commands/run-cloud.js +1 -1
  15. package/dist/commands/sessions-browser.d.ts +1 -1
  16. package/dist/commands/sessions-browser.js +27 -7
  17. package/dist/commands/sessions-resume.js +3 -2
  18. package/dist/commands/sessions.d.ts +13 -1
  19. package/dist/commands/sessions.js +24 -2
  20. package/dist/commands/setup-watchdog.js +5 -10
  21. package/dist/commands/setup.js +1 -1
  22. package/dist/commands/teams.js +3 -2
  23. package/dist/commands/watchdog.d.ts +3 -4
  24. package/dist/commands/watchdog.js +26 -66
  25. package/dist/index.js +36 -1
  26. package/dist/lib/agents.js +126 -21
  27. package/dist/lib/cloud/cursor.d.ts +79 -0
  28. package/dist/lib/cloud/cursor.js +228 -0
  29. package/dist/lib/cloud/registry.js +2 -0
  30. package/dist/lib/cloud/types.d.ts +7 -2
  31. package/dist/lib/cloud/types.js +14 -0
  32. package/dist/lib/crabbox/cli.d.ts +2 -2
  33. package/dist/lib/crabbox/config.d.ts +7 -8
  34. package/dist/lib/crabbox/config.js +14 -14
  35. package/dist/lib/crabbox/lease.d.ts +11 -4
  36. package/dist/lib/crabbox/lease.js +40 -8
  37. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  38. package/dist/lib/crabbox/setup-copy.js +17 -1
  39. package/dist/lib/daemon.js +27 -1
  40. package/dist/lib/device-config.js +7 -0
  41. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  42. package/dist/lib/devices/doctor-findings.js +40 -1
  43. package/dist/lib/events.d.ts +9 -0
  44. package/dist/lib/events.js +58 -0
  45. package/dist/lib/exec.d.ts +3 -3
  46. package/dist/lib/exec.js +24 -10
  47. package/dist/lib/feed-outcome.d.ts +3 -0
  48. package/dist/lib/feed-outcome.js +18 -10
  49. package/dist/lib/feed.d.ts +4 -0
  50. package/dist/lib/hosts/passthrough.d.ts +21 -0
  51. package/dist/lib/hosts/passthrough.js +39 -12
  52. package/dist/lib/mcp.js +5 -1
  53. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  54. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  55. package/dist/lib/menubar/snapshot.d.ts +15 -0
  56. package/dist/lib/menubar/snapshot.js +40 -0
  57. package/dist/lib/plugins.js +13 -1
  58. package/dist/lib/resources/mcp.js +3 -0
  59. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  60. package/dist/lib/routine-process-cleanup.js +73 -0
  61. package/dist/lib/runner.js +5 -5
  62. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  63. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  64. package/dist/lib/secrets/bundles.js +1 -20
  65. package/dist/lib/secrets/filestore.d.ts +2 -0
  66. package/dist/lib/secrets/filestore.js +13 -0
  67. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  68. package/dist/lib/secrets/rc-hygiene.js +14 -1
  69. package/dist/lib/session/active.d.ts +4 -0
  70. package/dist/lib/session/db.js +8 -2
  71. package/dist/lib/session/remote-list.d.ts +2 -0
  72. package/dist/lib/session/remote-list.js +1 -0
  73. package/dist/lib/session/session-cache.d.ts +4 -4
  74. package/dist/lib/session/session-cache.js +4 -4
  75. package/dist/lib/shims.js +21 -1
  76. package/dist/lib/signin-badge.js +2 -0
  77. package/dist/lib/startup/command-registry.d.ts +15 -0
  78. package/dist/lib/startup/command-registry.js +42 -0
  79. package/dist/lib/teams/agents.js +1 -1
  80. package/dist/lib/teams/parsers.d.ts +1 -1
  81. package/dist/lib/types.d.ts +1 -1
  82. package/dist/lib/versions.js +16 -1
  83. package/dist/lib/watchdog/service.d.ts +17 -0
  84. package/dist/lib/watchdog/service.js +39 -0
  85. 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
+ }
@@ -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 (run.status !== 'running')
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
- // cursor-agent supports --plan, but the capability registry has not been
239
- // upgraded yet. RUSH-2101 tracks adding that read-only mode after PR #1721.
240
- const cursorMode = resolveHeadlessMode('cursor', mode, false, `routine ${config.name}`);
241
- if (cursorMode === 'skip') {
238
+ if (mode === 'plan') {
239
+ cmd.push('--plan');
240
+ }
241
+ else if (mode === 'skip') {
242
242
  cmd.push('-f');
243
243
  }
244
244
  else {
@@ -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;
@@ -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 = detectToolUsage(meta.id);
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 = 15000;
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 = 15000;
10
- /** Kick off the first warm ~25s after daemon start (staggered off other ticks). */
11
- export declare const SESSION_CACHE_WARM_KICKOFF_MS = 25000;
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 = 15_000;
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 = 15_000;
40
- /** Kick off the first warm ~25s after daemon start (staggered off other ticks). */
41
- export const SESSION_CACHE_WARM_KICKOFF_MS = 25_000;
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
- : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
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}
@@ -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
+ }
@@ -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). */
@@ -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';
@@ -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.24",
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",