@phnx-labs/agents-cli 1.22.5 → 1.22.7

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 (78) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +46 -5
  5. package/dist/commands/feed.d.ts +2 -1
  6. package/dist/commands/feed.js +47 -20
  7. package/dist/commands/focus.js +22 -1
  8. package/dist/commands/models.js +2 -2
  9. package/dist/commands/monitors.js +1 -1
  10. package/dist/commands/projects.js +122 -107
  11. package/dist/commands/secrets.js +1 -1
  12. package/dist/commands/sessions-backfill.d.ts +33 -0
  13. package/dist/commands/sessions-backfill.js +83 -1
  14. package/dist/commands/sessions-stats.d.ts +36 -0
  15. package/dist/commands/sessions-stats.js +263 -0
  16. package/dist/commands/sessions.d.ts +1 -1
  17. package/dist/commands/sessions.js +21 -1
  18. package/dist/commands/view.d.ts +2 -1
  19. package/dist/commands/view.js +62 -11
  20. package/dist/index.js +3 -3
  21. package/dist/lib/activity.js +2 -2
  22. package/dist/lib/agents.js +68 -11
  23. package/dist/lib/analytics/recipes.js +11 -5
  24. package/dist/lib/browser/profiles.d.ts +15 -7
  25. package/dist/lib/browser/profiles.js +53 -12
  26. package/dist/lib/byok-usage.d.ts +38 -0
  27. package/dist/lib/byok-usage.js +117 -0
  28. package/dist/lib/capabilities.js +1 -1
  29. package/dist/lib/exec.d.ts +20 -0
  30. package/dist/lib/exec.js +73 -8
  31. package/dist/lib/feed-outcome.d.ts +1 -0
  32. package/dist/lib/feed-outcome.js +2 -0
  33. package/dist/lib/feed-post.js +1 -1
  34. package/dist/lib/feed-ranking.d.ts +1 -0
  35. package/dist/lib/feed-ranking.js +4 -0
  36. package/dist/lib/feed.d.ts +4 -1
  37. package/dist/lib/feed.js +25 -0
  38. package/dist/lib/hosts/passthrough.js +0 -1
  39. package/dist/lib/mcp.js +6 -1
  40. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  41. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  42. package/dist/lib/model-tiers.js +4 -1
  43. package/dist/lib/models.js +63 -0
  44. package/dist/lib/profiles.d.ts +42 -1
  45. package/dist/lib/profiles.js +50 -2
  46. package/dist/lib/project-focus.d.ts +9 -0
  47. package/dist/lib/project-focus.js +23 -0
  48. package/dist/lib/project-key.d.ts +1 -1
  49. package/dist/lib/project-key.js +1 -1
  50. package/dist/lib/project-probe.d.ts +18 -0
  51. package/dist/lib/project-probe.js +46 -0
  52. package/dist/lib/project-status.d.ts +56 -0
  53. package/dist/lib/project-status.js +125 -18
  54. package/dist/lib/resources/mcp.js +3 -0
  55. package/dist/lib/resources/types.d.ts +1 -1
  56. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  57. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  58. package/dist/lib/secrets/audit.js +1 -1
  59. package/dist/lib/secrets/index.d.ts +1 -0
  60. package/dist/lib/secrets/index.js +29 -15
  61. package/dist/lib/secrets/remote.d.ts +1 -1
  62. package/dist/lib/secrets/remote.js +1 -1
  63. package/dist/lib/session/active.d.ts +2 -0
  64. package/dist/lib/session/bash-command.d.ts +2 -3
  65. package/dist/lib/session/db.d.ts +92 -1
  66. package/dist/lib/session/db.js +230 -1
  67. package/dist/lib/session/digest.d.ts +1 -1
  68. package/dist/lib/session/digest.js +1 -1
  69. package/dist/lib/share/publish.js +24 -0
  70. package/dist/lib/startup/command-registry.d.ts +0 -1
  71. package/dist/lib/startup/command-registry.js +0 -2
  72. package/dist/lib/subagents-registry.js +3 -0
  73. package/dist/lib/types.d.ts +11 -2
  74. package/dist/lib/usage.d.ts +5 -0
  75. package/dist/lib/usage.js +3 -3
  76. package/package.json +1 -1
  77. package/dist/commands/activity.d.ts +0 -87
  78. package/dist/commands/activity.js +0 -346
@@ -78,3 +78,26 @@ export async function readFocusAreas(root, windowDays) {
78
78
  return [];
79
79
  }
80
80
  }
