@phnx-labs/agents-cli 1.21.1 → 1.21.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 (111) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +1 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +34 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/doctor.js +5 -2
  8. package/dist/commands/feed.js +28 -19
  9. package/dist/commands/hooks.js +9 -45
  10. package/dist/commands/menubar.js +24 -24
  11. package/dist/commands/message.js +23 -3
  12. package/dist/commands/perf.d.ts +29 -0
  13. package/dist/commands/perf.js +90 -23
  14. package/dist/commands/projects.d.ts +21 -0
  15. package/dist/commands/projects.js +169 -23
  16. package/dist/commands/routines.js +46 -1
  17. package/dist/commands/sessions-picker.js +17 -2
  18. package/dist/commands/sessions.d.ts +5 -0
  19. package/dist/commands/sessions.js +14 -0
  20. package/dist/commands/ssh.js +69 -0
  21. package/dist/commands/trends.d.ts +2 -0
  22. package/dist/commands/trends.js +158 -0
  23. package/dist/commands/usage.d.ts +4 -4
  24. package/dist/commands/view.d.ts +6 -0
  25. package/dist/commands/view.js +90 -45
  26. package/dist/index.js +22 -1
  27. package/dist/lib/agents.js +2 -2
  28. package/dist/lib/analytics/dashboard.d.ts +11 -0
  29. package/dist/lib/analytics/dashboard.js +31 -0
  30. package/dist/lib/analytics/recipes.d.ts +32 -0
  31. package/dist/lib/analytics/recipes.js +316 -0
  32. package/dist/lib/analytics/usage-db.d.ts +84 -0
  33. package/dist/lib/analytics/usage-db.js +301 -0
  34. package/dist/lib/browser/service.js +31 -0
  35. package/dist/lib/cli-resources.d.ts +20 -0
  36. package/dist/lib/cli-resources.js +48 -1
  37. package/dist/lib/computer/dispatch.d.ts +3 -1
  38. package/dist/lib/computer/dispatch.js +10 -2
  39. package/dist/lib/daemon.js +51 -14
  40. package/dist/lib/devices/health-report.d.ts +5 -0
  41. package/dist/lib/devices/health-report.js +3 -0
  42. package/dist/lib/event-stream.d.ts +2 -0
  43. package/dist/lib/event-stream.js +3 -0
  44. package/dist/lib/events.d.ts +3 -1
  45. package/dist/lib/events.js +4 -2
  46. package/dist/lib/feed-broadcast.d.ts +52 -7
  47. package/dist/lib/feed-broadcast.js +125 -18
  48. package/dist/lib/fleet-cache.d.ts +37 -0
  49. package/dist/lib/fleet-cache.js +40 -0
  50. package/dist/lib/fleet-status.d.ts +53 -0
  51. package/dist/lib/fleet-status.js +120 -0
  52. package/dist/lib/friction-heuristics.d.ts +32 -0
  53. package/dist/lib/friction-heuristics.js +47 -0
  54. package/dist/lib/git.d.ts +14 -0
  55. package/dist/lib/git.js +36 -0
  56. package/dist/lib/hooks/cache.js +28 -6
  57. package/dist/lib/hooks/profile.d.ts +8 -0
  58. package/dist/lib/hooks/profile.js +15 -18
  59. package/dist/lib/hooks.js +72 -17
  60. package/dist/lib/linear-cache.d.ts +63 -0
  61. package/dist/lib/linear-cache.js +146 -0
  62. package/dist/lib/linear-project-counts.d.ts +35 -5
  63. package/dist/lib/linear-project-counts.js +61 -16
  64. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  65. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
  66. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  67. package/dist/lib/menubar/install-menubar.d.ts +7 -0
  68. package/dist/lib/menubar/install-menubar.js +36 -6
  69. package/dist/lib/percentile.d.ts +12 -0
  70. package/dist/lib/percentile.js +24 -0
  71. package/dist/lib/perf/db.d.ts +7 -3
  72. package/dist/lib/perf/db.js +37 -19
  73. package/dist/lib/perf/types.d.ts +10 -0
  74. package/dist/lib/plugins.js +12 -1
  75. package/dist/lib/project-doctor.d.ts +36 -0
  76. package/dist/lib/project-doctor.js +45 -0
  77. package/dist/lib/project-import.d.ts +11 -1
  78. package/dist/lib/project-import.js +17 -3
  79. package/dist/lib/project-status.d.ts +25 -5
  80. package/dist/lib/project-status.js +48 -6
  81. package/dist/lib/resources.d.ts +16 -0
  82. package/dist/lib/resources.js +25 -14
  83. package/dist/lib/rotate.d.ts +27 -0
  84. package/dist/lib/rotate.js +44 -17
  85. package/dist/lib/routines.d.ts +16 -0
  86. package/dist/lib/routines.js +26 -0
  87. package/dist/lib/runner.js +41 -0
  88. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  89. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  90. package/dist/lib/secrets/usage-db.d.ts +3 -63
  91. package/dist/lib/secrets/usage-db.js +46 -186
  92. package/dist/lib/session/db.d.ts +17 -1
  93. package/dist/lib/session/db.js +240 -6
  94. package/dist/lib/session/discover.d.ts +31 -0
  95. package/dist/lib/session/discover.js +50 -1
  96. package/dist/lib/session/highlights.d.ts +24 -4
  97. package/dist/lib/session/highlights.js +52 -7
  98. package/dist/lib/session/parse.js +19 -3
  99. package/dist/lib/session/prompt.d.ts +19 -0
  100. package/dist/lib/session/prompt.js +43 -0
  101. package/dist/lib/session/types.d.ts +43 -0
  102. package/dist/lib/startup/command-registry.d.ts +1 -0
  103. package/dist/lib/startup/command-registry.js +2 -0
  104. package/dist/lib/state.d.ts +31 -3
  105. package/dist/lib/state.js +53 -10
  106. package/dist/lib/types.d.ts +22 -4
  107. package/dist/lib/usage-refresh.d.ts +106 -0
  108. package/dist/lib/usage-refresh.js +238 -0
  109. package/dist/lib/usage.d.ts +152 -17
  110. package/dist/lib/usage.js +393 -79
  111. package/package.json +1 -1
