@phnx-labs/agents-cli 1.19.2 → 1.20.3

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 (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -2,9 +2,9 @@
2
2
  * Agent event stream parsers.
3
3
  *
4
4
  * Normalizes the heterogeneous JSON event formats emitted by each agent CLI
5
- * (Claude, Codex, Gemini, Cursor, OpenCode) into a unified event schema
6
- * with consistent types: init, message, tool_use, bash, file_read, file_write,
7
- * file_create, file_delete, result, error, and others.
5
+ * (Claude, Codex, Gemini, Cursor, OpenCode, Grok, Antigravity) into a unified
6
+ * event schema with consistent types: init, message, tool_use, bash,
7
+ * file_read, file_write, file_create, file_delete, result, error, and others.
8
8
  */
9
9
  import { extractFileOpsFromBash } from './file_ops.js';
10
10
  const claudeToolUseMap = new Map();
@@ -25,6 +25,12 @@ export function normalizeEvents(agentType, raw) {
25
25
  else if (agentType === 'opencode') {
26
26
  return normalizeOpencode(raw);
27
27
  }
28
+ else if (agentType === 'grok') {
29
+ return normalizeGrok(raw);
30
+ }
31
+ else if (agentType === 'antigravity') {
32
+ return normalizeAntigravity(raw);
33
+ }
28
34
  const timestamp = new Date().toISOString();
29
35
  return [{
30
36
  type: raw.type || 'unknown',
@@ -824,6 +830,150 @@ function normalizeOpencode(raw) {
824
830
  timestamp: timestamp,
825
831
  }];
826
832
  }
833
+ // --- Grok parsing ---
834
+ // Grok's streaming-json mode emits one JSON object per token, with three event
835
+ // types:
836
+ // {"type":"thought","data":"<chunk>"} — reasoning tokens (many, small)
837
+ // {"type":"text","data":"<chunk>"} — visible response tokens (many, small)
838
+ // {"type":"end","stopReason":"EndTurn","sessionId":"<uuid>","requestId":"<uuid>"}
839
+ //
840
+ // Tool calls are NOT exposed as separate events in this format; they appear
841
+ // inside the `thought` text as XML-like markup. Extracting them reliably would
842
+ // require running a streaming XML/markup parser over concatenated thought
843
+ // chunks, which is out of scope for v1. The teams summary will show grok
844
+ // teammates' bash/file ops as empty — known limitation, fixable later by
845
+ // switching to grok's `agent` subcommand (richer event stream) once stable.
846
+ //
847
+ // Tokens are emitted as `message` events with `complete: false` so the
848
+ // summarizer can concatenate them into a final message; `thinking` events are
849
+ // already collapsed by the summarizer's groupAndFlattenEvents pathway.
850
+ function normalizeGrok(raw) {
851
+ if (!raw || typeof raw !== 'object') {
852
+ return [{
853
+ type: 'unknown',
854
+ agent: 'grok',
855
+ raw: raw,
856
+ timestamp: new Date().toISOString(),
857
+ }];
858
+ }
859
+ const eventType = raw.type || 'unknown';
860
+ const timestamp = new Date().toISOString();
861
+ if (eventType === 'thought') {
862
+ const data = typeof raw.data === 'string' ? raw.data : '';
863
+ if (!data)
864
+ return [];
865
+ return [{
866
+ type: 'thinking',
867
+ agent: 'grok',
868
+ content: data,
869
+ timestamp: timestamp,
870
+ }];
871
+ }
872
+ if (eventType === 'text') {
873
+ const data = typeof raw.data === 'string' ? raw.data : '';
874
+ if (!data)
875
+ return [];
876
+ return [{
877
+ type: 'message',
878
+ agent: 'grok',
879
+ content: data,
880
+ complete: false,
881
+ timestamp: timestamp,
882
+ }];
883
+ }
884
+ if (eventType === 'end') {
885
+ const stopReason = typeof raw.stopReason === 'string' ? raw.stopReason : '';
886
+ const status = stopReason === 'EndTurn' || stopReason === 'StopSequence' || stopReason === ''
887
+ ? 'success'
888
+ : 'error';
889
+ return [{
890
+ type: 'result',
891
+ agent: 'grok',
892
+ status: status,
893
+ stop_reason: stopReason || null,
894
+ session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
895
+ timestamp: timestamp,
896
+ }];
897
+ }
898
+ return [{
899
+ type: eventType,
900
+ agent: 'grok',
901
+ raw: raw,
902
+ timestamp: timestamp,
903
+ }];
904
+ }
905
+ // --- Antigravity parsing ---
906
+ // Intentionally conservative. Antigravity's `agy` binary advertises an
907
+ // `--output-format json` flag in its docs, but the released binary errors with
908
+ // `flags provided but not defined: -output-format` (tracked upstream as
909
+ // google-antigravity/antigravity-cli#7, open as of May 2026). Until JSON
910
+ // streaming stabilizes, this parser treats agy output as a black box:
911
+ // - non-object input (a plain string line, or null/number) becomes a single
912
+ // `message` event with the full content and complete:true so the
913
+ // summarizer captures it without token-level concatenation
914
+ // - objects with a recognizable `type` field (e.g. `init`, `message`,
915
+ // `result`) get a minimal shape-preserving normalization
916
+ // - everything else falls through to the generic unknown-event shape
917
+ // Once agy ships stable streaming JSON, replace this with proper event
918
+ // mapping mirroring normalizeGrok / normalizeClaude.
919
+ function normalizeAntigravity(raw) {
920
+ const timestamp = new Date().toISOString();
921
+ if (typeof raw === 'string') {
922
+ if (!raw)
923
+ return [];
924
+ return [{
925
+ type: 'message',
926
+ agent: 'antigravity',
927
+ content: raw,
928
+ complete: true,
929
+ timestamp: timestamp,
930
+ }];
931
+ }
932
+ if (!raw || typeof raw !== 'object') {
933
+ return [{
934
+ type: 'unknown',
935
+ agent: 'antigravity',
936
+ raw: raw,
937
+ timestamp: timestamp,
938
+ }];
939
+ }
940
+ const eventType = raw.type || 'unknown';
941
+ if (eventType === 'init') {
942
+ return [{
943
+ type: 'init',
944
+ agent: 'antigravity',
945
+ session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
946
+ timestamp: timestamp,
947
+ }];
948
+ }
949
+ if (eventType === 'message') {
950
+ const content = typeof raw.content === 'string' ? raw.content : '';
951
+ if (!content)
952
+ return [];
953
+ return [{
954
+ type: 'message',
955
+ agent: 'antigravity',
956
+ content: content,
957
+ complete: raw.complete !== false,
958
+ timestamp: timestamp,
959
+ }];
960
+ }
961
+ if (eventType === 'result') {
962
+ return [{
963
+ type: 'result',
964
+ agent: 'antigravity',
965
+ status: raw.status === 'error' ? 'error' : 'success',
966
+ session_id: typeof raw.sessionId === 'string' ? raw.sessionId : null,
967
+ timestamp: timestamp,
968
+ }];
969
+ }
970
+ return [{
971
+ type: eventType,
972
+ agent: 'antigravity',
973
+ raw: raw,
974
+ timestamp: timestamp,
975
+ }];
976
+ }
827
977
  /** Parse a single JSONL line into normalized events. Returns null if the line is not valid JSON. */
828
978
  export function parseEvent(agentType, line) {
829
979
  try {
@@ -159,11 +159,18 @@ export function groupAndFlattenEvents(events) {
159
159
  if (eventType === 'message' || eventType === 'thinking') {
160
160
  let count = 1;
161
161
  let combinedContent = event.content || '';
162
+ // Streaming token events (complete:false) get concatenated without a
163
+ // separator so tokens reassemble into readable prose. Whole-turn events
164
+ // get joined with newlines so distinct turns/thoughts stay separated.
165
+ const isStreaming = event.complete === false;
162
166
  let j = i + 1;
163
167
  while (j < events.length && events[j].type === eventType) {
164
168
  count++;
165
169
  if (events[j].content) {
166
- combinedContent += (combinedContent ? '\n' : '') + events[j].content;
170
+ const sep = isStreaming || events[j].complete === false
171
+ ? ''
172
+ : (combinedContent ? '\n' : '');
173
+ combinedContent += sep + events[j].content;
167
174
  }
168
175
  j++;
169
176
  }
@@ -405,7 +412,16 @@ export function summarizeEvents(agentId, agentType, status, events, duration = n
405
412
  else if (eventType === 'message') {
406
413
  const content = event.content || '';
407
414
  if (content) {
408
- summary.finalMessage = content;
415
+ // Streaming token-by-token messages (e.g., grok) arrive as many small
416
+ // `message` events with complete:false; concatenate them so the final
417
+ // turn reads as one message. Whole-turn messages (claude, codex,
418
+ // gemini) keep their complete:true semantics — last one wins.
419
+ if (event.complete === false) {
420
+ summary.finalMessage = (summary.finalMessage || '') + content;
421
+ }
422
+ else {
423
+ summary.finalMessage = content;
424
+ }
409
425
  }
410
426
  }
411
427
  else if (eventType === 'error') {
@@ -8,7 +8,9 @@ import { execFile } from 'child_process';
8
8
  import { promisify } from 'util';
9
9
  import * as fs from 'fs/promises';
10
10
  import * as path from 'path';
11
+ import { safeJoin } from '../paths.js';
11
12
  const execFileAsync = promisify(execFile);
13
+ const WORKTREE_NAME_RE = /^[A-Za-z0-9_-]+$/;
12
14
  export async function isGitRepo(dir) {
13
15
  try {
14
16
  await execFileAsync('git', ['rev-parse', '--git-dir'], { cwd: dir });
@@ -42,8 +44,11 @@ export async function hasUncommittedChanges(worktreePath) {
42
44
  * @returns The absolute path to the created worktree
43
45
  */
44
46
  export async function createWorktree(repoDir, worktreeName) {
47
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
48
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
49
+ }
45
50
  const gitRoot = await getGitRoot(repoDir);
46
- const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
51
+ const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
47
52
  const branchName = `agents/${worktreeName}`;
48
53
  await fs.mkdir(path.dirname(worktreePath), { recursive: true });
49
54
  await execFileAsync('git', ['worktree', 'add', '-b', branchName, worktreePath, 'HEAD'], {
@@ -59,8 +64,11 @@ export async function createWorktree(repoDir, worktreeName) {
59
64
  * @param deleteBranch - Whether to delete the associated branch
60
65
  */
61
66
  export async function removeWorktree(repoDir, worktreeName, deleteBranch = true) {
67
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
68
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
69
+ }
62
70
  const gitRoot = await getGitRoot(repoDir);
63
- const worktreePath = path.join(gitRoot, '.agents', 'worktrees', worktreeName);
71
+ const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
64
72
  const branchName = `agents/${worktreeName}`;
65
73
  try {
66
74
  await execFileAsync('git', ['worktree', 'remove', '--force', worktreePath], { cwd: gitRoot });
@@ -86,7 +94,10 @@ export async function removeWorktree(repoDir, worktreeName, deleteBranch = true)
86
94
  * Get the worktree path for a given name.
87
95
  */
88
96
  export function getWorktreePath(gitRoot, worktreeName) {
89
- return path.join(gitRoot, '.agents', 'worktrees', worktreeName);
97
+ if (!WORKTREE_NAME_RE.test(worktreeName)) {
98
+ throw new Error(`Invalid worktree name: ${worktreeName}`);
99
+ }
100
+ return safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
90
101
  }
91
102
  /**
92
103
  * Get the branch name for a worktree.
@@ -40,6 +40,12 @@ export interface AgentConfig {
40
40
  skills: Capability;
41
41
  commands: Capability;
42
42
  plugins: Capability;
43
+ /**
44
+ * Permission modes this agent natively supports. Modes outside this set
45
+ * are gated by buildExecCommand: `auto` silently degrades to `edit`,
46
+ * `skip` errors with a clear message naming the supported modes.
47
+ */
48
+ modes: Mode[];
43
49
  /**
44
50
  * Whether the agent natively resolves `@path/to/file` imports inside its
45
51
  * rules file at session start. If false, agents-cli must pre-compile the
@@ -60,6 +66,19 @@ export type Capability = boolean | {
60
66
  };
61
67
  /** Names of every gateable capability on AgentConfig. */
62
68
  export type CapabilityName = 'hooks' | 'mcp' | 'allowlist' | 'skills' | 'commands' | 'plugins';
69
+ /**
70
+ * Permission modes controlling agent autonomy.
71
+ * plan read-only investigation; no writes, no shell side-effects
72
+ * edit may edit files; prompts for shell/risky operations
73
+ * auto smart classifier auto-approves safe operations, prompts for risky ones
74
+ * skip bypasses every permission prompt (dangerously-skip-permissions)
75
+ *
76
+ * `full` is accepted as a permanent silent alias for `skip` via normalizeMode().
77
+ * Per-agent support is declared on AgentConfig.capabilities.modes.
78
+ */
79
+ export type Mode = 'plan' | 'edit' | 'auto' | 'skip';
80
+ /** Every canonical mode in declaration order. Useful for iteration / validation. */
81
+ export declare const ALL_MODES: readonly Mode[];
63
82
  /** Reason a capability check failed. */
64
83
  export type CapabilityFailReason = 'unsupported' | 'too_old' | 'too_new';
65
84
  /** Result of `supports(agent, cap, version?)`. */
@@ -100,6 +119,36 @@ export interface HookMatches {
100
119
  cwd_includes?: string | string[];
101
120
  project_has?: string;
102
121
  }
122
+ /**
123
+ * Cache scoping. Determines which cache file a hook invocation reads/writes:
124
+ * - `global` one file per hook, shared across cwds/sessions. Right for
125
+ * SessionStart hooks pulling org-wide context (Linear sprint).
126
+ * - `per-cwd` keyed on the working directory the hook fires from.
127
+ * - `per-session` keyed on the agent's session_id (read from stdin JSON).
128
+ * - `per-project` keyed on the nearest git repo root above cwd.
129
+ */
130
+ export type HookCacheKey = 'global' | 'per-cwd' | 'per-session' | 'per-project';
131
+ /** Prefetch strategy when the cache is stale. */
132
+ export type HookCachePrefetch = 'none' | 'background';
133
+ /**
134
+ * Full hook cache config. Authors usually use the shorthand string form
135
+ * (`HookCache`) below. Shorthand examples in hooks.yaml:
136
+ *
137
+ * cache: 5m # → { ttl: 300, key: 'global', prefetch: 'none' }
138
+ * cache: 5m-bg # → { ttl: 300, key: 'global', prefetch: 'background' }
139
+ * cache: # full form
140
+ * ttl: 1h
141
+ * key: per-cwd
142
+ * prefetch: background
143
+ */
144
+ export interface HookCacheConfig {
145
+ /** TTL in seconds or duration string ("30s", "5m", "1h"). */
146
+ ttl: number | string;
147
+ key?: HookCacheKey;
148
+ prefetch?: HookCachePrefetch;
149
+ }
150
+ /** Cache shorthand: duration string, optionally suffixed `-bg` for background prefetch. */
151
+ export type HookCache = string | HookCacheConfig;
103
152
  /** Hook entry as declared in a package manifest (agents.yaml). */
104
153
  export interface ManifestHook {
105
154
  script: string;
@@ -114,6 +163,13 @@ export interface ManifestHook {
114
163
  override?: boolean;
115
164
  /** Optional pre-filter predicates evaluated before invoking the script. */
116
165
  matches?: HookMatches;
166
+ /**
167
+ * Opt-in caching. When set, the registrar generates a per-hook shim
168
+ * under the hook shims dir that handles cache lookup, stale-while-revalidate,
169
+ * and per-invocation timing/logging, then registers that shim with the agent
170
+ * instead of the raw script. The underlying script is unchanged.
171
+ */
172
+ cache?: HookCache;
117
173
  }
118
174
  /** Lightweight hook descriptor used in resource listings. */
119
175
  export interface HookResourceEntry {
@@ -203,9 +259,12 @@ export interface RegistryConfig {
203
259
  /** Built-in registry endpoints shipped with agents-cli. */
204
260
  export declare const DEFAULT_REGISTRIES: Record<RegistryType, Record<string, RegistryConfig>>;
205
261
  /**
206
- * Registries that ship pre-seeded into new users' agents.yaml once, but are
207
- * not "defaults" — after seeding they behave like any user-added registry
208
- * (listable, disable-able, removable, and gone for good once removed).
262
+ * Third-party registries pre-seeded on first install for discoverability.
263
+ *
264
+ * These ship into new users' agents.yaml once, but are not "defaults" after
265
+ * seeding they behave like any user-added registry (listable, disable-able,
266
+ * removable). Removed users can `agents registry remove <name>` to opt out;
267
+ * once removed they don't come back.
209
268
  */
210
269
  export declare const SEEDED_REGISTRIES: Record<RegistryType, Record<string, RegistryConfig>>;
211
270
  /** A single installable package within an MCP server entry. */
@@ -466,7 +525,7 @@ export interface BrowserProfileConfig {
466
525
  };
467
526
  /** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
468
527
  logDir?: string;
469
- /** Optional SSH host where logDir lives, e.g. "user@mac-mini". */
528
+ /** Optional SSH host where logDir lives, e.g. "user@remote-host". */
470
529
  logHost?: string;
471
530
  }
472
531
  /** Options controlling which agents and resources are synced during `agents pull` / `agents use`. */
package/dist/lib/types.js CHANGED
@@ -5,6 +5,8 @@
5
5
  * configuration schemas, resource tracking, registry types, and permission
6
6
  * formats for each supported agent.
7
7
  */
8
+ /** Every canonical mode in declaration order. Useful for iteration / validation. */
9
+ export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
8
10
  /** Canonical system repo cloned into ~/.agents-system/. */
9
11
  export const DEFAULT_SYSTEM_REPO = 'gh:phnx-labs/.agents-system';
10
12
  /** Strip the `gh:` prefix and `.git` suffix to get a GitHub `owner/repo` slug. */
@@ -22,9 +24,12 @@ export const DEFAULT_REGISTRIES = {
22
24
  skill: {},
23
25
  };
24
26
  /**
25
- * Registries that ship pre-seeded into new users' agents.yaml once, but are
26
- * not "defaults" — after seeding they behave like any user-added registry
27
- * (listable, disable-able, removable, and gone for good once removed).
27
+ * Third-party registries pre-seeded on first install for discoverability.
28
+ *
29
+ * These ship into new users' agents.yaml once, but are not "defaults" after
30
+ * seeding they behave like any user-added registry (listable, disable-able,
31
+ * removable). Removed users can `agents registry remove <name>` to opt out;
32
+ * once removed they don't come back.
28
33
  */
29
34
  export const SEEDED_REGISTRIES = {
30
35
  mcp: {},
@@ -71,12 +71,37 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Pr
71
71
  usageByKey: Map<string, UsageInfo>;
72
72
  }>;
73
73
  /**
74
- * Fetch usage for a single identity, with a 2-minute cache fast path.
75
- * Falls back to cached data when the live fetch fails.
74
+ * Fetch usage for a single identity using stale-while-revalidate.
75
+ *
76
+ * - Cache fresh (< 2 min): return cached snapshot, NO network.
77
+ * - Cache stale but < 24h: return cached snapshot instantly, fire background refresh.
78
+ * - Cache too stale or absent: block on live fetch, fall back to cache on error.
79
+ *
80
+ * This keeps `agents run` startup off the network on the hot path. The first
81
+ * invocation after a cold install or 24h gap still blocks once to seed the
82
+ * cache; every run after that returns instantly while the cache silently
83
+ * refreshes in the background.
76
84
  */
77
85
  export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Promise<UsageInfo>;
78
86
  /** Format a one-line usage summary with compact bars for inline display. */
79
87
  export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
88
+ /**
89
+ * Compact colored badge for the account's overall usage status. Renders only
90
+ * when the account is throttled — `available` and `null` return ''.
91
+ *
92
+ * - `out_of_credits` → red "out of credits" (terminal account, all buckets dry)
93
+ * - `rate_limited` → yellow "rate-limited" (transient throttling)
94
+ *
95
+ * The badge sits between the usage bars and `lastActive` in `agents view`, so
96
+ * a glance at the row tells the user whether the version can do useful work.
97
+ * The same signal is exposed as `usageStatus` in `agents view --json` for
98
+ * programmatic consumers (e.g. the swarmify panel's "resume in healthy agent").
99
+ *
100
+ * The switch is exhaustive on purpose — adding a new `AccountInfo.usageStatus`
101
+ * value without updating the cases here is a build error at `_exhaustive`,
102
+ * which is exactly the bug class this PR is fixing.
103
+ */
104
+ export declare function formatUsageStatusBadge(usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null | undefined): string;
80
105
  /** Format a multi-line usage section for detailed agent views. */
81
106
  export declare function formatUsageSection(usage: UsageInfo): string[];
82
107
  /** Load Claude OAuth credentials from the system keychain/keyring. */
package/dist/lib/usage.js CHANGED
@@ -96,42 +96,95 @@ export async function getUsageInfoByIdentity(inputs) {
96
96
  usageByKey: new Map(usageResults.map(({ key, usage }) => [key, usage])),
97
97
  };
98
98
  }
99
- const USAGE_CACHE_FRESH_MS = 2 * 60 * 1000; // 2 minutes
99
+ const USAGE_CACHE_FRESH_MS = 2 * 60 * 1000; // 2 minutes — fresh window: don't refresh.
100
+ const USAGE_CACHE_SWR_MS = 24 * 60 * 60 * 1000; // 24 hours — beyond this, block on live fetch.
101
+ /** In-process dedup: don't fire concurrent background refreshes for the same identity. */
102
+ const inFlightRefreshes = new Map();
100
103
  /**
101
- * Fetch usage for a single identity, with a 2-minute cache fast path.
102
- * Falls back to cached data when the live fetch fails.
104
+ * Fetch usage for a single identity using stale-while-revalidate.
105
+ *
106
+ * - Cache fresh (< 2 min): return cached snapshot, NO network.
107
+ * - Cache stale but < 24h: return cached snapshot instantly, fire background refresh.
108
+ * - Cache too stale or absent: block on live fetch, fall back to cache on error.
109
+ *
110
+ * This keeps `agents run` startup off the network on the hot path. The first
111
+ * invocation after a cold install or 24h gap still blocks once to seed the
112
+ * cache; every run after that returns instantly while the cache silently
113
+ * refreshes in the background.
103
114
  */
104
115
  export async function getUsageInfoForIdentity(input) {
105
116
  const usageKey = getUsageLookupKey(input.info);
106
- // Fast path: serve from cache if fresh. Skips the network call entirely.
107
- if (input.agentId === 'claude' && usageKey) {
108
- const cached = readClaudeUsageCache(usageKey);
109
- if (cached?.capturedAt) {
110
- const ageMs = Date.now() - cached.capturedAt.getTime();
111
- if (ageMs < USAGE_CACHE_FRESH_MS) {
112
- return { snapshot: cached, error: null };
113
- }
114
- }
117
+ // Non-Claude or no identity: legacy path, blocking fetch.
118
+ if (input.agentId !== 'claude' || !usageKey) {
119
+ return getUsageInfo(input.agentId, {
120
+ home: input.home,
121
+ cliVersion: input.cliVersion,
122
+ organizationId: input.info.organizationId,
123
+ });
115
124
  }
116
- // Cache miss or stale -- make the network call.
125
+ const cached = readClaudeUsageCache(usageKey);
126
+ const ageMs = cached?.capturedAt ? Date.now() - cached.capturedAt.getTime() : Infinity;
127
+ // Fresh: cache is recent enough, skip network entirely.
128
+ if (cached && ageMs < USAGE_CACHE_FRESH_MS) {
129
+ return { snapshot: cached, error: null };
130
+ }
131
+ // Stale-while-revalidate: cache exists and isn't ancient, return it now and
132
+ // refresh in the background so the next invocation has fresh data.
133
+ if (cached && ageMs < USAGE_CACHE_SWR_MS) {
134
+ triggerBackgroundUsageRefresh(input, usageKey);
135
+ return { snapshot: cached, error: null };
136
+ }
137
+ // Cold cache or > 24h old: block on live fetch.
117
138
  const usage = await getUsageInfo(input.agentId, {
118
139
  home: input.home,
119
140
  cliVersion: input.cliVersion,
120
141
  organizationId: input.info.organizationId,
121
142
  });
122
- if (input.agentId !== 'claude' || !usageKey) {
123
- return usage;
124
- }
125
143
  if (usage.snapshot?.source === 'live') {
126
144
  writeClaudeUsageCache(usageKey, usage.snapshot);
127
145
  return usage;
128
146
  }
129
- const cached = readClaudeUsageCache(usageKey);
147
+ // Live fetch failed — last-resort fallback to whatever cache we had.
130
148
  if (cached) {
131
149
  return { snapshot: cached, error: usage.error };
132
150
  }
133
151
  return usage;
134
152
  }
153
+ /**
154
+ * Kick off a background refresh of the usage cache. Errors are swallowed —
155
+ * a failed background refresh leaves the existing cache in place for the
156
+ * next invocation. The work is deferred to a future event-loop tick via
157
+ * `setImmediate` because some of the call chain (loadClaudeOauth →
158
+ * getKeychainToken → execFileSync) does synchronous I/O even though the
159
+ * functions are declared `async`. Without the defer, that sync I/O blocks
160
+ * the SWR caller and defeats the whole point of returning the cache instantly.
161
+ */
162
+ function triggerBackgroundUsageRefresh(input, usageKey) {
163
+ if (inFlightRefreshes.has(usageKey))
164
+ return;
165
+ const promise = new Promise((resolve) => {
166
+ setImmediate(async () => {
167
+ try {
168
+ const usage = await getUsageInfo(input.agentId, {
169
+ home: input.home,
170
+ cliVersion: input.cliVersion,
171
+ organizationId: input.info.organizationId,
172
+ });
173
+ if (usage.snapshot?.source === 'live') {
174
+ writeClaudeUsageCache(usageKey, usage.snapshot);
175
+ }
176
+ }
177
+ catch {
178
+ /* background refresh failed — leave existing cache in place */
179
+ }
180
+ finally {
181
+ inFlightRefreshes.delete(usageKey);
182
+ resolve();
183
+ }
184
+ });
185
+ });
186
+ inFlightRefreshes.set(usageKey, promise);
187
+ }
135
188
  /** Format a one-line usage summary with compact bars for inline display. */
136
189
  export function formatUsageSummary(plan, snapshot, planWidth = 3) {
137
190
  const parts = [];
@@ -148,6 +201,36 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
148
201
  }
149
202
  return parts.join(' ');
150
203
  }
204
+ /**
205
+ * Compact colored badge for the account's overall usage status. Renders only
206
+ * when the account is throttled — `available` and `null` return ''.
207
+ *
208
+ * - `out_of_credits` → red "out of credits" (terminal account, all buckets dry)
209
+ * - `rate_limited` → yellow "rate-limited" (transient throttling)
210
+ *
211
+ * The badge sits between the usage bars and `lastActive` in `agents view`, so
212
+ * a glance at the row tells the user whether the version can do useful work.
213
+ * The same signal is exposed as `usageStatus` in `agents view --json` for
214
+ * programmatic consumers (e.g. the swarmify panel's "resume in healthy agent").
215
+ *
216
+ * The switch is exhaustive on purpose — adding a new `AccountInfo.usageStatus`
217
+ * value without updating the cases here is a build error at `_exhaustive`,
218
+ * which is exactly the bug class this PR is fixing.
219
+ */
220
+ export function formatUsageStatusBadge(usageStatus) {
221
+ if (usageStatus === null || usageStatus === undefined)
222
+ return '';
223
+ switch (usageStatus) {
224
+ case 'available': return '';
225
+ case 'out_of_credits': return chalk.red('out of credits');
226
+ case 'rate_limited': return chalk.yellow('rate-limited');
227
+ default: {
228
+ const _exhaustive = usageStatus;
229
+ void _exhaustive;
230
+ return '';
231
+ }
232
+ }
233
+ }
151
234
  /** Format a multi-line usage section for detailed agent views. */
152
235
  export function formatUsageSection(usage) {
153
236
  if (!usage.snapshot && !usage.error) {