@phnx-labs/agents-cli 1.22.6 → 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 (77) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +7 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +35 -4
  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/projects.js +122 -107
  10. package/dist/commands/secrets.js +1 -1
  11. package/dist/commands/sessions-backfill.d.ts +33 -0
  12. package/dist/commands/sessions-backfill.js +83 -1
  13. package/dist/commands/sessions-stats.d.ts +36 -0
  14. package/dist/commands/sessions-stats.js +263 -0
  15. package/dist/commands/sessions.d.ts +1 -1
  16. package/dist/commands/sessions.js +21 -1
  17. package/dist/commands/view.d.ts +2 -1
  18. package/dist/commands/view.js +62 -11
  19. package/dist/index.js +1 -2
  20. package/dist/lib/activity.js +2 -2
  21. package/dist/lib/agents.js +68 -11
  22. package/dist/lib/analytics/recipes.js +11 -5
  23. package/dist/lib/browser/profiles.d.ts +15 -7
  24. package/dist/lib/browser/profiles.js +53 -12
  25. package/dist/lib/byok-usage.d.ts +38 -0
  26. package/dist/lib/byok-usage.js +117 -0
  27. package/dist/lib/capabilities.js +1 -1
  28. package/dist/lib/exec.d.ts +20 -0
  29. package/dist/lib/exec.js +73 -8
  30. package/dist/lib/feed-outcome.d.ts +1 -0
  31. package/dist/lib/feed-outcome.js +2 -0
  32. package/dist/lib/feed-post.js +1 -1
  33. package/dist/lib/feed-ranking.d.ts +1 -0
  34. package/dist/lib/feed-ranking.js +4 -0
  35. package/dist/lib/feed.d.ts +4 -1
  36. package/dist/lib/feed.js +25 -0
  37. package/dist/lib/hosts/passthrough.js +0 -1
  38. package/dist/lib/mcp.js +6 -1
  39. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  40. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  41. package/dist/lib/model-tiers.js +4 -1
  42. package/dist/lib/models.js +63 -0
  43. package/dist/lib/profiles.d.ts +42 -1
  44. package/dist/lib/profiles.js +50 -2
  45. package/dist/lib/project-focus.d.ts +9 -0
  46. package/dist/lib/project-focus.js +23 -0
  47. package/dist/lib/project-key.d.ts +1 -1
  48. package/dist/lib/project-key.js +1 -1
  49. package/dist/lib/project-probe.d.ts +18 -0
  50. package/dist/lib/project-probe.js +46 -0
  51. package/dist/lib/project-status.d.ts +56 -0
  52. package/dist/lib/project-status.js +125 -18
  53. package/dist/lib/resources/mcp.js +3 -0
  54. package/dist/lib/resources/types.d.ts +1 -1
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/secrets/audit.js +1 -1
  58. package/dist/lib/secrets/index.d.ts +1 -0
  59. package/dist/lib/secrets/index.js +29 -15
  60. package/dist/lib/secrets/remote.d.ts +1 -1
  61. package/dist/lib/secrets/remote.js +1 -1
  62. package/dist/lib/session/active.d.ts +2 -0
  63. package/dist/lib/session/bash-command.d.ts +2 -3
  64. package/dist/lib/session/db.d.ts +92 -1
  65. package/dist/lib/session/db.js +230 -1
  66. package/dist/lib/session/digest.d.ts +1 -1
  67. package/dist/lib/session/digest.js +1 -1
  68. package/dist/lib/share/publish.js +24 -0
  69. package/dist/lib/startup/command-registry.d.ts +0 -1
  70. package/dist/lib/startup/command-registry.js +0 -2
  71. package/dist/lib/subagents-registry.js +3 -0
  72. package/dist/lib/types.d.ts +11 -2
  73. package/dist/lib/usage.d.ts +5 -0
  74. package/dist/lib/usage.js +3 -3
  75. package/package.json +1 -1
  76. package/dist/commands/activity.d.ts +0 -87
  77. package/dist/commands/activity.js +0 -346
