@phnx-labs/agents-cli 1.20.78 → 1.20.82

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 (176) hide show
  1. package/CHANGELOG.md +487 -0
  2. package/README.md +25 -25
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +65 -0
  12. package/dist/commands/doctor.js +575 -31
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +187 -11
  29. package/dist/commands/routines.js +12 -2
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +100 -32
  33. package/dist/commands/sessions-inject.d.ts +7 -4
  34. package/dist/commands/sessions-inject.js +18 -18
  35. package/dist/commands/sessions-picker.js +32 -3
  36. package/dist/commands/sessions-resume.d.ts +16 -0
  37. package/dist/commands/sessions-resume.js +14 -13
  38. package/dist/commands/sessions.d.ts +31 -0
  39. package/dist/commands/sessions.js +141 -16
  40. package/dist/commands/set.d.ts +15 -0
  41. package/dist/commands/set.js +79 -0
  42. package/dist/commands/setup-fleet.d.ts +21 -0
  43. package/dist/commands/setup-fleet.js +201 -0
  44. package/dist/commands/setup-secrets.d.ts +20 -0
  45. package/dist/commands/setup-secrets.js +223 -0
  46. package/dist/commands/setup.js +17 -3
  47. package/dist/commands/skills.js +1 -1
  48. package/dist/commands/sync.js +7 -2
  49. package/dist/commands/teams-picker.js +0 -1
  50. package/dist/commands/teams.d.ts +33 -1
  51. package/dist/commands/teams.js +208 -44
  52. package/dist/commands/versions.js +10 -5
  53. package/dist/commands/view.d.ts +1 -0
  54. package/dist/commands/view.js +11 -0
  55. package/dist/index.js +5 -8
  56. package/dist/lib/acp/harnesses.js +0 -7
  57. package/dist/lib/activity.d.ts +156 -0
  58. package/dist/lib/activity.js +282 -0
  59. package/dist/lib/agents.d.ts +9 -2
  60. package/dist/lib/agents.js +72 -63
  61. package/dist/lib/browser/service.js +3 -0
  62. package/dist/lib/browser/types.d.ts +7 -0
  63. package/dist/lib/capabilities.js +6 -2
  64. package/dist/lib/crabbox/lease.js +2 -2
  65. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  66. package/dist/lib/crabbox/setup-copy.js +4 -4
  67. package/dist/lib/daemon.d.ts +18 -0
  68. package/dist/lib/daemon.js +47 -6
  69. package/dist/lib/devices/fleet.d.ts +1 -1
  70. package/dist/lib/devices/fleet.js +1 -1
  71. package/dist/lib/doctor-diff.d.ts +20 -0
  72. package/dist/lib/doctor-diff.js +6 -1
  73. package/dist/lib/drift.d.ts +31 -11
  74. package/dist/lib/drift.js +58 -7
  75. package/dist/lib/events.d.ts +8 -1
  76. package/dist/lib/events.js +10 -1
  77. package/dist/lib/exec.js +35 -8
  78. package/dist/lib/feed-post.d.ts +28 -1
  79. package/dist/lib/feed-post.js +110 -2
  80. package/dist/lib/fleet/apply.js +8 -1
  81. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  82. package/dist/lib/fleet/auth-sync.js +25 -11
  83. package/dist/lib/fleet/remote-login.js +5 -0
  84. package/dist/lib/git.d.ts +14 -14
  85. package/dist/lib/git.js +41 -44
  86. package/dist/lib/hooks.d.ts +42 -0
  87. package/dist/lib/hooks.js +137 -93
  88. package/dist/lib/hosts/dispatch.d.ts +28 -1
  89. package/dist/lib/hosts/dispatch.js +33 -5
  90. package/dist/lib/hosts/option.js +2 -2
  91. package/dist/lib/hosts/passthrough.d.ts +22 -1
  92. package/dist/lib/hosts/passthrough.js +238 -13
  93. package/dist/lib/hosts/run-target.d.ts +2 -0
  94. package/dist/lib/hosts/run-target.js +1 -0
  95. package/dist/lib/hq/floor.d.ts +1 -1
  96. package/dist/lib/hq/floor.js +6 -1
  97. package/dist/lib/mcp.js +3 -71
  98. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  99. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  100. package/dist/lib/menubar/notify-desktop.js +41 -5
  101. package/dist/lib/merged-resources.d.ts +11 -0
  102. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  103. package/dist/lib/permissions.d.ts +0 -32
  104. package/dist/lib/permissions.js +4 -157
  105. package/dist/lib/picker.js +1 -1
  106. package/dist/lib/platform/process.d.ts +16 -5
  107. package/dist/lib/platform/process.js +54 -0
  108. package/dist/lib/plugins.d.ts +0 -8
  109. package/dist/lib/plugins.js +4 -110
  110. package/dist/lib/project-resources.js +4 -1
  111. package/dist/lib/refresh.d.ts +5 -3
  112. package/dist/lib/refresh.js +7 -5
  113. package/dist/lib/resources/commands.js +4 -1
  114. package/dist/lib/resources/mcp.js +0 -4
  115. package/dist/lib/resources/permissions.d.ts +1 -1
  116. package/dist/lib/resources/permissions.js +1 -5
  117. package/dist/lib/resources/rules.js +4 -1
  118. package/dist/lib/resources/skills.js +4 -1
  119. package/dist/lib/resources/subagents.js +4 -0
  120. package/dist/lib/resources/types.d.ts +1 -1
  121. package/dist/lib/routines.d.ts +20 -0
  122. package/dist/lib/routines.js +6 -0
  123. package/dist/lib/run-defaults.d.ts +1 -0
  124. package/dist/lib/run-defaults.js +9 -0
  125. package/dist/lib/runner.d.ts +0 -12
  126. package/dist/lib/runner.js +49 -10
  127. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  128. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  129. package/dist/lib/secrets/agent.d.ts +16 -0
  130. package/dist/lib/secrets/agent.js +105 -3
  131. package/dist/lib/secrets/audit.d.ts +46 -0
  132. package/dist/lib/secrets/audit.js +56 -0
  133. package/dist/lib/secrets/bundles.d.ts +11 -8
  134. package/dist/lib/secrets/bundles.js +31 -23
  135. package/dist/lib/secrets/index.d.ts +2 -1
  136. package/dist/lib/secrets/index.js +8 -2
  137. package/dist/lib/secrets/remote.d.ts +75 -2
  138. package/dist/lib/secrets/remote.js +139 -5
  139. package/dist/lib/secrets/sync.js +3 -3
  140. package/dist/lib/session/active.d.ts +110 -26
  141. package/dist/lib/session/active.js +161 -50
  142. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  143. package/dist/lib/session/actor-sidecar.js +101 -0
  144. package/dist/lib/session/db.d.ts +4 -1
  145. package/dist/lib/session/db.js +65 -8
  146. package/dist/lib/session/discover.d.ts +4 -0
  147. package/dist/lib/session/discover.js +13 -0
  148. package/dist/lib/session/hook-sessions.d.ts +9 -0
  149. package/dist/lib/session/hook-sessions.js +57 -8
  150. package/dist/lib/session/pid-registry.d.ts +13 -0
  151. package/dist/lib/session/render.d.ts +2 -0
  152. package/dist/lib/session/render.js +1 -1
  153. package/dist/lib/session/types.d.ts +16 -0
  154. package/dist/lib/staleness/detectors/commands.js +2 -2
  155. package/dist/lib/staleness/detectors/permissions.js +1 -45
  156. package/dist/lib/staleness/writers/commands.js +4 -5
  157. package/dist/lib/staleness/writers/hooks.js +1 -1
  158. package/dist/lib/startup/command-registry.d.ts +1 -2
  159. package/dist/lib/startup/command-registry.js +2 -4
  160. package/dist/lib/subagents-registry.js +5 -12
  161. package/dist/lib/subagents.d.ts +0 -10
  162. package/dist/lib/subagents.js +0 -12
  163. package/dist/lib/teams/agents.d.ts +0 -2
  164. package/dist/lib/teams/agents.js +10 -27
  165. package/dist/lib/teams/index.d.ts +1 -1
  166. package/dist/lib/teams/index.js +1 -1
  167. package/dist/lib/types.d.ts +17 -9
  168. package/dist/lib/usage.d.ts +28 -5
  169. package/dist/lib/usage.js +271 -8
  170. package/dist/lib/versions.js +7 -1
  171. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  172. package/dist/lib/watchdog/watchdog.js +25 -13
  173. package/package.json +1 -2
  174. package/dist/commands/check.d.ts +0 -15
  175. package/dist/commands/check.js +0 -180
  176. package/dist/commands/resources.d.ts +0 -5