81
+ /** Compact count: 2329 → "2.3k", under 1000 stays exact. */
82
+ export function formatFocusCount(n) {
83
+ if (!Number.isFinite(n) || n < 0)
84
+ return '0';
85
+ if (n < 1000)
86
+ return String(Math.round(n));
87
+ const k = n / 1000;
88
+ const s = k >= 10 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '');
89
+ return `${s}k`;
90
+ }
91
+ /**
92
+ * One scannable focus line: path + count, with a single unit trailer so the
93
+ * bare integer is never mistaken for commits or minutes.
94
+ *
95
+ * apps/cli/src 2.3k · apps/cli/docs 302 · apps/factory/src 245 file-touches (7d)
96
+ */
97
+ export function formatFocusAreas(areas, windowDays) {
98
+ if (areas.length === 0)
99
+ return '';
100
+ const body = areas.map((a) => `${a.path} ${formatFocusCount(a.touches)}`).join(' · ');
101
+ const unit = `file-touches (${windowDays}d)`;
102
+ return `${body} ${unit}`;
103
+ }
@@ -2,7 +2,7 @@
2
2
  * The one worktree-aware cwd -> project fold.
3
3
  *
4
4
  * Several surfaces bucket work "by project": the `agents sessions` overview,
5
- * the `agents activity` timeline, and anything else that has a cwd and needs a
5
+ * the `agents feed` timeline, and anything else that has a cwd and needs a
6
6
  * stable repo-level key. They must agree, or the same session shows up under
7
7
  * `agents-cli` in one view and `my-branch-slug` in another — so the rule lives
8
8
  * here and every caller delegates.
@@ -2,7 +2,7 @@
2
2
  * The one worktree-aware cwd -> project fold.
3
3
  *
4
4
  * Several surfaces bucket work "by project": the `agents sessions` overview,
5
- * the `agents activity` timeline, and anything else that has a cwd and needs a
5
+ * the `agents feed` timeline, and anything else that has a cwd and needs a
6
6
  * stable repo-level key. They must agree, or the same session shows up under
7
7
  * `agents-cli` in one view and `my-branch-slug` in another — so the rule lives
8
8
  * here and every caller delegates.
@@ -73,3 +73,21 @@ export declare function formatWorkspaceLine(s: RepoWorkspaceStatus): string;
73
73
  * `fleet` row label.
74
74
  */
75
75
  export declare function formatFleetWorkspaces(statuses: HostWorkspaceStatus[]): string[];
76
+ /** Severity for a workspace/repo warning on the project card. */
77
+ export type WorkspaceWarningSeverity = 'critical' | 'continue';
78
+ export interface WorkspaceWarning {
79
+ severity: WorkspaceWarningSeverity;
80
+ text: string;
81
+ remediation?: string;
82
+ }
83
+ /**
84
+ * Turn probed workspace rows into card-footer warnings.
85
+ *
86
+ * - missing / unreadable git → critical (agents there cannot share a tree)
87
+ * - behind upstream → critical when ≥10 commits, continue otherwise
88
+ * - dirty tree → continue (local work is fine; just note it)
89
+ * - ahead-only is not a warning (that is progress waiting to push)
90
+ *
91
+ * Pure. Caller decides whether the rows came from `--fleet` or a local probe.
92
+ */
93
+ export declare function workspaceWarnings(statuses: HostWorkspaceStatus[]): WorkspaceWarning[];
@@ -158,3 +158,49 @@ export function formatFleetWorkspaces(statuses) {
158
158
  return multi ? `${chalk.dim(`${p} · `)}${body}` : body;
159
159
  });
160
160
  }