@@ -17,12 +17,32 @@ export interface SkillUse {
17
17
  count: number;
18
18
  }
19
19
  /**
20
- * Skills invoked during the session, from `Skill` tool calls. Claude and Kimi
21
- * both name the tool `Skill` and carry the skill id in `args.skill` (plugin
22
- * skills surface here too — a plugin-provided skill is invoked through the
23
- * same tool). Sorted by count desc, then name.
20
+ * True for a tool_use event that is that harness's skill-invocation call (see
21
+ * {@link SKILL_TOOL_NAME_BY_AGENT}). Exported so an INCREMENTAL parser
22
+ * (discover.ts's ClaudeParseState/foldDerivedToolState) can select matching
23
+ * events into a small held array as it streams, then run {@link extractSkills}
24
+ * over just that subset at finalize — without re-parsing the whole transcript.
25
+ */
26
+ export declare function isSkillInvocation(e: SessionEvent): boolean;
27
+ /**
28
+ * Skills invoked during the session, from that harness's skill-invocation
29
+ * tool calls (see {@link SKILL_TOOL_NAME_BY_AGENT}). Carries the skill id in
30
+ * `args.skill` (or `args.name`). Sorted by count desc, then name.
24
31
  */
25
32
  export declare function extractSkills(events: SessionEvent[]): SkillUse[];