@@ -0,0 +1,263 @@
1
+ import chalk from 'chalk';
2
+ import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
3
+ import { queryResourceUsageStats, resourceUsageCoverage, } from '../lib/session/db.js';
4
+ import { listResources } from '../lib/resources.js';
5
+ import { discoverPlugins } from '../lib/plugins.js';
6
+ import { setHelpSections } from '../lib/help.js';
7
+ import { terminalWidth, truncateToWidth, padToWidth, stringWidth } from '../lib/session/width.js';
8
+ const DEFAULT_TOP = 20;
9
+ /**
10
+ * The both-ends "dead weight" set: installed resources whose identity
11
+ * (kind + name — name already embeds `plugin:short`, so it matches the stored
12
+ * invoked name) never appears in the invoked rows. Sorted kind then name.
13
+ * Pure set-difference so it is unit-testable without touching the filesystem.
14
+ */
15
+ export function diffZeroInvoked(installed, invoked) {
16
+ const invokedKeys = new Set(invoked.map(r => `${r.kind}:${r.name}`));
17
+ return installed
18
+ .filter(r => !invokedKeys.has(`${r.kind}:${r.name}`))
19
+ .sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind.localeCompare(b.kind)));
20
+ }
21
+ /**
22
+ * Enumerate the installed resource set the invoked rows are measured against:
23
+ * flat skills/commands from the DotAgents layers (`listResources`) plus each
24
+ * plugin's own skills/commands as `plugin:short` (invisible to the flat scan) —
25
+ * exactly the union `resolveResourceProvenance` reconciles against at write time.
26
+ */
27
+ function installedResources(kindFilter, pluginFilter, cwd) {
28
+ const kinds = kindFilter ? [kindFilter] : ['skill', 'command'];
29
+ const byKey = new Map();
30
+ const plugins = discoverPlugins({ cwd });
31
+ for (const kind of kinds) {
32
+ const listKind = kind === 'skill' ? 'skills' : 'commands';
33
+ // Flat (non-namespaced) resources are plugin-less; skip them when the caller
34
+ // narrows to one plugin.
35
+ if (!pluginFilter) {
36
+ for (const r of listResources(listKind, cwd)) {
37
+ byKey.set(`${kind}:${r.name}`, { kind, name: r.name, plugin: null, source: r.source });
38
+ }
39
+ }
40
+ for (const p of plugins) {
41
+ if (pluginFilter && p.name !== pluginFilter)
42
+ continue;
43
+ const names = kind === 'skill' ? p.skills : p.commands;
44
+ for (const short of names) {
45
+ const name = `${p.name}:${short}`;
46
+ byKey.set(`${kind}:${name}`, { kind, name, plugin: p.name, source: p.marketplace ?? null });
47
+ }
48
+ }
49
+ }
50
+ return [...byKey.values()];
51
+ }
52
+ export function registerSessionsStatsCommand(sessionsCmd) {
53
+ const stats = sessionsCmd
54
+ .command('stats')
55
+ .description('Which skills/commands you actually invoke, and which installed ones are dead weight.')
56
+ .option('--kind <kind>', 'Limit to one kind: skill or command')
57
+ .option('--plugin <name>', "Only this plugin's resources (r.plugin), not every resource a plugin-touching session used")
58
+ .option('-a, --agent <agent>', 'Only sessions from this agent (e.g. claude)')
59
+ .option('-p, --project <name>', 'Only sessions in this project')
60
+ .option('--machine <name>', 'Only sessions recorded on this machine (host)')
61
+ .option('--since <time>', 'Only sessions newer than this (e.g. 7d, 30d, or ISO date)')
62
+ .option('--top <n>', `How many ranked rows to show (default ${DEFAULT_TOP})`)
63
+ .option('--bottom', 'Rank the least-invoked resources first instead of the most-invoked')
64
+ .option('--zero', 'Show only installed-but-never-invoked resources (the dead weight)')
65
+ .option('--json', 'Output the stats as JSON')
66
+ .action(async (_opts, cmd) => {
67
+ await statsAction(cmd);
68
+ });
69
+ setHelpSections(stats, {
70
+ examples: `
71
+ # Most-invoked skills and commands, plus the installed ones you never invoke
72
+ agents sessions stats
73
+
74
+ # Just the dead weight — installed but never explicitly invoked
75
+ agents sessions stats --zero
76
+
77
+ # Skills only, last 30 days, machine-readable
78
+ agents sessions stats --kind skill --since 30d --json
79
+
80
+ # One plugin's resources, least-used first
81
+ agents sessions stats --plugin rush --bottom
82
+
83
+ # Backfill historical sessions first if coverage is low
84
+ agents sessions backfill resources
85
+ `,
86
+ notes: `
87
+ - The signal captures EXPLICIT invocations only: slash commands and \`Skill\` tool calls. An auto-triggered skill (loaded by description match) emits no event, so it reads as 0 — that means "never explicitly invoked", not "never loaded".
88
+ - Skill invocations are recorded for Claude and Kimi; slash-commands for Claude only. Other harnesses contribute nothing to these counts.
89
+ - Counts come from the SQLite index. New/changed sessions are recorded on their normal scan; run \`agents sessions backfill resources\` once to fold in historical sessions (a low coverage line means it hasn't run).
90
+ - --plugin filters the resource rows (this plugin's skills/commands), distinct from the top-level \`agents sessions --plugin\` which filters SESSIONS.
91
+ `,
92
+ });
93
+ }
94
+ async function statsAction(cmd) {
95
+ // The parent `sessions` command owns --agent/--project/--plugin/--since/--json
96
+ // and keeps parsing them past the subcommand name (it has a positional
97
+ // [query]), binding them to the PARENT — so read the merged view, not the
98
+ // action's own options. Same reason sessions-favorite.ts uses optsWithGlobals.
99
+ const opts = cmd.optsWithGlobals();
100
+ const kind = normalizeKind(opts.kind);
101
+ const sinceMs = opts.since ? parseTimeFilter(opts.since) : undefined;
102
+ const top = resolveTop(opts.top);
103
+ // Warm the incremental index (like `agents cost`) so recent sessions' usage is
104
+ // current before we read. Historical gaps still need the explicit backfill.
105
+ await discoverSessions({ all: true, since: opts.since, limit: 1 });
106
+ const filter = {};
107
+ if (opts.agent)
108
+ filter.agent = opts.agent;
109
+ if (opts.project)
110
+ filter.project = opts.project;
111
+ if (opts.machine)
112
+ filter.machine = opts.machine;
113
+ if (typeof sinceMs === 'number')
114
+ filter.sinceMs = sinceMs;
115
+ const statsOpts = {
116
+ ...filter,
117
+ kind,
118
+ pluginFilter: opts.plugin,
119
+ order: opts.bottom ? 'bottom' : 'top',
120
+ };
121
+ // Full invoked set (no limit) drives both the zero-invoked diff and the totals;
122
+ // the ranked view is a slice of it.
123
+ const allInvoked = queryResourceUsageStats(statsOpts);
124
+ const ranked = top > 0 ? allInvoked.slice(0, top) : allInvoked;
125
+ const installed = installedResources(kind, opts.plugin, process.cwd());
126
+ const zeroInvoked = diffZeroInvoked(installed, allInvoked);
127
+ const coverage = resourceUsageCoverage();
128
+ const totalInvocations = allInvoked.reduce((s, r) => s + r.invocations, 0);
129
+ if (opts.json) {
130
+ process.stdout.write(JSON.stringify({
131
+ schemaVersion: 1,
132
+ kind: 'sessions-stats',
133
+ generatedAt: new Date().toISOString(),
134
+ filters: {
135
+ kind: kind ?? null,
136
+ plugin: opts.plugin ?? null,
137
+ agent: opts.agent ?? null,
138
+ project: opts.project ?? null,
139
+ machine: opts.machine ?? null,
140
+ since: opts.since ?? null,
141
+ },
142
+ signal: {
143
+ explicitOnly: true,
144
+ note: 'Explicit invocations only (slash commands + Skill tool calls). Auto-triggered skills emit no event and read as 0. Skill invocations are recorded for Claude and Kimi; slash-commands for Claude only.',
145
+ },
146
+ coverage: {
147
+ sessionsWithUsage: coverage.covered,
148
+ sessionsIndexed: coverage.total,
149
+ },
150
+ totals: {
151
+ invokedResources: allInvoked.length,
152
+ invocations: totalInvocations,
153
+ zeroInvoked: zeroInvoked.length,
154
+ },
155
+ order: opts.bottom ? 'bottom' : 'top',
156
+ ranked: ranked.map(serializeRow),
157
+ zeroInvoked: zeroInvoked.map(r => ({
158
+ kind: r.kind,
159
+ name: r.name,
160
+ plugin: r.plugin,
161
+ source: r.source,
162
+ })),
163
+ }, null, 2) + '\n');
164
+ return;
165
+ }
166
+ renderHuman({ opts, kind, ranked, zeroInvoked, coverage, totalInvocations, allInvokedCount: allInvoked.length, top });
167
+ }
168
+ function serializeRow(r) {
169
+ return {
170
+ kind: r.kind,
171
+ name: r.name,
172
+ plugin: r.plugin,
173
+ source: r.source,
174
+ sessions: r.sessions,
175
+ invocations: r.invocations,
176
+ };
177
+ }
178
+ function normalizeKind(raw) {
179
+ if (raw === undefined)
180
+ return undefined;
181
+ const k = raw.toLowerCase();
182
+ if (k === 'skill' || k === 'skills')
183
+ return 'skill';
184
+ if (k === 'command' || k === 'commands')
185
+ return 'command';
186
+ console.error(chalk.red('error: --kind must be one of: skill, command'));
187
+ process.exit(1);
188
+ }
189
+ function resolveTop(raw) {
190
+ if (raw === undefined)
191
+ return DEFAULT_TOP;
192
+ const n = Number.parseInt(raw, 10);
193
+ if (!Number.isFinite(n) || n < 0) {
194
+ console.error(chalk.red('error: --top must be a non-negative integer'));
195
+ process.exit(1);
196
+ }
197
+ return n;
198
+ }
199
+ function renderHuman(args) {
200
+ const { opts, kind, ranked, zeroInvoked, coverage, totalInvocations, allInvokedCount } = args;
201
+ const out = [];
202
+ const scopeBits = [
203
+ kind ? kind : null,
204
+ opts.plugin ? `plugin ${opts.plugin}` : null,
205
+ opts.agent ? opts.agent : null,
206
+ opts.project ? `project ${opts.project}` : null,
207
+ opts.machine ? opts.machine : null,
208
+ opts.since ? `since ${opts.since}` : null,
209
+ ].filter(Boolean);
210
+ out.push(chalk.bold('Resource usage') +
211
+ chalk.gray(` · ${allInvokedCount} invoked · ${totalInvocations} invocation${totalInvocations !== 1 ? 's' : ''}` +
212
+ (scopeBits.length ? ` · ${scopeBits.join(' · ')}` : '')));
213
+ out.push(chalk.gray(` coverage: ${coverage.covered}/${coverage.total} sessions carry the signal`) +
214
+ (coverage.total > 0 && coverage.covered / coverage.total < 0.5
215
+ ? chalk.yellow(' — run `agents sessions backfill resources` to fold in history')
216
+ : ''));
217
+ out.push(chalk.gray(' signal: explicit invocations only (slash commands + Skill tool); auto-triggered skills read as 0; skills from Claude+Kimi, slash-commands Claude-only'));
218
+ out.push('');
219
+ // Ranked section (skipped by --zero).
220
+ if (!opts.zero) {
221
+ const heading = opts.bottom ? 'Least invoked' : 'Most invoked';
222
+ out.push(chalk.bold(heading));
223
+ if (ranked.length === 0) {
224
+ out.push(chalk.gray(' (no explicit invocations recorded — run the backfill, or nothing has been invoked yet)'));
225
+ }
226
+ else {
227
+ out.push(...renderRankedTable(ranked));
228
+ }
229
+ out.push('');
230
+ }
231
+ // Zero-invoked section (the dead weight).
232
+ out.push(chalk.bold('Installed but never invoked') + chalk.gray(` (${zeroInvoked.length})`));
233
+ if (zeroInvoked.length === 0) {
234
+ out.push(chalk.gray(' (every installed resource has at least one explicit invocation in this window)'));
235
+ }
236
+ else {
237
+ const cols = terminalWidth();
238
+ for (const r of zeroInvoked) {
239
+ const tag = chalk.gray(`[${r.kind}]`);
240
+ const owner = r.plugin ? chalk.gray(` ${r.plugin}`) : r.source ? chalk.gray(` ${r.source}`) : '';
241
+ const nameW = Math.max(12, cols - 12 - stringWidth(owner));
242
+ out.push(` ${tag} ${truncateToWidth(r.name, nameW)}${owner}`);
243
+ }
244
+ }
245
+ console.log(out.join('\n'));
246
+ }
247
+ function renderRankedTable(rows) {
248
+ const cols = terminalWidth();
249
+ const invW = Math.max(...rows.map(r => String(r.invocations).length), 3);
250
+ const sessW = Math.max(...rows.map(r => `${r.sessions} sess`.length), 6);
251
+ const lines = [];
252
+ for (const r of rows) {
253
+ const inv = String(r.invocations).padStart(invW);
254
+ const sess = padToWidth(`${r.sessions} sess`, sessW);
255
+ const tag = chalk.gray(`[${r.kind}]`);
256
+ const owner = r.plugin ? chalk.gray(` ${r.plugin}`) : r.source ? chalk.gray(` ${r.source}`) : '';
257
+ const prefix = ` ${chalk.green(inv)} ${chalk.gray(sess)} ${tag} `;
258
+ const suffix = owner;
259
+ const nameW = Math.max(12, cols - stringWidth(prefix) - stringWidth(suffix));
260
+ lines.push(prefix + truncateToWidth(r.name, nameW) + suffix);
261
+ }
262
+ return lines;
263
+ }
@@ -389,7 +389,7 @@ export declare function flatSessionRow(session: SessionMeta, live?: ActiveSessio
389
389
  export declare function maybeLiveIndex(options: SessionsOptions): Promise<Map<string, ActiveSession> | undefined>;
390
390
  /**
391
391
  * Group key for the overview: resolve the cwd through the same canonical
392
- * resolver the `agents activity` timeline groups by — a defined project's name
392
+ * resolver the `agents feed` timeline groups by — a defined project's name
393
393
  * when `defs` contains the cwd (multi-repo projects read as one group), else
394
394
  * the repo-level key, so a monorepo subdir (`<repo>/apps/cli`) reads as
395
395
  * `<repo>` in both views. Falls back to the indexed project name (stamped at
@@ -59,6 +59,7 @@ import { registerSessionsExportCommand } from './sessions-export.js';
59
59
  import { registerSessionsImportCommand } from './sessions-import.js';
60
60
  import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
61
61
  import { registerSessionsBackfillCommand } from './sessions-backfill.js';
62
+ import { registerSessionsStatsCommand } from './sessions-stats.js';
62
63
  import { runBrowserSessions } from '../lib/browser/sessions-list.js';
63
64
  import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
64
65
  const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
@@ -2210,7 +2211,7 @@ export async function maybeLiveIndex(options) {
2210
2211
  }
2211
2212
  /**
2212
2213
  * Group key for the overview: resolve the cwd through the same canonical
2213
- * resolver the `agents activity` timeline groups by — a defined project's name
2214
+ * resolver the `agents feed` timeline groups by — a defined project's name
2214
2215
  * when `defs` contains the cwd (multi-repo projects read as one group), else
2215
2216
  * the repo-level key, so a monorepo subdir (`<repo>/apps/cli`) reads as
2216
2217
  * `<repo>` in both views. Falls back to the indexed project name (stamped at
@@ -3643,6 +3644,18 @@ export function registerSessionsCommands(program) {
3643
3644
  # Show only what's running right now (terminals, teams, cloud, headless)
3644
3645
  agents sessions --active
3645
3646
 
3647
+ # --- Session lifecycle (one verb per intent) ---
3648
+ # Jump to a live session (attach its terminal, or open a tab + resume)
3649
+ agents sessions focus a1b2c3d4
3650
+ # Attach only — never fork a copy (old: sessions go)
3651
+ agents sessions focus a1b2c3d4 --attach-only
3652
+ # Interactive → headless (keep working unattended)
3653
+ agents sessions detach a1b2c3d4
3654
+ # Headless → interactive in this terminal
3655
+ agents sessions attach a1b2c3d4
3656
+ # Multi-select history and open each in a tab
3657
+ agents sessions resume
3658
+
3646
3659
  # The interactive list folds in other online machines automatically,
3647
3660
  # labelled by host with this machine first. Stay local with --local:
3648
3661
  agents sessions --local
@@ -3683,6 +3696,12 @@ export function registerSessionsCommands(program) {
3683
3696
  agents sessions --all "deploy script" --host box-a --host box-b
3684
3697
  `,
3685
3698
  notes: `
3699
+ Session lifecycle (pick one verb — they are not synonyms):
3700
+ focus [id] jump to a live session (attach, or open tab + resume)
3701
+ focus [id] --attach-only attach only; never fork (replaces sessions go)
3702
+ detach <id> interactive → headless continuation
3703
+ attach <id> headless → interactive in this terminal
3704
+ resume [query] multi-select history → open tabs (or run --resume <id>)
3686
3705
  - The interactive listing folds in your other online machines automatically (live over SSH, no sync) — each row is labelled by host, this machine first. Use --local to skip the fan-out; --json and single-id lookups stay local.
3687
3706
  - --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
3688
3707
  - --in-team matches both ends of the lineage: the session that ran 'agents teams create/add', and (with --teams) that team's teammates. In the interactive list, 't' cycles the same filter over the teams in view.
@@ -3720,6 +3739,7 @@ export function registerSessionsCommands(program) {
3720
3739
  registerSessionsMigrateCommand(sessionsCmd);
3721
3740
  registerSessionsMigrationsCommand(sessionsCmd);
3722
3741
  registerSessionsBackfillCommand(sessionsCmd);
3742
+ registerSessionsStatsCommand(sessionsCmd);
3723
3743
  }
3724
3744
  function formatNoSessionsMessage(showAll, project) {
3725
3745
  const projectQuery = project?.trim();
@@ -12,6 +12,7 @@ import type { AccountInfo } from '../lib/agents.js';
12
12
  import type { AgentId } from '../lib/types.js';
13
13
  import { type ConfiguredModelSource } from '../lib/models.js';
14
14
  import { type ProfileSummary } from '../lib/profiles.js';
15
+ import { type ByokUsageResult } from '../lib/byok-usage.js';
15
16
  /** Shared account identity formatter, re-exported for the view-specific tests. */
16
17
  export declare const accountColumnLabel: typeof accountDisplayLabel;
17
18
  /**
@@ -46,7 +47,7 @@ export declare function descriptionForPrefix(desc: string | undefined, prefix: s
46
47
  * `installedHosts` is the set of agent ids with a usable install; a harness whose
47
48
  * host is missing is flagged rather than silently listed as runnable.
48
49
  */
49
- export declare function renderHarnessBlocks(harnesses: ProfileSummary[], installedHosts: Set<AgentId>, showPaths: boolean): void;
50
+ export declare function renderHarnessBlocks(harnesses: ProfileSummary[], installedHosts: Set<AgentId>, showPaths: boolean, byokMap?: Map<string, ByokUsageResult>): void;
50
51
  /** Machine-readable entry for a single installed version. */
51
52
  export interface ViewJsonVersion {
52
53
  version: string;
@@ -27,6 +27,7 @@ import { getConfiguredRunStrategy } from '../lib/rotate.js';
27
27
  import { resolveRunDefaults } from '../lib/run-defaults.js';
28
28
  import { resolveConfiguredModel } from '../lib/models.js';
29
29
  import { listProfiles, profileExists, profileSummary, readProfile } from '../lib/profiles.js';
30
+ import { getByokUsageForHarness, hasByokProvider, renderByokBar } from '../lib/byok-usage.js';
30
31
  import { renderHarnessDetail } from './harness.js';
31
32
  import { loadManifest, isStale } from '../lib/staleness/index.js';
32
33
  import { confirm } from '@inquirer/prompts';
@@ -67,9 +68,28 @@ function getHarnesses(filterAgentId) {
67
68
  .filter((profile) => !filterAgentId || profile.host.agent === filterAgentId)
68
69
  .map(profileSummary);
69
70
  }
70
- /** "via <host> <version>" — which native harness actually executes this one. */
71
- function harnessHostTag(harness) {
72
- return harness.hostVersion ? `via ${harness.agent} ${harness.hostVersion}` : `via ${harness.agent}`;
71
+ /** Version-first label: "<version> (forked from <host>[, tracks default])" */
72
+ function harnessVersionLabel(harness, globalDefault) {
73
+ const version = harness.hostVersion ?? globalDefault;
74
+ if (version) {
75
+ const trailer = harness.hostVersion
76
+ ? chalk.gray(` (forked from ${harness.agent})`)
77
+ : chalk.gray(` (forked from ${harness.agent}, `) +
78
+ chalk.green(`tracks default`) +
79
+ chalk.gray(`)`);
80
+ return `${version}${trailer}`;
81
+ }
82
+ return chalk.gray(`(forked from ${harness.agent})`);
83
+ }
84
+ /** One-hop or two-hop fork origin label for the harness block header. */
85
+ function buildHarnessOrigin(harness, allHarnesses) {
86
+ if (!harness.forkedFrom || harness.forkedFrom === harness.agent)
87
+ return 'custom';
88
+ const parent = allHarnesses.find((h) => h.name === harness.forkedFrom);
89
+ if (parent?.forkedFrom) {
90
+ return `custom · forked from ${harness.forkedFrom} -> ${parent.forkedFrom}`;
91
+ }
92
+ return `custom · forked from ${harness.forkedFrom}`;
73
93
  }
74
94
  /**
75
95
  * Resolve a resource path to something the IDE can open inline. When `p` is a
@@ -172,24 +192,23 @@ export function descriptionForPrefix(desc, prefix) {
172
192
  * `installedHosts` is the set of agent ids with a usable install; a harness whose
173
193
  * host is missing is flagged rather than silently listed as runnable.
174
194
  */
175
- export function renderHarnessBlocks(harnesses, installedHosts, showPaths) {
195
+ export function renderHarnessBlocks(harnesses, installedHosts, showPaths, byokMap) {
176
196
  if (harnesses.length === 0)
177
197
  return;
178
198
  const modelWidth = Math.max(...harnesses.map((h) => h.model.length));
179
199
  const authWidth = Math.max(...harnesses.map((h) => h.auth.length));
180
200
  for (const harness of harnesses) {
181
- // The `via <host>` tag on the row already names a native fork parent, so
182
- // only a fork of another custom harness adds lineage worth printing.
183
- const origin = harness.forkedFrom && harness.forkedFrom !== harness.agent
184
- ? `custom · forked from ${harness.forkedFrom}`
185
- : 'custom';
201
+ const origin = buildHarnessOrigin(harness, harnesses);
186
202
  const missingHost = installedHosts.has(harness.agent)
187
203
  ? ''
188
204
  : chalk.yellow(` (host ${harness.agent} not installed)`);
189
205
  console.log(` ${chalk.bold(harness.label)}${chalk.gray(` (${origin})`)}${missingHost}`);
206
+ const versionTag = harnessVersionLabel(harness, getGlobalDefault(harness.agent));
207
+ const byokEntry = byokMap?.get(harness.name);
208
+ const byokBar = byokEntry ? ` ${renderByokBar(byokEntry)}` : '';
190
209
  console.log(` ${chalk.yellow(harness.model.padEnd(modelWidth))} ` +
191
210
  `${chalk.cyan(harness.auth.padEnd(authWidth))} ` +
192
- chalk.gray(harnessHostTag(harness)));
211
+ versionTag + byokBar);
193
212
  if (showPaths)
194
213
  console.log(chalk.gray(` ${harness.path}`));
195
214
  console.log();
@@ -616,7 +635,39 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
616
635
  // Custom harnesses sit in the same list as the native ones — they are run the
617
636
  // same way (`agents run <name>`), so they read as their own agent type rather
618
637
  // than as an indented row under whichever host CLI executes them.
619
- renderHarnessBlocks(harnesses, installedHosts, showPaths);
638
+ const byokMap = new Map();
639
+ const byKeychainItem = new Map();
640
+ for (const h of harnesses) {
641
+ if (!h.provider || !hasByokProvider(h.provider))
642
+ continue;
643
+ let prof = null;
644
+ try {
645
+ prof = readProfile(h.name);
646
+ }
647
+ catch {
648
+ continue;
649
+ }
650
+ if (!prof.auth)
651
+ continue;
652
+ const item = prof.auth.keychainItem;
653
+ const existing = byKeychainItem.get(item);
654
+ if (existing) {
655
+ existing.names.push(h.name);
656
+ }
657
+ else {
658
+ byKeychainItem.set(item, { profile: prof, names: [h.name] });
659
+ }
660
+ }
661
+ if (byKeychainItem.size > 0) {
662
+ const fetched = await Promise.all([...byKeychainItem.values()].map(({ profile, names }) => getByokUsageForHarness(profile, { forceRefresh: viewOpts?.forceRefresh }).then((result) => ({ result, names }))));
663
+ for (const { result, names } of fetched) {
664
+ if (!result?.budget)
665
+ continue;
666
+ for (const name of names)
667
+ byokMap.set(name, result);
668
+ }
669
+ }
670
+ renderHarnessBlocks(harnesses, installedHosts, showPaths, byokMap.size > 0 ? byokMap : undefined);
620
671
  // Show globally installed (not managed) agents
621
672
  if (globallyInstalled.length > 0) {
622
673
  console.log(chalk.bold('Not Managed by Agents CLI\n'));
package/dist/index.js CHANGED
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
94
94
  // module on each invocation (which loaded the whole ~50-module tree before the
95
95
  // first byte of output), the registry maps a command name to a thunk that
96
96
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
97
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadActivity, loadMailboxes, } from './lib/startup/command-registry.js';
97
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
98
98
  import { applyGlobalHelpConventions } from './lib/help.js';
99
99
  import { renderWhatsNew } from './lib/whats-new.js';
100
100
  import { getCliLaunch } from './lib/cli-entry.js';
@@ -972,7 +972,6 @@ async function registerAllEagerCommands() {
972
972
  await reg(loadFunnel);
973
973
  registerHqTombstoneCommand(program);
974
974
  await reg(loadFeed);
975
- await reg(loadActivity);
976
975
  await reg(loadMailboxes);
977
976
  await reg(loadSsh);
978
977
  registerJobsCronAliasCommand(program, 'jobs');
@@ -300,7 +300,7 @@ export function readActivityAsEventRecords(opts = {}) {
300
300
  return readRecentActivity(opts).map(activityEventToRecord);
301
301
  }
302
302
  // ---------------------------------------------------------------------------
303
- // Rendering (shared by `agents activity` and `agents feed`)
303
+ // Rendering (shared by the feed activity lane and `agents feed --filter updates`)
304
304
  // ---------------------------------------------------------------------------
305
305
  /** Glyph + color + human label per event, so the lane reads at a glance. */
306
306
  export const EVENT_STYLE = {
@@ -669,7 +669,7 @@ export function formatActivityGroupMeta(group, opts = {}) {
669
669
  * a logging hiccup never blocks a tool call.
670
670
  */
671
671
  export const ACTIVITY_LOG_HOOK_SCRIPT = String.raw `#!/usr/bin/env python3
672
- """Append agent-activity events for 'agents feed' / 'agents activity'.
672
+ """Append agent-activity events for 'agents feed'.
673
673
 
674
674
  Bound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,
675
675
  MultiEdit, TodoWrite, update_plan, TaskUpdate, todo_write, TaskCreate). One