161
+ /**
162
+ * Turn probed workspace rows into card-footer warnings.
163
+ *
164
+ * - missing / unreadable git → critical (agents there cannot share a tree)
165
+ * - behind upstream → critical when ≥10 commits, continue otherwise
166
+ * - dirty tree → continue (local work is fine; just note it)
167
+ * - ahead-only is not a warning (that is progress waiting to push)
168
+ *
169
+ * Pure. Caller decides whether the rows came from `--fleet` or a local probe.
170
+ */
171
+ export function workspaceWarnings(statuses) {
172
+ const out = [];
173
+ for (const s of [...statuses].sort((a, b) => a.host.localeCompare(b.host) || a.path.localeCompare(b.path))) {
174
+ const where = s.host ? `${s.host}` : 'local';
175
+ const pathBit = s.path ? ` (${s.path})` : '';
176
+ if (!s.present) {
177
+ out.push({
178
+ severity: 'critical',
179
+ text: `${where}: checkout missing${pathBit}`,
180
+ remediation: 'clone or sync the project root on that host before landing agents there',
181
+ });
182
+ continue;
183
+ }
184
+ if (s.error) {
185
+ out.push({
186
+ severity: 'critical',
187
+ text: `${where}: ${s.error}${pathBit}`,
188
+ });
189
+ continue;
190
+ }
191
+ if (s.behind !== undefined && s.behind > 0) {
192
+ out.push({
193
+ severity: s.behind >= 10 ? 'critical' : 'continue',
194
+ text: `${where} is ${s.behind} commit${s.behind === 1 ? '' : 's'} behind ${s.upstream ?? 'upstream'}${pathBit}`,
195
+ remediation: 'pull (or rebase) before agents on this host open PRs against a stale base',
196
+ });
197
+ }
198
+ if (s.dirty !== undefined && s.dirty > 0) {
199
+ out.push({
200
+ severity: 'continue',
201
+ text: `${where} has ${s.dirty} uncommitted change${s.dirty === 1 ? '' : 's'}${pathBit}`,
202
+ });
203
+ }
204
+ }
205
+ return out;
206
+ }
@@ -53,6 +53,13 @@ export interface ProjectSessionRollup {
53
53
  * containing only projects with at least one matched session — callers merge
54
54
  * with the full definition list to show zero-agent projects.
55
55
  */
56
+ /**
57
+ * Ensure every session carries a host for the card roster. Local
58
+ * `getActiveSessions()` omits `machine`; remotes already set it. Pure.
59
+ */
60
+ export declare function withDefaultMachine<T extends {
61
+ machine?: string;
62
+ }>(sessions: T[], defaultHost: string): T[];
56
63
  export declare function rollupSessionsByProject(defs: ProjectDef[], sessions: ActiveSession[]): Map<string, ProjectSessionRollup>;
57
64
  /**
58
65
  * True when a session's status means it is over. Exported so the card can keep
@@ -84,6 +91,19 @@ export declare function liveDeadSplit(byStatus: Partial<Record<ActiveStatus, num
84
91
  export declare function sortProjectMembers(members: ProjectMember[]): ProjectMember[];
85
92
  /** Cap for the members line before it collapses to `+N more`. */
86
93
  export declare const MEMBERS_LINE_LIMIT = 6;
94
+ /** Cap for host groups on the multi-line agents roster. */
95
+ export declare const MEMBERS_HOST_LIMIT = 8;
96
+ /**
97
+ * Collapse members into distinct state cells (`agent · status · ticket[@host]`),
98
+ * counting duplicates as `×N`. Pure.
99
+ */
100
+ export declare function collapseMemberCells(members: ProjectMember[], opts?: {
101
+ includeHostOnCell?: boolean;
102
+ }): Array<{
103
+ cell: string;
104
+ n: number;
105
+ members: number;
106
+ }>;
87
107
  /**
88
108
  * The `agents` line under `live`: one cell per DISTINCT member state —
89
109
  * `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
@@ -91,8 +111,44 @@ export declare const MEMBERS_LINE_LIMIT = 6;
91
111
  * truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
92
112
  * `+N more` tail counting members, not cells. Pure (chalk styling only); the
93
113
  * caller adds the label.
114
+ *
115
+ * Prefer {@link formatProjectMembersByHost} on the card: a flat line hides which
116
+ * machine is running the work when the same harness×status spans hosts.
94
117
  */
95
118
  export declare function formatProjectMembers(members: ProjectMember[], limit?: number): string;
119
+ /**
120
+ * Host-grouped agents roster. One content line per host:
121
+ * `@zion claude · running ×9 · claude · idle ×4`
122
+ *
123
+ * When no member carries a host (local-only rollup with no machine stamp), falls
124
+ * back to a single flat line via {@link formatProjectMembers}. Pure.
125
+ */
126
+ export declare function formatProjectMembersByHost(members: ProjectMember[], opts?: {
127
+ cellLimit?: number;
128
+ hostLimit?: number;
129
+ }): string[];
130
+ /** A card-level warning collected for the footer. */
131
+ export type ProjectWarningSeverity = 'critical' | 'continue';
132
+ export interface ProjectWarning {
133
+ severity: ProjectWarningSeverity;
134
+ /** One human line. */
135
+ text: string;
136
+ /** Optional fix or next step. */
137
+ remediation?: string;
138
+ }
139
+ /**
140
+ * Severity markers for the warnings footer. User-facing by design: critical
141
+ * stops you (wrong repo, missing checkout, large drift); continue is a soft
142
+ * nudge (dirty tree, schedule not measurable).
143
+ */
144
+ export declare function warningEmoji(severity: ProjectWarningSeverity): string;
145
+ /** Stable sort: critical first, then continue; stable within a tier. */
146
+ export declare function sortProjectWarnings(warnings: ProjectWarning[]): ProjectWarning[];
147
+ /**
148
+ * Format one or more warning lines for the card footer. Pure (chalk only).
149
+ * Returns empty when there is nothing to say.
150
+ */
151
+ export declare function formatProjectWarnings(warnings: ProjectWarning[]): string[];
96
152
  /** Harvested signals not on the session list: repo-global merged PRs + releases, local artifacts, in a time window. */
97
153
  export interface ProjectRemoteSignals {
98
154
  windowDays: number;
@@ -41,6 +41,13 @@ function blank(name) {
41
41
  * containing only projects with at least one matched session — callers merge
42
42
  * with the full definition list to show zero-agent projects.
43
43
  */
44
+ /**
45
+ * Ensure every session carries a host for the card roster. Local
46
+ * `getActiveSessions()` omits `machine`; remotes already set it. Pure.
47
+ */
48
+ export function withDefaultMachine(sessions, defaultHost) {
49
+ return sessions.map((s) => (s.machine ? s : { ...s, machine: defaultHost }));
50
+ }
44
51
  export function rollupSessionsByProject(defs, sessions) {
45
52
  const map = new Map();
46
53
  const prSeen = new Map();
@@ -148,25 +155,20 @@ export function sortProjectMembers(members) {
148
155
  }
149
156
  /** Cap for the members line before it collapses to `+N more`. */
150
157
  export const MEMBERS_LINE_LIMIT = 6;
158
+ /** Cap for host groups on the multi-line agents roster. */
159
+ export const MEMBERS_HOST_LIMIT = 8;
151
160
  /**
152
- * The `agents` line under `live`: one cell per DISTINCT member state —
153
- * `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
154
- * `×N` count (35 same-harness sessions in one state are one fact, not six
155
- * truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
156
- * `+N more` tail counting members, not cells. Pure (chalk styling only); the
157
- * caller adds the label.
161
+ * Collapse members into distinct state cells (`agent · status · ticket[@host]`),
162
+ * counting duplicates as `×N`. Pure.
158
163
  */
159
- export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
160
- if (members.length === 0)
161
- return '';
162
- // Collapse identical cells — 35 same-harness sessions in the same state are
163
- // one fact (`claude · running ×16`), not six truncated duplicates.
164
+ export function collapseMemberCells(members, opts = {}) {
165
+ const includeHost = opts.includeHostOnCell !== false;
164
166
  const counts = new Map();
165
167
  for (const m of sortProjectMembers(members)) {
166
168
  const parts = [m.agent, m.status];
167
169
  if (m.ticket)
168
170
  parts.push(m.ticket);
169
- const cell = parts.join(' · ') + (m.host ? ` @${m.host}` : '');
171
+ const cell = parts.join(' · ') + (includeHost && m.host ? ` @${m.host}` : '');
170
172
  const key = cell.toLowerCase();
171
173
  const entry = counts.get(key);
172
174
  if (entry)
@@ -174,12 +176,117 @@ export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
174
176
  else
175
177
  counts.set(key, { cell, n: 1 });
176
178
  }
177
- const entries = [...counts.values()];
178
- const shown = entries.slice(0, Math.max(1, limit));
179
- const shownMembers = shown.reduce((acc, e) => acc + e.n, 0);
180
- const more = members.length - shownMembers;
181
- const cells = shown.map(({ cell, n }) => (n > 1 ? `${cell} ×${n}` : cell));
182
- return cells.join(chalk.dim(' · ')) + (more > 0 ? chalk.dim(` · +${more} more`) : '');
179
+ return [...counts.values()].map(({ cell, n }) => ({ cell, n, members: n }));
180
+ }
181
+ function formatCollapsedCells(cells, memberTotal, limit) {
182
+ if (cells.length === 0)
183
+ return '';
184
+ const shown = cells.slice(0, Math.max(1, limit));
185
+ const shownMembers = shown.reduce((acc, e) => acc + e.members, 0);
186
+ const more = memberTotal - shownMembers;
187
+ const parts = shown.map(({ cell, n }) => (n > 1 ? `${cell} ×${n}` : cell));
188
+ return parts.join(chalk.dim(' · ')) + (more > 0 ? chalk.dim(` · +${more} more`) : '');
189
+ }
190
+ /**
191
+ * The `agents` line under `live`: one cell per DISTINCT member state —
192
+ * `claude · running · RUSH-2107 @zion` — with identical cells collapsed to a
193
+ * `×N` count (35 same-harness sessions in one state are one fact, not six
194
+ * truncated duplicates), capped at {@link MEMBERS_LINE_LIMIT} cells with a
195
+ * `+N more` tail counting members, not cells. Pure (chalk styling only); the
196
+ * caller adds the label.
197
+ *
198
+ * Prefer {@link formatProjectMembersByHost} on the card: a flat line hides which
199
+ * machine is running the work when the same harness×status spans hosts.
200
+ */
201
+ export function formatProjectMembers(members, limit = MEMBERS_LINE_LIMIT) {
202
+ if (members.length === 0)
203
+ return '';
204
+ return formatCollapsedCells(collapseMemberCells(members, { includeHostOnCell: true }), members.length, limit);
205
+ }
206
+ /**
207
+ * Host-grouped agents roster. One content line per host:
208
+ * `@zion claude · running ×9 · claude · idle ×4`
209
+ *
210
+ * When no member carries a host (local-only rollup with no machine stamp), falls
211
+ * back to a single flat line via {@link formatProjectMembers}. Pure.
212
+ */
213
+ export function formatProjectMembersByHost(members, opts = {}) {
214
+ if (members.length === 0)
215
+ return [];
216
+ const cellLimit = opts.cellLimit ?? MEMBERS_LINE_LIMIT;
217
+ const hostLimit = opts.hostLimit ?? MEMBERS_HOST_LIMIT;
218
+ const byHost = new Map();
219
+ let anyHost = false;
220
+ for (const m of members) {
221
+ if (m.host)
222
+ anyHost = true;
223
+ const key = m.host ?? '';
224
+ const list = byHost.get(key);
225
+ if (list)
226
+ list.push(m);
227
+ else
228
+ byHost.set(key, [m]);
229
+ }
230
+ // Local-only (no host stamps at all) — keep the compact one-liner.
231
+ if (!anyHost) {
232
+ const line = formatProjectMembers(members, cellLimit);
233
+ return line ? [line] : [];
234
+ }
235
+ // Hosts with the most members first, then name; unstamped ("") last.
236
+ const hosts = [...byHost.entries()].sort((a, b) => {
237
+ if (a[0] === '' && b[0] !== '')
238
+ return 1;
239
+ if (b[0] === '' && a[0] !== '')
240
+ return -1;
241
+ if (b[1].length !== a[1].length)
242
+ return b[1].length - a[1].length;
243
+ return a[0].localeCompare(b[0]);
244
+ });
245
+ const shownHosts = hosts.slice(0, Math.max(1, hostLimit));
246
+ const hiddenMembers = hosts.slice(hostLimit).reduce((acc, [, ms]) => acc + ms.length, 0);
247
+ const hostWidth = Math.max(...shownHosts.map(([h]) => (h ? `@${h}` : '@local').length), 1);
248
+ const lines = shownHosts.map(([host, ms]) => {
249
+ const label = (host ? `@${host}` : '@local').padEnd(hostWidth);
250
+ // Host is the row key — do not repeat @host on every cell.
251
+ const cells = collapseMemberCells(ms, { includeHostOnCell: false });
252
+ const body = formatCollapsedCells(cells, ms.length, cellLimit);
253
+ return `${chalk.cyan(label)} ${body}`;
254
+ });
255
+ if (hiddenMembers > 0) {
256
+ const restHosts = hosts.length - shownHosts.length;
257
+ lines.push(chalk.dim(`+${hiddenMembers} more on ${restHosts} host${restHosts === 1 ? '' : 's'}`));
258
+ }
259
+ return lines;
260
+ }
261
+ /**
262
+ * Severity markers for the warnings footer. User-facing by design: critical
263
+ * stops you (wrong repo, missing checkout, large drift); continue is a soft
264
+ * nudge (dirty tree, schedule not measurable).
265
+ */
266
+ export function warningEmoji(severity) {
267
+ return severity === 'critical' ? '🔴' : '⚠️';
268
+ }
269
+ /** Stable sort: critical first, then continue; stable within a tier. */
270
+ export function sortProjectWarnings(warnings) {
271
+ const rank = { critical: 0, continue: 1 };
272
+ return [...warnings].sort((a, b) => rank[a.severity] - rank[b.severity] || a.text.localeCompare(b.text));
273
+ }
274
+ /**
275
+ * Format one or more warning lines for the card footer. Pure (chalk only).
276
+ * Returns empty when there is nothing to say.
277
+ */
278
+ export function formatProjectWarnings(warnings) {
279
+ if (warnings.length === 0)
280
+ return [];
281
+ const lines = [];
282
+ for (const w of sortProjectWarnings(warnings)) {
283
+ const mark = warningEmoji(w.severity);
284
+ const color = w.severity === 'critical' ? chalk.red : chalk.yellow;
285
+ lines.push(` ${mark} ${color(w.text)}`);
286
+ if (w.remediation)
287
+ lines.push(` ${chalk.dim(w.remediation)}`);
288
+ }
289
+ return lines;
183
290
  }
184
291
  /**
185
292
  * Harvest the signals that don't live on the active-session list: recently
@@ -124,6 +124,9 @@ export function getMcpConfigPath(agent, versionHome) {
124
124
  return path.join(versionHome, '.grok', 'mcp.json');
125
125
  case 'hermes':
126
126
  return path.join(versionHome, '.hermes', 'config.yaml');
127
+ case 'pi':
128
+ // omp reads user-scope MCP from ~/.omp/agent/.mcp.json (Claude schema).
129
+ return path.join(versionHome, '.omp', 'agent', '.mcp.json');
127
130
  default:
128
131
  return null;
129
132
  }
@@ -5,7 +5,7 @@
5
5
  * - Union: All resources from all layers are combined
6
6
  * - Override on name conflict: Higher layer wins (project > user > system)
7
7
  */
8
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
8
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
10
  export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
11
11
  /** A resolved resource with its origin layer. */
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * The event vocabulary, all audit-level and non-milestone (so they surface in
21
21
  * `agents events` and the persisted audit trail, but are NOT required in the
22
- * curated `agents activity` / `agents feed` surfaces):
22
+ * curated `agents feed` surface):
23
23
  * - `secrets.get` — a value was READ (exec inject, export, `view --reveal`,
24
24
  * raw `get <item>`, remote resolve, sync push,
25
25
  * `run --secrets`, and every other bundle read).
@@ -121,6 +121,7 @@ export declare function withRawKeychainServiceNames<T>(fn: () => T): T;
121
121
  * the re-key migration and tests; runtime callers go through the primitives,
122
122
  * which apply this transparently. */
123
123
  export declare function hashedServiceName(item: string, key: Buffer): string;
124
+ export declare function readHmacKeyRecord(): HmacKeyRecord | null;
124
125
  /**
125
126
  * Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
126
127
  * migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
@@ -235,7 +235,7 @@ function parseHmacKeyRecord(raw) {
235
235
  }
236
236
  return null;
237
237
  }
238
- function readHmacKeyRecord() {
238
+ export function readHmacKeyRecord() {
239
239
  // HMAC_KEY_ITEM is exempt from the transform, so this routes to the helper
240
240
  // (or the test backend) under its literal name. The item is no-ACL, so the
241
241
  // read is silent — attest that to the storm guard so a headless hashed-name
@@ -247,7 +247,30 @@ function readHmacKeyRecord() {
247
247
  catch {
248
248
  return null;
249
249
  }
250
- return parseHmacKeyRecord(raw);
250
+ const record = parseHmacKeyRecord(raw);
251
+ // Converge a stale-ACL'd hmackey to silent, on the HOT read path. An old helper
252
+ // (pre the metadata/hmackey no-ACL migration fix) re-stamped this
253
+ // contractually-no-ACL item with a biometry ACL, so the read just above pops the
254
+ // generic "Agents CLI needs to authenticate" sheet on EVERY hashed lookup — the
255
+ // `agents devices list` stats probe the SessionStart hook runs, and every other
256
+ // background hashed read. `maybeAutoRekey`'s one-shot heal only fires on a
257
+ // cleartext-bundle resolve and is bypassed for the hmackey/hashed-name path
258
+ // (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey), so
259
+ // it never converged exactly these reads and the machine prompted forever.
260
+ // Re-store the record no-ACL once per machine here (the read that produced it has
261
+ // already happened — and already prompted if the item was ACL'd); every
262
+ // subsequent read, in this process and all future ones, is silent.
263
+ if (record && !record.healedNoAcl) {
264
+ try {
265
+ healHmacKeyNoAclOnce(record);
266
+ record.healedNoAcl = true;
267
+ }
268
+ catch {
269
+ // A failed no-ACL re-store leaves the item still ACL'd (a still-prompting
270
+ // read) rather than a silent wrong state; the next process retries.
271
+ }
272
+ }
273
+ return record;
251
274
  }
252
275
  function writeHmacKeyRecord(rec) {
253
276
  // JSON.stringify drops undefined fields (used to clear pendingDeletes).
@@ -423,19 +446,10 @@ function maybeAutoRekey() {
423
446
  return;
424
447
  const st = resolveHashState();
425
448
  if (st.active) {
426
- // Heal an already-active machine whose hmackey was re-stamped ACL'd by an old
427
- // helper (its read popped the generic Touch ID sheet on every hashed lookup).
428
- // Runs once per machine; mutate the local so a later finishPendingDeletes write
429
- // preserves the healed flag.
430
- if (st.record && !st.record.healedNoAcl) {
431
- try {
432
- healHmacKeyNoAclOnce(st.record);
433
- st.record.healedNoAcl = true;
434
- }
435
- catch {
436
- /* next process retries */
437
- }
438
- }
449
+ // The stale-ACL'd-hmackey heal now runs on the hot read path
450
+ // (readHmacKeyRecord), which the resolveHashState() above just went through —
451
+ // so st.record is already healed here regardless of how this machine reached
452
+ // "hashing active". Nothing to do but finish any pending deletes.
439
453
  if (st.record?.pendingDeletes?.length) {
440
454
  try {
441
455
  finishPendingDeletes(st.record);
@@ -46,7 +46,7 @@ export declare function resolveHostSshTarget(nameOrAlias: string): Promise<strin
46
46
  * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
47
47
  * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
48
48
  * works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
49
- * fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
49
+ * fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
50
50
  * works on the secrets remote commands too. Empty when none is set.
51
51
  */
52
52
  export declare function parseHostsOption(opts: {
@@ -77,7 +77,7 @@ export async function resolveHostSshTarget(nameOrAlias) {
77
77
  * Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
78
78
  * aliases) into an ordered, de-duplicated list. All four flags compose; any alone
79
79
  * works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
80
- * fleet-wide `--device` vocabulary (see `agents activity`, `agents run --device`)
80
+ * fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
81
81
  * works on the secrets remote commands too. Empty when none is set.
82
82
  */
83
83
  export function parseHostsOption(opts) {
@@ -58,6 +58,8 @@ export interface ActiveSession {
58
58
  pid?: number;
59
59
  sessionId?: string;
60
60
  cwd?: string;
61
+ /** Project/repo key derived from cwd, when known. */
62
+ project?: string | null;
61
63
  /** User-given name from /rename command. */
62
64
  label?: string;
63
65
  /** Durable `agents run --name` launch handle, when the run was named. */
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Parses the raw command strings agents pass to Bash tool calls into structured
3
3
  * metadata: the executable, category, subcommand, and a display summary. Used by
4
- * session rendering and the activity-log hook so `agents sessions` and
5
- * `agents activity` can summarize what actually happened instead of printing a
6
- * wall of shell.
4
+ * session rendering and the activity-log hook so `agents sessions` can
5
+ * summarize what actually happened instead of printing a wall of shell.
7
6
  */
8
7
  export type BashCategory = 'vcs' | 'build-test' | 'install' | 'remote' | 'http' | 'media' | 'upscaling' | 'metadata' | 'probe' | 'search' | 'shell' | 'wait' | 'other';
9
8
  export interface BashToolInfo {
@@ -12,7 +12,14 @@ import { type IndexedToolCall } from './tool-calls.js';
12
12
  /** Current schema version; bumped when migrations are added. Exported so tests
13
13
  * assert against the constant instead of hardcoding a number that every bump
14
14
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
15
- export declare const SCHEMA_VERSION = 30;
15
+ export declare const SCHEMA_VERSION = 31;
16
+ /**
17
+ * Bump to force `agents sessions backfill resources` to re-derive every
18
+ * session's skill/slash-command tallies on its next run (resource_scan_ledger
19
+ * rows with a lower version are treated as stale — the same mechanism
20
+ * TOOL_INDEX_VERSION gives the tool backfill).
21
+ */
22
+ export declare const RESOURCE_INDEX_VERSION = 1;
16
23
  /** Raw row shape returned from the sessions table. */
17
24
  export interface SessionRow {
18
25
  id: string;
@@ -75,6 +82,8 @@ export interface QueryOptions {
75
82
  /** Match any session whose cwd equals this or is a descendant of it. */
76
83
  cwdPrefix?: string;
77
84
  project?: string;
85
+ /** Only sessions recorded on this machine (host), case-insensitive. */
86
+ machine?: string;
78
87
  /** Match the full session id or short id, case-insensitively (exact). */
79
88
  idExact?: string;
80
89
  /** Match sessions whose id or short id begins with this (case-insensitive prefix). */
@@ -324,6 +333,88 @@ export declare function queryAffinityRollup(options: {
324
333
  export declare function queryUsageRollup(options: QueryOptions & {
325
334
  groupBy: UsageRollupGroup;
326
335
  }): UsageRollupRow[];
336
+ /** One aggregated resource (skill or slash-command) in a usage-stats rollup. */
337
+ export interface ResourceStatRow {
338
+ /** 'skill' or 'command' (singular, as stored in session_resource_usage.kind). */
339
+ kind: string;
340
+ /** Stored resource name — bare, or `plugin:short` for a plugin-owned resource. */
341
+ name: string;
342
+ /** Owning plugin, or null for a flat (non-namespaced) resource. */
343
+ plugin: string | null;
344
+ /** DotAgents layer or plugin marketplace the resource resolved to at write time. */
345
+ source: string | null;
346
+ /** Distinct sessions that invoked this resource within the filter window. */
347
+ sessions: number;
348
+ /** Total invocations (sum of per-session counts) within the window. */
349
+ invocations: number;
350
+ }
351
+ /**
352
+ * Roll up skill / slash-command usage from session_resource_usage, joined to
353
+ * `sessions` for attribution so the same filter shape as querySessions
354
+ * (agent / project / since / machine) narrows WHICH sessions count. Grouped by
355
+ * resource identity (kind + name + plugin + source) and ordered by invocation
356
+ * volume — the read side of "which skills/commands do I actually use, and which
357
+ * are dead weight". `order: 'bottom'` ranks least-used first (the one-time
358
+ * skills); `limit` caps the returned rows.
359
+ *
360
+ * The signal only captures EXPLICIT invocations (slash commands and `Skill`
361
+ * tool calls). An auto-triggered skill (loaded by description match) emits no
362
+ * event, so it reads as zero here — a 0 means "never explicitly invoked", not
363
+ * "never loaded". Skill invocations are recorded for Claude and Kimi (the
364
+ * `Skill`-tool harnesses); slash-commands are Claude-only.
365
+ *
366
+ * `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
367
+ * distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
368
+ * — deliberately not routed through buildSessionWhere so `--plugin rush` shows
369
+ * only rush's resources rather than every resource used by a rush-touching
370
+ * session.
371
+ */
372
+ export declare function queryResourceUsageStats(options: QueryOptions & {
373
+ kind?: 'skill' | 'command';
374
+ pluginFilter?: string;
375
+ order?: 'top' | 'bottom';
376
+ limit?: number;
377
+ }): ResourceStatRow[];
378
+ /**
379
+ * Coverage of the resource-usage signal: how many distinct sessions carry any
380
+ * row in session_resource_usage vs. the total indexed. A low ratio means the
381
+ * historical backfill (`agents sessions backfill resources`) hasn't run — the
382
+ * stats surface uses this to tell the user their zero-counts may just be
383
+ * un-scanned history, not genuine non-use.
384
+ */
385
+ export declare function resourceUsageCoverage(): {
386
+ covered: number;
387
+ total: number;
388
+ };
389
+ /** Outcome of a resource-usage backfill run. */
390
+ export interface ResourceBackfillResult {
391
+ /** Sessions considered (matched the filter, had a real transcript). */
392
+ scanned: number;
393
+ /** Sessions (re)parsed and written this run. */
394
+ updated: number;
395
+ /** Sessions already current at this extractor version, skipped. */
396
+ skipped: number;
397
+ /** Sessions whose transcript could not be stat'd or parsed. */
398
+ failed: number;
399
+ /** Total session_resource_usage rows written across updated sessions. */
400
+ resourceRows: number;
401
+ }
402
+ /**
403
+ * One-shot historical backfill of session_resource_usage (#12). The normal
404
+ * incremental scan writes resource usage only for sessions whose transcript it
405
+ * (re)parses; a session indexed before this feature shipped keeps a fresh
406
+ * scan_ledger row and is never re-derived, so its skill/slash-command tallies
407
+ * were never recorded. This walks the session index, re-parses each transcript
408
+ * FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
409
+ * claude/codex re-derive their tallies from scratch), writes the usage, and
410
+ * stamps resource_scan_ledger so reruns skip completed transcripts — the same
411
+ * independent-ledger shape `agents sessions backfill tools` uses.
412
+ *
413
+ * Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
414
+ * every agent uniformly, so there is no per-harness branch to keep in parity.
415
+ * Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
416
+ */
417
+ export declare function backfillResourceUsage(filter?: QueryOptions, onProgress?: (done: number, total: number) => void): ResourceBackfillResult;
327
418
  /** Who spawned a team: the orchestrator session, from its transcript. */
328
419
  export interface TeamSpawner {
329
420
  sessionId: string;