package/dist/lib/usage.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Usage and rate-limit tracking for Claude, Codex, Kimi, and Droid agents.
2
+ * Usage and rate-limit tracking for Claude, Codex, Kimi, Droid, Grok, and Cursor agents.
3
3
  *
4
4
  * Fetches live usage data from each agent's usage API (Anthropic OAuth for
5
5
  * Claude, Kimi Code /usages, Factory billing limits for Droid) or parses
@@ -18,6 +18,7 @@ import chalk from 'chalk';
18
18
  import { decodeJwtPayload, decryptDroidAuthPayload } from './agents.js';
19
19
  import { walkForFiles } from './fs-walk.js';
20
20
  import { getKeychainToken, setKeychainToken, deleteKeychainToken, isKeychainBackendOverridden, } from './secrets/index.js';
21
+ import { bundleExists, bundleBackend, readAndResolveBundleEnv } from './secrets/bundles.js';
21
22
  import { getCacheDir } from './state.js';
22
23
  const execFileAsync = promisify(execFile);
23
24
  const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
@@ -53,6 +54,7 @@ const getClaudeUsageCachePath = () => path.join(getCacheDir(), 'claude-usage.jso
53
54
  const CACHED_CLAUDE_USAGE_SOURCE_LABEL = 'last seen live account data';
54
55
  const KIMI_USAGES_URL = 'https://api.kimi.com/coding/v1/usages';
55
56
  const DROID_USAGE_URL = 'https://api.factory.ai/api/billing/limits';
57
+ const CURSOR_USAGE_URL = 'https://cursor.com/api/usage';
56
58
  const COMPACT_BAR_LEN = 5;
57
59
  const USAGE_BAR_LEN = 10;
58
60
  const FULL = '\u2588';
@@ -68,6 +70,10 @@ export async function getUsageInfo(agentId, options) {
68
70
  return getKimiUsageInfo(options);
69
71
  case 'droid':
70
72
  return getDroidUsageInfo(options);
73
+ case 'grok':
74
+ return getGrokUsageInfo(options);
75
+ case 'cursor':
76
+ return getCursorUsageInfo(options);
71
77
  default:
72
78
  return { snapshot: null, error: null };
73
79
  }
@@ -104,14 +110,19 @@ export function buildCanonicalUsageContext(inputs) {
104
110
  return { canonicalByUsageKey, usageFetchInputs };
105
111
  }
106
112
  /**
107
- * Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid via
108
- * a live API, Codex via local session logs. Everything else has no usage concept,
109
- * so callers use this to decide whether a missing snapshot is worth flagging as
110
- * "usage unavailable" (a signed-in Claude account with no data) versus simply not
111
- * applicable (Antigravity, Grok, OpenCode).
113
+ * Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid/Cursor
114
+ * via a live API, Codex/Grok via local session logs. Everything else has no usage
115
+ * concept, so callers use this to decide whether a missing snapshot is worth
116
+ * flagging as "usage unavailable" (a signed-in Claude account with no data)
117
+ * versus simply not applicable (Antigravity, OpenCode).
112
118
  */
113
119
  export function agentReportsUsage(agentId) {
114
- return agentId === 'claude' || agentId === 'codex' || agentId === 'kimi' || agentId === 'droid';
120
+ return (agentId === 'claude' ||
121
+ agentId === 'codex' ||
122
+ agentId === 'kimi' ||
123
+ agentId === 'droid' ||
124
+ agentId === 'grok' ||
125
+ agentId === 'cursor');
115
126
  }
116
127
  /** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
117
128
  export async function getUsageInfoByIdentity(inputs, opts) {
@@ -604,7 +615,9 @@ export function formatKimiPlan(data) {
604
615
  * here would race a concurrently running droid session and can permanently
605
616
  * invalidate the user's login chain. Droid refreshes its own credential when
606
617
  * it runs; if the stored token is expired we skip the live fetch and let the
607
- * SWR cache serve the last-seen snapshot.
618
+ * SWR cache serve the last-seen snapshot. This same single-use-rotation property
619
+ * is why `agents apply` refuses to propagate droid credentials across machines
620
+ * (see `isCredentialSafeToPropagate` in `../fleet/auth-sync.ts`).
608
621
  */
609
622
  async function getDroidUsageInfo(options) {
610
623
  try {
@@ -986,6 +999,65 @@ function deleteCachedClaudeOauth(service) {
986
999
  /* best-effort — cache is an optimization */
987
1000
  }
988
1001
  }
1002
+ /**
1003
+ * Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
1004
+ * setup-tokens. Usage/probe reads authenticate with these instead of Claude
1005
+ * Code's ACL-bound login item, so they never pop Touch ID. Keyed strictly
1006
+ * per-account (`CLAUDE_CODE_OAUTH_TOKEN_<slug>` from the account email) — never a
1007
+ * bare key, so one account's token can't be misapplied to another in a
1008
+ * multi-account fleet.
1009
+ */
1010
+ const AUTH_BUNDLE = 'auth';
1011
+ /** The per-account key an email maps to inside the `auth` bundle. */
1012
+ function claudeAccountTokenKey(account) {
1013
+ const slug = account
1014
+ .trim()
1015
+ .toUpperCase()
1016
+ .replace(/@/g, '_AT_')
1017
+ .replace(/\./g, '_DOT_')
1018
+ .replace(/[^A-Z0-9_]/g, '_');
1019
+ return `CLAUDE_CODE_OAUTH_TOKEN_${slug}`;
1020
+ }
1021
+ /** Signed-in account email for a version home, from `.claude.json` (no keychain). */
1022
+ function readClaudeAccountEmail(home) {
1023
+ const base = home ?? os.homedir();
1024
+ for (const p of [path.join(base, '.claude', '.claude.json'), path.join(base, '.claude.json')]) {
1025
+ try {
1026
+ const email = JSON.parse(fs.readFileSync(p, 'utf-8')).oauthAccount?.emailAddress;
1027
+ if (typeof email === 'string' && email.trim().length > 0)
1028
+ return email.trim();
1029
+ }
1030
+ catch {
1031
+ // Missing/unreadable at this location — try the next.
1032
+ }
1033
+ }
1034
+ return null;
1035
+ }
1036
+ /**
1037
+ * Resolve a long-lived `claude setup-token` for the account signed into `home`
1038
+ * from the reserved FILE-BASED `auth` bundle. Returns the token or null. Reads
1039
+ * ONLY when the bundle is file-backed (never keychain), so this path itself can
1040
+ * never trigger a Touch ID prompt — that is the entire point: usage/probe reads
1041
+ * authenticate with the shareable setup-token, not the ACL-bound login item.
1042
+ */
1043
+ function resolveClaudeSetupToken(home) {
1044
+ try {
1045
+ // Require a known account (email) up front: without it we cannot key a
1046
+ // per-account token, and we must NOT fall back to a bare shared key that
1047
+ // would misapply one account's setup-token to another.
1048
+ const email = readClaudeAccountEmail(home);
1049
+ if (!email)
1050
+ return null;
1051
+ if (!bundleExists(AUTH_BUNDLE) || bundleBackend(AUTH_BUNDLE) !== 'file')
1052
+ return null;
1053
+ const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
1054
+ const v = (env[claudeAccountTokenKey(email)] ?? '').trim();
1055
+ return v.length > 0 ? v : null;
1056
+ }
1057
+ catch {
1058
+ return null;
1059
+ }
1060
+ }
989
1061
  /**
990
1062
  * Load a version home's Claude OAuth credential from the two stores Claude Code
991
1063
  * uses, tried in order:
@@ -1012,6 +1084,23 @@ function deleteCachedClaudeOauth(service) {
1012
1084
  * access-token-only consumers (the usage fetch and the auth-health probe) opt in.
1013
1085
  */
1014
1086
  export async function loadClaudeOauth(home, opts) {
1087
+ // Read-only usage/probe callers (accessTokenCache) authenticate with a
1088
+ // file-based setup-token when one is provisioned: the usage endpoint accepts
1089
+ // any sk-ant-oat01 bearer, and the token is read from the file-based `auth`
1090
+ // bundle — never Claude Code's ACL-bound keychain item — so it never pops
1091
+ // Touch ID. Transitional: an account with no provisioned setup-token still
1092
+ // falls through to the keychain/file read below (that fallback is removed once
1093
+ // the fleet is fully seeded, per docs/design/credential-management.md).
1094
+ if (opts?.accessTokenCache === true) {
1095
+ const setupToken = resolveClaudeSetupToken(home);
1096
+ if (setupToken) {
1097
+ // No expiresAt: a setup-token is long-lived and non-rotating, and a null
1098
+ // expiry reads as "still fresh" (claudeAccessTokenNeedsRefresh) so the
1099
+ // probe never reports it expired or tries to refresh it. The endpoint is
1100
+ // the source of truth if it has actually been revoked.
1101
+ return { accessToken: setupToken };
1102
+ }
1103
+ }
1015
1104
  // The OS keychain/keyring step is macOS/Linux-only. Windows skips straight
1016
1105
  // to the .credentials.json fallback — the Claude CLI has no keychain
1017
1106
  // integration there and stores its OAuth token in that file too. An injected
@@ -1448,3 +1537,177 @@ function safeStatSync(filePath) {
1448
1537
  return null;
1449
1538
  }
1450
1539
  }
1540
+ /** Parse the latest billing info from Grok's unified log. */
1541
+ async function getGrokUsageInfo(options) {
1542
+ try {
1543
+ const base = options?.home || os.homedir();
1544
+ const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
1545
+ if (!fs.existsSync(logPath))
1546
+ return { snapshot: null, error: null };
1547
+ const match = await readLatestGrokBilling(logPath);
1548
+ if (!match)
1549
+ return { snapshot: null, error: null };
1550
+ return {
1551
+ snapshot: {
1552
+ source: 'last_seen',
1553
+ sourceLabel: 'last seen in Grok logs',
1554
+ capturedAt: match.capturedAt,
1555
+ windows: match.windows,
1556
+ plan: match.subscriptionTier,
1557
+ },
1558
+ error: null,
1559
+ };
1560
+ }
1561
+ catch {
1562
+ return { snapshot: null, error: null };
1563
+ }
1564
+ }
1565
+ async function readLatestGrokBilling(filePath) {
1566
+ return new Promise((resolve) => {
1567
+ let latest = null;
1568
+ const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
1569
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
1570
+ rl.on('line', (line) => {
1571
+ if (!line.trim())
1572
+ return;
1573
+ try {
1574
+ const parsed = JSON.parse(line);
1575
+ if (parsed.msg === 'billing: fetched credits config' && parsed.ctx?.config) {
1576
+ const config = parsed.ctx.config;
1577
+ const windows = [];
1578
+ if (config.currentPeriod?.end) {
1579
+ // `creditUsagePercent` is Grok's weekly credit consumption (0-100);
1580
+ // the billing period's `end` is when that window resets.
1581
+ const rawPercent = typeof config.creditUsagePercent === 'number' ? config.creditUsagePercent : 0;
1582
+ windows.push({
1583
+ key: 'week',
1584
+ label: 'Current week',
1585
+ shortLabel: 'W',
1586
+ usedPercent: Math.max(0, Math.min(100, rawPercent)),
1587
+ resetsAt: parseDateValue(config.currentPeriod.end),
1588
+ windowMinutes: inferWindowMinutes('week'),
1589
+ });
1590
+ }
1591
+ latest = {
1592
+ capturedAt: parseDateValue(parsed.ts),
1593
+ subscriptionTier: parsed.ctx.subscriptionTier || null,
1594
+ windows,
1595
+ };
1596
+ }
1597
+ }
1598
+ catch {
1599
+ /* malformed session line */
1600
+ }
1601
+ });
1602
+ rl.on('close', () => resolve(latest));
1603
+ rl.on('error', () => resolve(latest));
1604
+ });
1605
+ }
1606
+ /**
1607
+ * Normalize Cursor's /api/usage payload into the common UsageWindow shape.
1608
+ *
1609
+ * Only free / legacy request-capped plans carry a `maxRequestUsage` on the
1610
+ * premium ("gpt-4") bucket — that's the fast-request cap the plan meters, and it
1611
+ * maps cleanly to a monthly window. Usage-based plans report `maxRequestUsage:
1612
+ * null` (no request cap — spend is metered in dollars instead), so they have no
1613
+ * bar to draw here and return no windows rather than a misleading empty gauge.
1614
+ */
1615
+ export function normalizeCursorUsage(data) {
1616
+ const premium = data['gpt-4'];
1617
+ if (!premium || typeof premium !== 'object')
1618
+ return [];
1619
+ const max = premium.maxRequestUsage;
1620
+ if (typeof max !== 'number' || !Number.isFinite(max) || max <= 0)
1621
+ return [];
1622
+ const used = typeof premium.numRequests === 'number' ? premium.numRequests : 0;
1623
+ const startOfMonth = typeof data.startOfMonth === 'string' ? parseDateValue(data.startOfMonth) : null;
1624
+ // The request quota resets one calendar month after the period start. Guard the
1625
+ // month-end overflow: setMonth on a day the target month lacks (Jan 31 -> Feb 31)
1626
+ // rolls forward into the month after (Mar 3), so clamp back to the intended
1627
+ // month's last day.
1628
+ let resetsAt = null;
1629
+ if (startOfMonth) {
1630
+ resetsAt = new Date(startOfMonth);
1631
+ const intendedMonth = (resetsAt.getMonth() + 1) % 12;
1632
+ resetsAt.setMonth(resetsAt.getMonth() + 1);
1633
+ if (resetsAt.getMonth() !== intendedMonth)
1634
+ resetsAt.setDate(0);
1635
+ }
1636
+ return [
1637
+ {
1638
+ key: 'month',
1639
+ label: 'Current month',
1640
+ shortLabel: 'M',
1641
+ usedPercent: Math.max(0, Math.min(100, (used / max) * 100)),
1642
+ resetsAt,
1643
+ windowMinutes: inferWindowMinutes('month'),
1644
+ },
1645
+ ];
1646
+ }
1647
+ /** Read Cursor's OAuth subject + access token from the local CLI config/auth files. */
1648
+ function readCursorCredentials(base) {
1649
+ try {
1650
+ const cfgPath = path.join(base, '.cursor', 'cli-config.json');
1651
+ const authPath = path.join(base, '.config', 'cursor', 'auth.json');
1652
+ if (!fs.existsSync(cfgPath) || !fs.existsSync(authPath))
1653
+ return null;
1654
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
1655
+ const sub = cfg?.authInfo?.authId;
1656
+ const auth = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
1657
+ const accessToken = auth?.accessToken;
1658
+ if (typeof sub !== 'string' || !sub)
1659
+ return null;
1660
+ if (typeof accessToken !== 'string' || !accessToken)
1661
+ return null;
1662
+ return { sub, accessToken };
1663
+ }
1664
+ catch {
1665
+ return null;
1666
+ }
1667
+ }
1668
+ /**
1669
+ * Fetch Cursor usage from the dashboard usage endpoint. Cursor authenticates the
1670
+ * request with a `WorkosCursorSessionToken` cookie of the form
1671
+ * `<oauth-subject>::<access-token>` (the same pair the web dashboard sends), not a
1672
+ * bearer header. Free/legacy plans return a monthly request bar; usage-based plans
1673
+ * have no request cap, so they return a live-but-window-less snapshot (the account
1674
+ * row still renders, without a misleading empty gauge).
1675
+ */
1676
+ async function getCursorUsageInfo(options) {
1677
+ try {
1678
+ const base = options?.home || os.homedir();
1679
+ const creds = readCursorCredentials(base);
1680
+ if (!creds)
1681
+ return { snapshot: null, error: null };
1682
+ const exp = decodeJwtPayload(creds.accessToken)?.exp;
1683
+ if (typeof exp === 'number' && Date.now() / 1000 >= exp) {
1684
+ return { snapshot: null, error: null };
1685
+ }
1686
+ const url = `${CURSOR_USAGE_URL}?user=${encodeURIComponent(creds.sub)}`;
1687
+ const response = await fetch(url, {
1688
+ method: 'GET',
1689
+ headers: {
1690
+ Cookie: `WorkosCursorSessionToken=${creds.sub}%3A%3A${creds.accessToken}`,
1691
+ Accept: 'application/json',
1692
+ },
1693
+ signal: AbortSignal.timeout(5000),
1694
+ });
1695
+ // 401/redirect => revoked/expired session; render nothing rather than a
1696
+ // misleading empty bar.
1697
+ if (!response.ok)
1698
+ return { snapshot: null, error: null };
1699
+ const data = (await response.json());
1700
+ return {
1701
+ snapshot: {
1702
+ source: 'live',
1703
+ sourceLabel: 'live account data',
1704
+ capturedAt: new Date(),
1705
+ windows: normalizeCursorUsage(data),
1706
+ },
1707
+ error: null,
1708
+ };
1709
+ }
1710
+ catch {
1711
+ return { snapshot: null, error: null };
1712
+ }
1713
+ }
@@ -29,7 +29,7 @@ import { activeRulesPreset, filterNamesForActiveResourceProfile } from './resour
29
29
  // (single source of truth). Re-exported below so existing importers of
30
30
  // `compareVersions` from './versions.js' keep working.
31
31
  import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
32
- import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent } from './agents.js';
32
+ import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent, isAgentHardDeprecated, hardDeprecationError } from './agents.js';
33
33
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
34
34
  import { parseMcpServerConfig, isProjectMcpTrusted } from './mcp.js';