33
+ export interface SlashCommandUse {
34
+ /** WITH the leading slash, e.g. `/recap`, `/code:commit` — matches SessionEvent.slashCommand's shape. */
35
+ name: string;
36
+ count: number;
37
+ }
38
+ /**
39
+ * Slash commands invoked during the session — either the user typing one
40
+ * (Claude's `<command-name>` wrapper) or the model invoking one via the
41
+ * `SlashCommand` tool (`SessionEvent.slashCommand`, populated by
42
+ * `parseClaudeContent` for both sources — see session/prompt.ts). Sorted by
43
+ * count desc, then name.
44
+ */
45
+ export declare function extractSlashCommands(events: SessionEvent[]): SlashCommandUse[];
26
46
  export interface HookUse {
27
47
  /** Hook name as configured, e.g. `SessionStart:startup`. */
28
48
  name: string;
@@ -15,17 +15,44 @@ import os from 'node:os';
15
15
  import path from 'node:path';
16
16
  import { isNoisePath } from './digest.js';
17
17
  /**
18
- * Skills invoked during the session, from `Skill` tool calls. Claude and Kimi
19
- * both name the tool `Skill` and carry the skill id in `args.skill` (plugin
20
- * skills surface here too a plugin-provided skill is invoked through the
21
- * same tool). Sorted by count desc, then name.
18
+ * The tool name a harness uses to invoke a skill, keyed by `SessionAgentId` —
19
+ * a registry, not a single hardcoded string, so a harness whose transcript
20
+ * names the tool differently is one table row away from support instead of a
21
+ * near-identical `if` arm (see CLAUDE.md's registry-over-if-chain convention).
22
+ *
23
+ * Verified entries only: Claude and Kimi both empirically name the tool
24
+ * `Skill` (a plugin-provided skill rides the same tool). No other harness has
25
+ * a confirmed skill-invocation tool name in this codebase's fixtures — an
26
+ * absent entry means "we don't know yet", not "this harness has no skills";
27
+ * extractSkills yields `[]` for it rather than guessing a name and silently
28
+ * mismatching (or worse, matching a coincidental tool with the same name).
29
+ */
30
+ const SKILL_TOOL_NAME_BY_AGENT = {
31
+ claude: 'Skill',
32
+ kimi: 'Skill',
33
+ };
34
+ /**
35
+ * True for a tool_use event that is that harness's skill-invocation call (see
36
+ * {@link SKILL_TOOL_NAME_BY_AGENT}). Exported so an INCREMENTAL parser
37
+ * (discover.ts's ClaudeParseState/foldDerivedToolState) can select matching
38
+ * events into a small held array as it streams, then run {@link extractSkills}
39
+ * over just that subset at finalize — without re-parsing the whole transcript.
40
+ */
41
+ export function isSkillInvocation(e) {
42
+ if (e.type !== 'tool_use' || e._local)
43
+ return false;
44
+ const skillTool = SKILL_TOOL_NAME_BY_AGENT[e.agent];
45
+ return !!skillTool && e.tool === skillTool;
46
+ }
47
+ /**
48
+ * Skills invoked during the session, from that harness's skill-invocation
49
+ * tool calls (see {@link SKILL_TOOL_NAME_BY_AGENT}). Carries the skill id in
50
+ * `args.skill` (or `args.name`). Sorted by count desc, then name.
22
51
  */
23
52
  export function extractSkills(events) {
24
53
  const counts = new Map();
25
54
  for (const e of events) {
26
- if (e.type !== 'tool_use' || e._local)
27
- continue;
28
- if (e.tool !== 'Skill')
55
+ if (!isSkillInvocation(e))
29
56
  continue;
30
57
  const name = e.args?.skill ?? e.args?.name;
31
58
  if (typeof name !== 'string' || !name.trim())
@@ -37,6 +64,24 @@ export function extractSkills(events) {
37
64
  .map(([name, count]) => ({ name, count }))
38
65
  .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
39
66
  }
67
+ /**
68
+ * Slash commands invoked during the session — either the user typing one
69
+ * (Claude's `<command-name>` wrapper) or the model invoking one via the
70
+ * `SlashCommand` tool (`SessionEvent.slashCommand`, populated by
71
+ * `parseClaudeContent` for both sources — see session/prompt.ts). Sorted by
72
+ * count desc, then name.
73
+ */
74
+ export function extractSlashCommands(events) {
75
+ const counts = new Map();
76
+ for (const e of events) {
77
+ if (!e.slashCommand)
78
+ continue;
79
+ counts.set(e.slashCommand, (counts.get(e.slashCommand) ?? 0) + 1);
80
+ }
81
+ return [...counts.entries()]
82
+ .map(([name, count]) => ({ name, count }))
83
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
84
+ }
40
85
  /**
41
86
  * Hooks that fired during the session, folded from `hook` events (parsed from
42
87
  * Claude's `hook_success`/`hook_error`/… attachment records; other harnesses
@@ -9,7 +9,7 @@ import * as fs from 'fs';
9
9
  import { truncate } from '../format.js';
10
10
  import * as path from 'path';
11
11
  import Database from '../sqlite.js';
12
- import { isSyntheticUserMessage } from './prompt.js';
12
+ import { isSyntheticUserMessage, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
13
13
  /**
14
14
  * Largest session file we will load into memory. Above this we throw a clean
15
15
  * error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
@@ -379,6 +379,14 @@ export function parseClaudeContent(content) {
379
379
  };
380
380
  if (isLocal)
381
381
  event._local = true;
382
+ // SlashCommand: the MODEL invoking a slash command programmatically
383
+ // (distinct from the <command-name> wrapper below, which is the
384
+ // USER typing one) — see prompt.ts's extractSlashCommandFromToolInput.
385
+ if (toolName === 'SlashCommand') {
386
+ const slashCommand = extractSlashCommandFromToolInput(toolInput);
387
+ if (slashCommand)
388
+ event.slashCommand = slashCommand;
389
+ }
382
390
  events.push(event);
383
391
  }
384
392
  }
@@ -403,13 +411,21 @@ export function parseClaudeContent(content) {
403
411
  // Simple user text
404
412
  const text = contentBlocks.trim();
405
413
  if (text) {
406
- events.push({
414
+ const event = {
407
415
  type: 'message',
408
416
  agent: 'claude',
409
417
  timestamp,
410
418
  role: 'user',
411
419
  content: text,
412
- });
420
+ };
421
+ // The USER typing a slash command — Claude injects a <command-name>
422
+ // wrapper as the message content (see prompt.ts's
423
+ // extractSlashCommandName; distinct from the SlashCommand tool-use
424
+ // above, which is the model invoking one programmatically).
425
+ const slashCommand = extractSlashCommandName(text);
426
+ if (slashCommand)
427
+ event.slashCommand = slashCommand;
428
+ events.push(event);
413
429
  }
414
430
  }
415
431
  else if (Array.isArray(contentBlocks)) {
@@ -12,6 +12,25 @@
12
12
  * own scaffolding at parse time simply never reach here.
13
13
  */
14
14
  export declare function isSyntheticUserMessage(raw: string | undefined): boolean;
15
+ /**
16
+ * Extract the invoked slash-command name from a raw `role=user` message, or
17
+ * undefined when the message carries no `<command-name>` wrapper. Used by
18
+ * `parseClaudeContent` to populate `SessionEvent.slashCommand`.
19
+ *
20
+ * Normalizes a leading `/` onto the result: current Claude Code wraps the
21
+ * name WITH the slash already (`/recap`), but the wrapper predates that and
22
+ * older transcripts carry the bare name (`recap`) — normalizing here means
23
+ * every consumer sees one consistent `/name` shape regardless of vintage.
24
+ */
25
+ export declare function extractSlashCommandName(raw: string | undefined): string | undefined;
26
+ /**
27
+ * Extract the invoked slash-command name from the `SlashCommand` tool's input
28
+ * — the MODEL invoking a command programmatically, not the user typing it.
29
+ * The tool's `command` field carries the full invocation text (name plus any
30
+ * args, e.g. `/code:commit fix the bug`); only the leading command token is
31
+ * the name, matching {@link extractSlashCommandName}'s shape.
32
+ */
33
+ export declare function extractSlashCommandFromToolInput(toolInput: Record<string, any> | undefined): string | undefined;
15
34
  export declare const HEADLESS_PLAN_MODE_PREFIX = "You are running in HEADLESS PLAN MODE.";
16
35
  /** Strip framework noise from a raw session prompt, returning only meaningful user text. */
17
36
  export declare function cleanSessionPrompt(raw: string): string;
@@ -63,6 +63,49 @@ export function isSyntheticUserMessage(raw) {
63
63
  return false;
64
64
  return SYNTHETIC_USER_MESSAGE_PATTERNS.some(pattern => pattern.test(raw));
65
65
  }
66
+ /**
67
+ * The `<command-name>` wrapper Claude injects into a `role=user` message when
68
+ * a typed slash command is invoked, e.g.:
69
+ * <command-message>recap</command-message>
70
+ * <command-name>/recap</command-name>
71
+ * Captures the name INCLUDING its leading slash (`/recap`), matching how the
72
+ * `SlashCommand` tool's own `command` input is formatted (see
73
+ * `extractSlashCommandFromToolInput`) so both sources land on the same shape.
74
+ */
75
+ const COMMAND_NAME_WRAPPER_RE = /<command-name>\s*([^<]*?)\s*<\/command-name>/i;
76
+ /**
77
+ * Extract the invoked slash-command name from a raw `role=user` message, or
78
+ * undefined when the message carries no `<command-name>` wrapper. Used by
79
+ * `parseClaudeContent` to populate `SessionEvent.slashCommand`.
80
+ *
81
+ * Normalizes a leading `/` onto the result: current Claude Code wraps the
82
+ * name WITH the slash already (`/recap`), but the wrapper predates that and
83
+ * older transcripts carry the bare name (`recap`) — normalizing here means
84
+ * every consumer sees one consistent `/name` shape regardless of vintage.
85
+ */
86
+ export function extractSlashCommandName(raw) {
87
+ if (!raw)
88
+ return undefined;
89
+ const m = raw.match(COMMAND_NAME_WRAPPER_RE);
90
+ const name = m?.[1]?.trim();
91
+ if (!name)
92
+ return undefined;
93
+ return name.startsWith('/') ? name : `/${name}`;
94
+ }
95
+ /**
96
+ * Extract the invoked slash-command name from the `SlashCommand` tool's input
97
+ * — the MODEL invoking a command programmatically, not the user typing it.
98
+ * The tool's `command` field carries the full invocation text (name plus any
99
+ * args, e.g. `/code:commit fix the bug`); only the leading command token is
100
+ * the name, matching {@link extractSlashCommandName}'s shape.
101
+ */
102
+ export function extractSlashCommandFromToolInput(toolInput) {
103
+ const command = toolInput?.command;
104
+ if (typeof command !== 'string')
105
+ return undefined;
106
+ const name = command.trim().split(/\s+/)[0];
107
+ return name || undefined;
108
+ }
66
109
  // Prefix prepended to every Claude-in-plan-mode team spawn prompt.
67
110
  // Ends at a blank line before the real user task.
68
111
  export const HEADLESS_PLAN_MODE_PREFIX = 'You are running in HEADLESS PLAN MODE.';
@@ -52,6 +52,15 @@ export interface SessionEvent {
52
52
  hookName?: string;
53
53
  /** Lifecycle event the hook fired on (SessionStart, PreToolUse, …). */
54
54
  hookEvent?: string;
55
+ /**
56
+ * Slash-command invocation name (e.g. `/recap`, `/code:commit`), captured
57
+ * two ways in a Claude transcript: a `role=user` message whose content is
58
+ * the `<command-name>` wrapper Claude injects for a typed slash command
59
+ * (`parseClaudeContent`, see `prompt.ts`'s `extractSlashCommandName`), or a
60
+ * `tool_use` event for the `SlashCommand` tool (a command the MODEL invoked
61
+ * programmatically, not the user). Undefined for every other event.
62
+ */
63
+ slashCommand?: string;
55
64
  }
56
65
  /** A displayable file attachment discovered in a session transcript. */
57
66
  export interface SessionAttachment {
@@ -132,6 +141,7 @@ export interface SessionMeta {
132
141
  durationMs?: number;
133
142
  /** Underlying LLM model observed in the transcript, when the agent records one. */
134
143
  model?: string;
144
+ toolCallCount?: number;
135
145
  version?: string;
136
146
  account?: string;
137
147
  topic?: string;
@@ -185,6 +195,39 @@ export interface SessionMeta {
185
195
  todos?: TodoProgress;
186
196
  /** Most-recent unique directories changed or used as a shell working directory. */
187
197
  recentDirectoriesTouched?: string[];
198
+ /**
199
+ * Skills invoked during the session (structurally identical to
200
+ * session/highlights.ts's SkillUse — declared inline here rather than
201
+ * imported, to avoid a circular import: highlights.ts imports SessionEvent
202
+ * from this file). Populated by discover.ts's incremental Claude
203
+ * accumulator (ClaudeParseState.skillEvents, run through extractSkills at
204
+ * finalize) so session/db.ts's upsertSessionsBatch can write
205
+ * session_resource_usage rows WITHOUT re-parsing the whole transcript —
206
+ * the same reason meta.todos/recentDirectoriesTouched are pre-computed by
207
+ * the caller for claude/codex instead of left to db.ts's re-parse path.
208
+ */
209
+ skillsUsed?: Array<{
210
+ name: string;
211
+ count: number;
212
+ }>;
213
+ /** Sibling of {@link skillsUsed} for slash-command invocations (SessionEvent.slashCommand). */
214
+ slashCommandsUsed?: Array<{
215
+ name: string;
216
+ count: number;
217
+ }>;
218
+ /**
219
+ * Whether this session emitted at least one `browser.navigate` /
220
+ * `browser.screenshot` event, computed at scan time from a sessionId-scoped
221
+ * read of the events log (events.ts `query({ sessionId })`) rather than a
222
+ * transcript re-scan — see `detectToolUsage` in session/db.ts. `undefined`
223
+ * means a legacy row this scanner hasn't computed the field for yet (never
224
+ * collapsed to `false`, so a consumer — e.g. sessions-picker.ts's
225
+ * `classifySessionTool` — knows to fall back to a transcript-derived guess
226
+ * instead of trusting a false negative).
227
+ */
228
+ usedBrowser?: boolean;
229
+ /** Sibling of {@link usedBrowser} for `computer.action` events. */
230
+ usedComputer?: boolean;
188
231
  /** Linear project containing ticketId, resolved lazily and cached in SQLite. */
189
232
  linearProject?: string;
190
233
  /** Browser URL for linearProject. */
@@ -71,6 +71,7 @@ export declare const loadFactory: ModuleLoader;
71
71
  export declare const loadUsage: ModuleLoader;
72
72
  export declare const loadCost: ModuleLoader;
73
73
  export declare const loadPerf: ModuleLoader;
74
+ export declare const loadTrends: ModuleLoader;
74
75
  export declare const loadOutput: ModuleLoader;
75
76
  export declare const loadBudget: ModuleLoader;
76
77
  export declare const loadAlias: ModuleLoader;
@@ -49,6 +49,7 @@ export const loadFactory = async () => (await import('../../commands/factory.js'
49
49
  export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
50
50
  export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
51
51
  export const loadPerf = async () => (await import('../../commands/perf.js')).registerPerfCommand;
52
+ export const loadTrends = async () => (await import('../../commands/trends.js')).registerTrendsCommand;
52
53
  export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
53
54
  export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
54
55
  export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
@@ -163,6 +164,7 @@ export const COMMAND_LOADERS = {
163
164
  usage: [loadUsage],
164
165
  cost: [loadCost],
165
166
  perf: [loadPerf],
167
+ trends: [loadTrends],
166
168
  output: [loadOutput],
167
169
  budget: [loadBudget],
168
170
  alias: [loadAlias],
@@ -117,6 +117,14 @@ export declare function getUserSecretsDir(): string;
117
117
  * metadata off the real session flow — not a second write path.
118
118
  */
119
119
  export declare function getSecretsDbPath(): string;
120
+ /**
121
+ * Path to the durable resource-usage warehouse (~/.agents/.history/analytics/usage.db).
122
+ * Value-free frequency/lifecycle events (secrets, agents, browser, …). Read at CALL
123
+ * time so AGENTS_USAGE_DB can redirect tests. Sync shards may also appear as
124
+ * usage.<machine-id>.db beside this default file.
125
+ */
126
+ export declare function getAnalyticsDir(): string;
127
+ export declare function getUsageDbPath(): string;
120
128
  export declare function getUserPromptcutsPath(): string;
121
129
  /** Canonical home anchor (HOME env override or os.homedir()). */
122
130
  export declare function getHomeDir(): string;
@@ -185,9 +193,20 @@ export declare function getActivityDir(): string;
185
193
  export declare function getVersionsDir(): string;
186
194
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
187
195
  export declare function getShimsDir(): string;
188
- /** Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/). */
196
+ /**
197
+ * Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/).
198
+ * Read at CALL time — since every hook now resolves through a shim (RUSH-2xxx,
199
+ * pass-through timing for matcher-only hooks), a test that registers hooks
200
+ * in-process (no subprocess HOME override) would otherwise write real shim
201
+ * files into the user's actual ~/.agents/.cache. AGENTS_HOOK_SHIMS_DIR mirrors
202
+ * the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR test-isolation escape hatches;
203
+ * never set in production code.
204
+ */
189
205
  export declare function getHookShimsDir(): string;
190
- /** Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). */
206
+ /**
207
+ * Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). Read at
208
+ * CALL time for the same reason as {@link getHookShimsDir} — see its doc.
209
+ */
191
210
  export declare function getHookCacheDir(): string;
192
211
  /** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
193
212
  export declare function getBinDir(): string;
@@ -225,11 +244,20 @@ export declare function getDevicesPendingDir(): string;
225
244
  export declare function getCloudDir(): string;
226
245
  /** Path to terminal session metadata (~/.agents/.cache/terminals/). */
227
246
  export declare function getTerminalsDir(): string;
228
- /** Path to runtime logs (~/.agents/.cache/logs/). */
247
+ /**
248
+ * Path to runtime logs (~/.agents/.cache/logs/). Read at CALL time so
249
+ * AGENTS_LOGS_DIR can redirect it in tests — same test-isolation escape hatch
250
+ * as {@link getHookShimsDir}; never set in production code.
251
+ */
229
252
  export declare function getLogsDir(): string;
230
253
  /**
231
254
  * Path to disposable performance samples (~/.agents/.cache/perf/).
232
255
  * Holds `perf.db` + a hook-shim spool. Loss is acceptable — wipe freely.
256
+ * Read at CALL time: AGENTS_PERF_DIR (the same override perf/db.ts and
257
+ * perf/spool.ts already honor for their own internal resolution) redirects
258
+ * this canonical getter too, so a caller that goes through it directly
259
+ * (hooks/cache.ts's shim generator, the OpenCode timeout sample writer in
260
+ * hooks.ts) doesn't leak samples into the user's real perf warehouse either.
233
261
  */
234
262
  export declare function getPerfDir(): string;
235
263
  /** Path to the perf SQLite warehouse (~/.agents/.cache/perf/perf.db). */
package/dist/lib/state.js CHANGED
@@ -96,6 +96,7 @@ const PROJECTS_DIR = path.join(USER_AGENTS_DIR, 'projects');
96
96
  // History bucket (durable).
97
97
  const SESSIONS_DIR = path.join(HISTORY_DIR, 'sessions');
98
98
  const SESSIONS_DB_PATH = path.join(SESSIONS_DIR, 'sessions.db');
99
+ const ANALYTICS_DIR = path.join(HISTORY_DIR, 'analytics');
99
100
  const VERSIONS_DIR = path.join(HISTORY_DIR, 'versions');
100
101
  const RUNS_DIR = path.join(HISTORY_DIR, 'runs');
101
102
  // Durable per-monitor state-diff store + fire history (last-seen value/hash,
@@ -377,6 +378,18 @@ export function getUserSecretsDir() { return USER_SECRETS_DIR; }
377
378
  export function getSecretsDbPath() {
378
379
  return process.env.AGENTS_SECRETS_DB ?? path.join(USER_SECRETS_DIR, 'secrets.db');
379
380
  }
381
+ /**
382
+ * Path to the durable resource-usage warehouse (~/.agents/.history/analytics/usage.db).
383
+ * Value-free frequency/lifecycle events (secrets, agents, browser, …). Read at CALL
384
+ * time so AGENTS_USAGE_DB can redirect tests. Sync shards may also appear as
385
+ * usage.<machine-id>.db beside this default file.
386
+ */
387
+ export function getAnalyticsDir() {
388
+ return process.env.AGENTS_ANALYTICS_DIR ?? ANALYTICS_DIR;
389
+ }
390
+ export function getUsageDbPath() {
391
+ return process.env.AGENTS_USAGE_DB ?? path.join(getAnalyticsDir(), 'usage.db');
392
+ }
380
393
  export function getUserPromptcutsPath() { return USER_PROMPTCUTS_FILE; }
381
394
  // ─── User operational path getters ────────────────────────────────────────────
382
395
  //
@@ -459,10 +472,25 @@ export function getActivityDir() { return ACTIVITY_DIR; }
459
472
  export function getVersionsDir() { return VERSIONS_DIR; }
460
473
  /** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
461
474
  export function getShimsDir() { return SHIMS_DIR; }
462
- /** Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/). */
463
- export function getHookShimsDir() { return HOOK_SHIMS_DIR; }
464
- /** Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). */
465
- export function getHookCacheDir() { return HOOK_CACHE_DIR; }
475
+ /**
476
+ * Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/).
477
+ * Read at CALL time — since every hook now resolves through a shim (RUSH-2xxx,
478
+ * pass-through timing for matcher-only hooks), a test that registers hooks
479
+ * in-process (no subprocess HOME override) would otherwise write real shim
480
+ * files into the user's actual ~/.agents/.cache. AGENTS_HOOK_SHIMS_DIR mirrors
481
+ * the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR test-isolation escape hatches;
482
+ * never set in production code.
483
+ */
484
+ export function getHookShimsDir() {
485
+ return process.env.AGENTS_HOOK_SHIMS_DIR ?? HOOK_SHIMS_DIR;
486
+ }
487
+ /**
488
+ * Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). Read at
489
+ * CALL time for the same reason as {@link getHookShimsDir} — see its doc.
490
+ */
491
+ export function getHookCacheDir() {
492
+ return process.env.AGENTS_HOOK_CACHE_DIR ?? HOOK_CACHE_DIR;
493
+ }
466
494
  /** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
467
495
  export function getBinDir() { return BIN_DIR; }
468
496
  /** Path to config backups (~/.agents/.history/backups/). */
@@ -487,7 +515,9 @@ export function getTrashDir() { return TRASH_DIR; }
487
515
  /** Path to local session indexer storage (~/.agents/.history/sessions/). */
488
516
  export function getSessionsDir() { return SESSIONS_DIR; }
489
517
  /** Path to the session index database (~/.agents/.history/sessions/sessions.db). */
490
- export function getSessionsDbPath() { return SESSIONS_DB_PATH; }
518
+ export function getSessionsDbPath() {
519
+ return process.env.AGENTS_SESSIONS_DB ?? SESSIONS_DB_PATH;
520
+ }
491
521
  /** Path to teams config + registry (~/.agents/teams/). */
492
522
  export function getTeamsDir() { return TEAMS_DIR; }
493
523
  /** Path to teams execution history (~/.agents/.history/teams/agents/). */
@@ -517,17 +547,30 @@ export function getDevicesPendingDir() { return path.join(RUNTIME_STATE_DIR, 'de
517
547
  export function getCloudDir() { return CLOUD_DIR; }
518
548
  /** Path to terminal session metadata (~/.agents/.cache/terminals/). */
519
549
  export function getTerminalsDir() { return TERMINALS_DIR; }
520
- /** Path to runtime logs (~/.agents/.cache/logs/). */
521
- export function getLogsDir() { return LOGS_DIR; }
550
+ /**
551
+ * Path to runtime logs (~/.agents/.cache/logs/). Read at CALL time so
552
+ * AGENTS_LOGS_DIR can redirect it in tests — same test-isolation escape hatch
553
+ * as {@link getHookShimsDir}; never set in production code.
554
+ */
555
+ export function getLogsDir() {
556
+ return process.env.AGENTS_LOGS_DIR ?? LOGS_DIR;
557
+ }
522
558
  /**
523
559
  * Path to disposable performance samples (~/.agents/.cache/perf/).
524
560
  * Holds `perf.db` + a hook-shim spool. Loss is acceptable — wipe freely.
561
+ * Read at CALL time: AGENTS_PERF_DIR (the same override perf/db.ts and
562
+ * perf/spool.ts already honor for their own internal resolution) redirects
563
+ * this canonical getter too, so a caller that goes through it directly
564
+ * (hooks/cache.ts's shim generator, the OpenCode timeout sample writer in
565
+ * hooks.ts) doesn't leak samples into the user's real perf warehouse either.
525
566
  */
526
- export function getPerfDir() { return PERF_DIR; }
567
+ export function getPerfDir() {
568
+ return process.env.AGENTS_PERF_DIR ?? PERF_DIR;
569
+ }
527
570
  /** Path to the perf SQLite warehouse (~/.agents/.cache/perf/perf.db). */
528
- export function getPerfDbPath() { return path.join(PERF_DIR, 'perf.db'); }
571
+ export function getPerfDbPath() { return path.join(getPerfDir(), 'perf.db'); }
529
572
  /** Path to the hook-shim NDJSON spool drained into perf.db on open. */
530
- export function getPerfSpoolPath() { return path.join(PERF_DIR, 'spool.jsonl'); }
573
+ export function getPerfSpoolPath() { return path.join(getPerfDir(), 'spool.jsonl'); }
531
574
  /** Path to per-process runtime state (~/.agents/.cache/state/). */
532
575
  export function getRuntimeStateDir() { return RUNTIME_STATE_DIR; }
533
576
  /** Path to companion-extension scratch (~/.agents/.cache/companion/). */
@@ -605,6 +605,20 @@ export interface DiscoveredPlugin {
605
605
  * (e.g. workflow-scoped) — those default to the user marketplace on sync.
606
606
  */
607
607
  marketplace?: string;
608
+ /**
609
+ * Absolute path to the DotAgents repo root containing this plugin — the
610
+ * grandparent of `root` (`<repo>/plugins/<name>` → `<repo>`), true for every
611
+ * marketplace kind (user/system/extra/project). DotAgents repos are
612
+ * git-tracked (plugins.ts), so this pairs with {@link snapshotSha}.
613
+ */
614
+ repoRoot: string;
615
+ /**
616
+ * Short HEAD sha of `repoRoot`'s git checkout, lazily resolved (a getter,
617
+ * not computed at discovery time) and memoized per repoRoot
618
+ * (`git.ts` `resolveSnapshotSha`) — see `ResolvedResource.snapshotSha` for
619
+ * the identical rationale. `undefined` when `repoRoot` isn't a git repo.
620
+ */
621
+ readonly snapshotSha: string | undefined;
608
622
  }
609
623
  /**
610
624
  * Identifies one DotAgents repo that contributes a plugin marketplace. Each
@@ -771,10 +785,14 @@ export interface Meta {
771
785
  /** Spend guardrails (issue #346). User-global caps; project agents.yaml overrides. */
772
786
  budget?: BudgetConfig;
773
787
  /**
774
- * `agents feed post` fan-out. `broadcast` maps a sink name to the argv template
775
- * run for each post, so mirroring to a tracker or a messaging CLI is the
776
- * operator's config rather than an integration compiled into this CLI. See
777
- * lib/feed-broadcast.ts and docs/06-observability.md.
788
+ * `agents feed post` fan-out. `broadcast` maps a sink name to either an argv
789
+ * template (`command:`, run for each post) or an in-process channel delivery
790
+ * (`channel:`, the same registry `agents send`/`agents notify` use), so
791
+ * mirroring to a tracker, a messaging CLI, or a channel provider is the
792
+ * operator's config rather than an integration compiled into this CLI. When
793
+ * this is unset/empty, an important-level post falls back to `notify.owner`
794
+ * implicitly (RUSH-2123) — see lib/feed-broadcast.ts and
795
+ * docs/06-observability.md.
778
796
  */
779
797
  feed?: {
780
798
  broadcast?: FeedBroadcastConfig;
@@ -0,0 +1,106 @@
1
+ import { type UsageHeadroom, type UsageSnapshot, type UsageInfo } from './usage.js';
2
+ import type { AgentId } from './types.js';
3
+ /** Floor on the adaptive interval: an account seconds from its cap still isn't
4
+ * polled faster than this. */
5
+ export declare const REFRESH_MIN_MS: number;
6
+ /** Ceiling on the adaptive interval: an idle account is still re-checked at
7
+ * least this often so a cache never silently rots. */
8
+ export declare const REFRESH_MAX_MS: number;
9
+ /** Schedule the next refresh at `minutesToLimit / K` — poll well before the cap,
10
+ * not exactly at it. */
11
+ export declare const REFRESH_BURN_DIVISOR = 4;
12
+ /** At most this many live fetches per account per rolling hour. */
13
+ export declare const HOURLY_CALL_CAP = 6;
14
+ /**
15
+ * One account's refresh state + published headroom. `sessionUsedPercent` /
16
+ * `capturedAt` are the prior sample the NEXT tick projects the burn rate from;
17
+ * `minutesToLimit` / `status` are what the routing hot path reads.
18
+ */
19
+ export interface HeadroomEntry {
20
+ status: UsageHeadroom['status'];
21
+ minutesToLimit: number | null;
22
+ /** The session window's usedPercent in the last snapshot (the prev sample). */
23
+ sessionUsedPercent: number | null;
24
+ /** Epoch ms the last snapshot was captured. */
25
+ capturedAt: number | null;
26
+ /** Epoch ms this account is next due for a live refresh. */
27
+ nextRefreshAt: number;
28
+ /** Epoch ms of recent live fetches, for the rolling-hour cap. */
29
+ callTimestamps: number[];
30
+ /** Epoch ms this entry was written. */
31
+ computedAt: number;
32
+ }
33
+ export declare function setHeadroomCachePathForTest(cachePath: string | null): string | null;
34
+ /** Read the whole headroom cache (best-effort; missing/corrupt → empty map). */
35
+ export declare function readHeadroomCache(): Record<string, HeadroomEntry>;
36
+ /** Read one account's headroom entry, or null. */
37
+ export declare function readHeadroomEntry(usageKey: string): HeadroomEntry | null;
38
+ /** Merge entries into the cache (best-effort; preserves other accounts' rows). */
39
+ export declare function writeHeadroomEntries(entries: Record<string, HeadroomEntry>): void;
40
+ /**
41
+ * The adaptive interval until the next refresh, clamped to [MIN, MAX]. A
42
+ * shorter `minutesToLimit` (closer to the cap) polls sooner; `null` (unknown /
43
+ * idle / not burning) waits the full ceiling.
44
+ */
45
+ export declare function computeNextRefreshDelayMs(minutesToLimit: number | null, opts?: {
46
+ minMs?: number;
47
+ maxMs?: number;
48
+ divisor?: number;
49
+ }): number;
50
+ /** Recent call timestamps trimmed to the trailing hour. */
51
+ export declare function pruneCallTimestamps(timestamps: number[], now: number, windowMs?: number): number[];
52
+ /**
53
+ * Whether an account may be live-refreshed right now: it is due (past its
54
+ * scheduled `nextRefreshAt`) AND under the rolling-hour call cap. Pure so the
55
+ * cadence + cap arithmetic is unit-tested without a daemon or a network call.
56
+ */
57
+ export declare function shouldRefreshAccount(entry: HeadroomEntry | null | undefined, now: number, opts?: {
58
+ hourlyCap?: number;
59
+ windowMs?: number;
60
+ }): boolean;
61
+ /**
62
+ * Build the next headroom entry after a live refresh: project headroom from the
63
+ * new snapshot against the prior sample, schedule the next refresh from the
64
+ * projection, and record this call for the hourly cap.
65
+ */
66
+ export declare function nextHeadroomEntry(prev: HeadroomEntry | null | undefined, snapshot: UsageSnapshot | null, now: number): HeadroomEntry;
67
+ /** An account whose credentials live on THIS host — the only ones we refresh. */
68
+ export interface LocalUsageAccount {
69
+ usageKey: string;
70
+ agentId: AgentId;
71
+ /** Live-fetch this account's usage; the daemon passes the real network fetch. */
72
+ fetch: () => Promise<UsageInfo>;
73
+ }
74
+ /**
75
+ * Enumerate the network-usage accounts whose credentials live on THIS host — one
76
+ * per unique usage key, deduped to the most-recently-active version (the same
77
+ * canonicalization `getUsageInfoByIdentity` uses). Each carries a closure that
78
+ * live-fetches its usage. This is the daemon's `listAccounts`; because it only
79
+ * ever lists local, signed-in accounts, each host is the sole writer for its own
80
+ * accounts' caches — no cross-host coordination.
81
+ */
82
+ export declare function buildLocalUsageAccounts(): Promise<LocalUsageAccount[]>;
83
+ /** Injectable side effects, so `runUsageRefresh` is drivable without the daemon. */
84
+ export interface UsageRefreshDeps {
85
+ now?: number;
86
+ /** Local-credential accounts to consider (one per unique usage key). */
87
+ listAccounts: () => Promise<LocalUsageAccount[]>;
88
+ /** Persist a fresh snapshot to the usage cache (writeClaudeUsageCache). */
89
+ writeUsageCache: (usageKey: string, snapshot: UsageSnapshot) => void;
90
+ /** Epoch ms a provider is backed off until, or null when free (usageRateLimitedUntil). */
91
+ backoffUntil: (agentId: AgentId) => number | null;
92
+ }
93
+ export interface UsageRefreshResult {
94
+ refreshed: number;
95
+ skippedNotDue: number;
96
+ skippedBackoff: number;
97
+ skippedCap: number;
98
+ failed: number;
99
+ }
100
+ /**
101
+ * One refresher tick: for each local account that is due, under its hourly cap,
102
+ * and not provider-backed-off, live-fetch its usage, update the cache, and
103
+ * reschedule from the new burn projection. Never throws — a single account's
104
+ * failed fetch leaves its cache untouched and counts as `failed`.
105
+ */
106
+ export declare function runUsageRefresh(deps: UsageRefreshDeps): Promise<UsageRefreshResult>;