35
35
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, } from './shims.js';
@@ -1337,6 +1337,9 @@ function relocateGrokBinaryToVersionHome(installedVersion) {
1337
1337
  */
1338
1338
  export async function installVersion(agent, version, onProgress, opts) {
1339
1339
  const agentConfig = AGENTS[agent];
1340
+ if (isAgentHardDeprecated(agent)) {
1341
+ return { success: false, installedVersion: version, error: hardDeprecationError(agent) };
1342
+ }
1340
1343
  // Validate before deriving filesystem paths or npm package specs. The CLI
1341
1344
  // parser already enforces this for user input; this guard protects direct
1342
1345
  // callers and tests the critical install path at the source.
@@ -2416,6 +2419,9 @@ export function mergeRepoScopedSelections(repos, cwd = process.cwd()) {
2416
2419
  * For Gemini: commands are converted from markdown to TOML.
2417
2420
  */
2418
2421
  export function syncResourcesToVersion(agent, version, selection, options = {}) {
2422
+ if (isAgentHardDeprecated(agent)) {
2423
+ return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
2424
+ }
2419
2425
  const agentConfig = AGENTS[agent];
2420
2426
  const versionHome = getVersionHomePath(agent, version);
2421
2427
  const agentDir = path.join(versionHome, agentConfigDirName(agent));
@@ -33,7 +33,7 @@ export interface ClassifyInput {
33
33
  dormantMs: number;
34
34
  }
35
35
  export declare function classifyTerminal(input: ClassifyInput): StallStatus;
36
- export declare const WATCHDOG_SYSTEM_PROMPT = "You are the watchdog for AI coding agents running in terminals. These agents are\nexpected to DRIVE TO COMPLETION end-to-end. They too often stop to ask a needless\nquestion or pause with the task unfinished. For each stalled terminal below, decide\nNUDGE (send a message that unsticks it and pushes it to finish) or SKIP (it genuinely\nneeds the human).\n\nNUDGE when the agent could have continued on its own:\n- It asked permission for an obvious or already-authorized next step\n (\"should I proceed?\", \"want me to continue?\", \"shall I run the tests?\").\n- It asked a question it could answer itself from the available context or a\n reasonable default.\n- It announced an action (\"I'll run X\", \"let me write Y\") but no tool call followed.\n- It paused with the task incomplete and no real blocker.\nThe nudge text MUST: restate the goal, tell it to use best judgment and finish\nend-to-end WITHOUT asking again, and give ONE concrete hint \u2014 the specific next\nstep, a tool it forgot, or the sensible default to take. Imperative, 1-2 sentences,\nno emojis, under 200 characters.\n\nSKIP when the agent genuinely needs the human:\n- Credentials, auth, login, 2FA, or biometric.\n- An irreversible or outward-facing action that needs sign-off (force-push, delete\n prod data, publish/release, spend money, send an external message).\n- A real product or intent decision with genuine ambiguity (not a trivial default).\n- The task is actually complete.\n- You cannot tell what the agent is doing.\n\nRespond with ONLY a JSON array (no prose, no code fence):\n[{\"terminalId\":\"<id>\",\"action\":\"nudge\"|\"skip\",\"text\":\"<message or empty>\",\"reason\":\"<brief>\"}]";
36
+ export declare const WATCHDOG_SYSTEM_PROMPT = "You are the watchdog for AI coding agents running in terminals. Your one job is to get\nIDLE agents moving to completion: each agent has a goal and is expected to DRIVE TO\nCOMPLETION end-to-end, but the terminals below have gone idle. Read each one's goal and\nWHY it stopped, then decide NUDGE (send a message that unsticks it and drives it to\nfinish) or SKIP (it genuinely needs the human).\n\nRead the transcript before judging \u2014 an agent that already reached a decision needs \"do\nit,\" not \"decide.\"\n\nNUDGE when the agent went idle and could keep going on its own:\n- It asked permission for an obvious or already-authorized next step\n (\"should I proceed?\", \"want me to continue?\", \"shall I run the tests?\").\n- It asked a question it could answer itself from the available context or a\n reasonable default, or by using a tool it already has.\n- It announced an action (\"I'll run X\", \"let me write Y\") but no tool call followed.\n- It already decided what to do, then stalled without doing it.\n- It paused with the task incomplete and no real blocker.\n\nThe nudge text MUST carry context, not shove:\n- Restate the goal and reference the conclusion the agent ALREADY reached.\n- Give ONE concrete next step \u2014 the specific action, the sensible default, or a TOOL it\n forgot it has (e.g. \"agents computer\" to drive the Mac, \"agents browser\" for the web,\n \"agents ssh <mac> \\\"agents computer \u2026\\\"\" to drive a Mac from another box).\n- Split the ask: drive the reversible, goal-advancing part now; flag only a genuinely\n disruptive sub-step for the human.\n- Tell it to use best judgment and finish end-to-end WITHOUT asking again.\n- Imperative, 1-2 sentences, no emojis, under 240 characters.\n\nSKIP when the agent genuinely needs the human (these belong in the user's feed, not a\nnudge):\n- Credentials, auth, login, 2FA, or biometric.\n- An irreversible or outward-facing action that needs sign-off (force-push, delete\n prod data, publish/release, spend money, send an external message) \u2014 UNLESS the House\n Rules below authorize it.\n- A real product or intent decision with genuine ambiguity (not a trivial default).\n- The task is actually complete.\n- You cannot tell what the agent is doing.\n\nRespond with ONLY a JSON array (no prose, no code fence):\n[{\"terminalId\":\"<id>\",\"action\":\"nudge\"|\"skip\",\"text\":\"<message or empty>\",\"reason\":\"<brief>\"}]";
37
37
  export declare function composePromptWithPlaybook(basePrompt: string, playbook: string): string;
38
38
  export declare function renderWatchdogPrompt(candidates: WatchdogCandidate[], playbook?: string): string;
39
39
  export declare function parseWatchdogResponse(stdout: string): Decision[];
@@ -64,28 +64,40 @@ export function classifyTerminal(input) {
64
64
  }
65
65
  return { kind: 'stalled', stalledForMs: age };
66
66
  }
67
- export const WATCHDOG_SYSTEM_PROMPT = `You are the watchdog for AI coding agents running in terminals. These agents are
68
- expected to DRIVE TO COMPLETION end-to-end. They too often stop to ask a needless
69
- question or pause with the task unfinished. For each stalled terminal below, decide
70
- NUDGE (send a message that unsticks it and pushes it to finish) or SKIP (it genuinely
71
- needs the human).
67
+ export const WATCHDOG_SYSTEM_PROMPT = `You are the watchdog for AI coding agents running in terminals. Your one job is to get
68
+ IDLE agents moving to completion: each agent has a goal and is expected to DRIVE TO
69
+ COMPLETION end-to-end, but the terminals below have gone idle. Read each one's goal and
70
+ WHY it stopped, then decide NUDGE (send a message that unsticks it and drives it to
71
+ finish) or SKIP (it genuinely needs the human).
72
72
 
73
- NUDGE when the agent could have continued on its own:
73
+ Read the transcript before judging — an agent that already reached a decision needs "do
74
+ it," not "decide."
75
+
76
+ NUDGE when the agent went idle and could keep going on its own:
74
77
  - It asked permission for an obvious or already-authorized next step
75
78
  ("should I proceed?", "want me to continue?", "shall I run the tests?").
76
79
  - It asked a question it could answer itself from the available context or a
77
- reasonable default.
80
+ reasonable default, or by using a tool it already has.
78
81
  - It announced an action ("I'll run X", "let me write Y") but no tool call followed.
82
+ - It already decided what to do, then stalled without doing it.
79
83
  - It paused with the task incomplete and no real blocker.
80
- The nudge text MUST: restate the goal, tell it to use best judgment and finish
81
- end-to-end WITHOUT asking again, and give ONE concrete hint — the specific next
82
- step, a tool it forgot, or the sensible default to take. Imperative, 1-2 sentences,
83
- no emojis, under 200 characters.
84
84
 
85
- SKIP when the agent genuinely needs the human:
85
+ The nudge text MUST carry context, not shove:
86
+ - Restate the goal and reference the conclusion the agent ALREADY reached.
87
+ - Give ONE concrete next step — the specific action, the sensible default, or a TOOL it
88
+ forgot it has (e.g. "agents computer" to drive the Mac, "agents browser" for the web,
89
+ "agents ssh <mac> \\"agents computer …\\"" to drive a Mac from another box).
90
+ - Split the ask: drive the reversible, goal-advancing part now; flag only a genuinely
91
+ disruptive sub-step for the human.
92
+ - Tell it to use best judgment and finish end-to-end WITHOUT asking again.
93
+ - Imperative, 1-2 sentences, no emojis, under 240 characters.
94
+
95
+ SKIP when the agent genuinely needs the human (these belong in the user's feed, not a
96
+ nudge):
86
97
  - Credentials, auth, login, 2FA, or biometric.
87
98
  - An irreversible or outward-facing action that needs sign-off (force-push, delete
88
- prod data, publish/release, spend money, send an external message).
99
+ prod data, publish/release, spend money, send an external message) — UNLESS the House
100
+ Rules below authorize it.
89
101
  - A real product or intent decision with genuine ambiguity (not a trivial default).
90
102
  - The task is actually complete.
91
103
  - You cannot tell what the agent is doing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.78",
3
+ "version": "1.20.82",
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",
@@ -63,7 +63,6 @@
63
63
  "dotfiles",
64
64
  "claude",
65
65
  "codex",
66
- "gemini",
67
66
  "cursor",
68
67
  "opencode",
69
68
  "grok",
@@ -1,15 +0,0 @@
1
- /**
2
- * `agents check` — scriptable drift gate for CI.
3
- *
4
- * Runs the SAME drift/divergence diagnostic `agents doctor` computes (via the
5
- * shared `computeDrift` in `../lib/drift.js`) and turns it into an exit code:
6
- * non-zero when any installed version is stale or never-synced, zero when the
7
- * whole install is fresh. `agents doctor` is the human report; `agents check`
8
- * is the machine gate — same engine, different output.
9
- *
10
- * Orphans are surfaced informationally but never fail the check: they are a
11
- * `prune` concern, not sync drift (mirrors the sync-status engine, where an
12
- * orphan alone never flags needsSync).
13
- */
14
- import type { Command } from 'commander';
15
- export declare function registerCheckCommand(program: Command): void;
@@ -1,180 +0,0 @@
1
- import chalk from 'chalk';
2
- import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
3
- import { setHelpSections } from '../lib/help.js';
4
- import { computeDrift } from '../lib/drift.js';
5
- import { loadDevices } from '../lib/devices/registry.js';
6
- import { fanOutDevices, planFleetTargets, remoteFleetTargets } from '../lib/devices/fleet.js';
7
- import { fleetDialTarget } from '../lib/devices/connect.js';
8
- import { machineId } from '../lib/session/sync/config.js';
9
- import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
10
- import { sshExecAsync } from '../lib/ssh-exec.js';
11
- const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
12
- function label(row) {
13
- return `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
14
- }
15
- export function registerCheckCommand(program) {
16
- const checkCmd = program
17
- .command('check')
18
- .description('CI drift gate: exit non-zero when any installed version is out of sync (stale or never-synced), zero when clean.')
19
- .option('--json', 'Output machine-readable JSON')
20
- .option('-q, --quiet', 'Suppress per-version lines; print only the one-line verdict')
21
- .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
22
- .option('--devices', 'Run the drift gate across every registered online device');
23
- setHelpSections(checkCmd, {
24
- examples: `
25
- # Fail the build if anything drifted (exit 1), pass if clean (exit 0)
26
- agents check
27
-
28
- # Just the verdict line, nothing per-version
29
- agents check --quiet
30
-
31
- # Machine-readable, for scripting
32
- agents check --json
33
-
34
- # Gate every registered device
35
- agents check --devices
36
-
37
- # Gate in a CI step
38
- agents check || { echo "resources drifted — run 'agents doctor --fix'"; exit 1; }
39
- `,
40
- });
41
- checkCmd.action(async (opts) => {
42
- const cwd = opts.cwd ? opts.cwd : process.cwd();
43
- if (opts.devices) {
44
- await runDevicesCheck(opts, cwd);
45
- return;
46
- }
47
- const drift = computeDrift(cwd);
48
- if (opts.json) {
49
- console.log(JSON.stringify({
50
- hasDrift: drift.hasDrift,
51
- stale: drift.staleCount,
52
- neverSynced: drift.neverSyncedCount,
53
- orphanVersions: drift.orphanVersionCount,
54
- versions: drift.syncRows.map((r) => ({
55
- agent: r.agent,
56
- version: r.version,
57
- status: r.status,
58
- isDefault: r.isDefault,
59
- divergence: r.divergence ?? [],
60
- })),
61
- }, null, 2));
62
- process.exit(drift.hasDrift ? 1 : 0);
63
- }
64
- if (drift.syncRows.length === 0) {
65
- // Nothing installed is a clean state, not a failure — CI on a fresh
66
- // checkout with no versions should pass, not error.
67
- console.log(chalk.gray('check: no installed versions — nothing to verify'));
68
- process.exit(0);
69
- }
70
- if (!drift.hasDrift) {
71
- const orphanNote = drift.orphanVersionCount > 0
72
- ? chalk.gray(` (${drift.orphanVersionCount} version(s) carry orphans — run \`agents prune cleanup\`)`)
73
- : '';
74
- console.log(`${chalk.green('ok')} ${drift.syncRows.length} version(s) in sync${orphanNote}`);
75
- process.exit(0);
76
- }
77
- // Drift: one-line verdict always, per-version detail unless --quiet.
78
- const parts = [];
79
- if (drift.staleCount > 0)
80
- parts.push(`${drift.staleCount} stale`);
81
- if (drift.neverSyncedCount > 0)
82
- parts.push(`${drift.neverSyncedCount} never-synced`);
83
- console.error(`${chalk.red('drift')} ${parts.join(', ')} of ${drift.syncRows.length} version(s)`);
84
- if (!opts.quiet) {
85
- for (const row of drift.syncRows) {
86
- if (row.status === 'fresh')
87
- continue;
88
- const tag = row.status === 'stale' ? chalk.yellow('stale') : chalk.gray('cold ');
89
- console.error(` ${tag} ${label(row)}`);
90
- for (const line of row.divergence ?? []) {
91
- console.error(chalk.gray(` ${line}`));
92
- }
93
- }
94
- console.error(chalk.gray('\nReconcile with `agents doctor --fix` (or `agents doctor <agent>@<version> --fix`).'));
95
- }
96
- process.exit(1);
97
- });
98
- }
99
- function checkPayload(device, drift) {
100
- return {
101
- device,
102
- hasDrift: drift.hasDrift,
103
- stale: drift.staleCount,
104
- neverSynced: drift.neverSyncedCount,
105
- orphanVersions: drift.orphanVersionCount,
106
- };
107
- }
108
- async function probeDeviceCheck(target) {
109
- const isWin = /^win/i.test((target.platform ?? '').trim());
110
- const remoteCmd = buildRemoteAgentsInvocation(['check', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
111
- const res = await sshExecAsync(target.dialTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
112
- if (res.code !== 0 && !res.stdout.trim()) {
113
- throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
114
- }
115
- try {
116
- const parsed = JSON.parse(res.stdout);
117
- return {
118
- device: target.name,
119
- hasDrift: Boolean(parsed.hasDrift),
120
- stale: parsed.stale ?? 0,
121
- neverSynced: parsed.neverSynced ?? 0,
122
- orphanVersions: parsed.orphanVersions ?? 0,
123
- };
124
- }
125
- catch (err) {
126
- throw new Error(`invalid JSON (${err?.message ?? 'parse error'})`);
127
- }
128
- }
129
- async function runDevicesCheck(opts, cwd) {
130
- const registry = await loadDevices();
131
- const self = machineId();
132
- const planned = planFleetTargets(registry);
133
- const local = checkPayload(self, computeDrift(cwd));
134
- const remoteTargets = remoteFleetTargets(planned, self)
135
- .map((t) => ({
136
- name: t.device.name,
137
- platform: t.device.platform,
138
- skip: t.skip,
139
- dialTarget: fleetDialTarget(t.device),
140
- }));
141
- const remote = await fanOutDevices(remoteTargets, probeDeviceCheck);
142
- const devices = [local];
143
- for (const result of remote) {
144
- if (result.status === 'ok' && result.value) {
145
- devices.push(result.value);
146
- }
147
- else {
148
- devices.push({
149
- device: result.name,
150
- hasDrift: true,
151
- stale: 0,
152
- neverSynced: 0,
153
- orphanVersions: 0,
154
- error: result.error ?? String(result.reason ?? 'skipped'),
155
- });
156
- }
157
- }
158
- const hasDrift = devices.some((d) => d.hasDrift || d.error);
159
- if (opts.json) {
160
- console.log(JSON.stringify({ hasDrift, devices }, null, 2));
161
- process.exit(hasDrift ? 1 : 0);
162
- }
163
- if (!hasDrift) {
164
- console.log(chalk.green('ok') + chalk.gray(` ${devices.length} device(s) in sync`));
165
- process.exit(0);
166
- }
167
- console.error(chalk.red('drift') + chalk.gray(` ${devices.filter((d) => d.hasDrift || d.error).length} of ${devices.length} device(s)`));
168
- if (!opts.quiet) {
169
- for (const d of devices) {
170
- if (!d.hasDrift && !d.error)
171
- continue;
172
- const detail = d.error
173
- ? d.error
174
- : [`${d.stale} stale`, `${d.neverSynced} never-synced`].filter((p) => !p.startsWith('0 ')).join(', ');
175
- console.error(` ${chalk.yellow(d.device.padEnd(18))} ${detail || 'drift'}`);
176
- }
177
- console.error(chalk.gray('\nReconcile each device with `agents doctor --fix` or `agents repo pull user`.'));
178
- }
179
- process.exit(1);
180
- }
@@ -1,5 +0,0 @@
1
- /**
2
- * `agents resources` — show the merged DotAgents resource surface.
3
- */
4
- import type { Command } from 'commander';
5
- export declare function registerResourcesCommand(program: Command): void;