@phnx-labs/agents-cli 1.21.0 → 1.21.2

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 (102) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +2 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/doctor.js +5 -2
  5. package/dist/commands/feed.js +63 -40
  6. package/dist/commands/hooks.js +9 -45
  7. package/dist/commands/menubar.js +24 -24
  8. package/dist/commands/message.js +23 -3
  9. package/dist/commands/monitors.js +8 -6
  10. package/dist/commands/perf.d.ts +13 -0
  11. package/dist/commands/perf.js +80 -23
  12. package/dist/commands/projects.d.ts +11 -0
  13. package/dist/commands/projects.js +153 -21
  14. package/dist/commands/routines.js +46 -1
  15. package/dist/commands/ssh.js +69 -0
  16. package/dist/commands/trends.d.ts +2 -0
  17. package/dist/commands/trends.js +158 -0
  18. package/dist/commands/usage.d.ts +4 -4
  19. package/dist/commands/view.d.ts +6 -0
  20. package/dist/commands/view.js +90 -45
  21. package/dist/index.js +14 -1
  22. package/dist/lib/activity.d.ts +6 -0
  23. package/dist/lib/activity.js +3 -0
  24. package/dist/lib/agents.js +2 -2
  25. package/dist/lib/analytics/dashboard.d.ts +11 -0
  26. package/dist/lib/analytics/dashboard.js +31 -0
  27. package/dist/lib/analytics/recipes.d.ts +32 -0
  28. package/dist/lib/analytics/recipes.js +316 -0
  29. package/dist/lib/analytics/usage-db.d.ts +84 -0
  30. package/dist/lib/analytics/usage-db.js +301 -0
  31. package/dist/lib/browser/service.js +18 -0
  32. package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
  33. package/dist/lib/channels/resolve.d.ts +18 -1
  34. package/dist/lib/channels/resolve.js +15 -5
  35. package/dist/lib/cli-resources.d.ts +20 -0
  36. package/dist/lib/cli-resources.js +48 -1
  37. package/dist/lib/daemon.js +51 -14
  38. package/dist/lib/devices/health-report.d.ts +5 -0
  39. package/dist/lib/devices/health-report.js +3 -0
  40. package/dist/lib/feed-broadcast.d.ts +93 -12
  41. package/dist/lib/feed-broadcast.js +235 -30
  42. package/dist/lib/feed-post.d.ts +16 -2
  43. package/dist/lib/feed-post.js +35 -7
  44. package/dist/lib/feed.js +1 -1
  45. package/dist/lib/fleet-cache.d.ts +37 -0
  46. package/dist/lib/fleet-cache.js +40 -0
  47. package/dist/lib/fleet-status.d.ts +53 -0
  48. package/dist/lib/fleet-status.js +120 -0
  49. package/dist/lib/friction-heuristics.d.ts +32 -0
  50. package/dist/lib/friction-heuristics.js +47 -0
  51. package/dist/lib/hooks/cache.js +28 -6
  52. package/dist/lib/hooks/profile.d.ts +8 -0
  53. package/dist/lib/hooks/profile.js +14 -4
  54. package/dist/lib/hooks.d.ts +16 -1
  55. package/dist/lib/hooks.js +133 -18
  56. package/dist/lib/linear-cache.d.ts +63 -0
  57. package/dist/lib/linear-cache.js +146 -0
  58. package/dist/lib/linear-project-counts.d.ts +35 -5
  59. package/dist/lib/linear-project-counts.js +61 -16
  60. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  61. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
  62. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  63. package/dist/lib/menubar/install-menubar.d.ts +7 -0
  64. package/dist/lib/menubar/install-menubar.js +36 -6
  65. package/dist/lib/monitors/config.d.ts +1 -1
  66. package/dist/lib/monitors/dispatch.d.ts +6 -2
  67. package/dist/lib/monitors/dispatch.js +10 -15
  68. package/dist/lib/notify.d.ts +42 -6
  69. package/dist/lib/notify.js +41 -32
  70. package/dist/lib/perf/db.d.ts +6 -1
  71. package/dist/lib/perf/db.js +35 -5
  72. package/dist/lib/perf/types.d.ts +10 -0
  73. package/dist/lib/project-doctor.d.ts +36 -0
  74. package/dist/lib/project-doctor.js +45 -0
  75. package/dist/lib/project-import.d.ts +11 -1
  76. package/dist/lib/project-import.js +17 -3
  77. package/dist/lib/project-status.d.ts +25 -5
  78. package/dist/lib/project-status.js +48 -6
  79. package/dist/lib/rotate.d.ts +27 -0
  80. package/dist/lib/rotate.js +44 -17
  81. package/dist/lib/routines.d.ts +16 -0
  82. package/dist/lib/routines.js +39 -0
  83. package/dist/lib/runner.js +34 -0
  84. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  85. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  86. package/dist/lib/secrets/usage-db.d.ts +3 -63
  87. package/dist/lib/secrets/usage-db.js +46 -186
  88. package/dist/lib/session/db.d.ts +2 -1
  89. package/dist/lib/session/db.js +14 -3
  90. package/dist/lib/session/discover.d.ts +3 -0
  91. package/dist/lib/session/discover.js +8 -0
  92. package/dist/lib/session/types.d.ts +1 -0
  93. package/dist/lib/startup/command-registry.d.ts +1 -0
  94. package/dist/lib/startup/command-registry.js +2 -0
  95. package/dist/lib/state.d.ts +31 -3
  96. package/dist/lib/state.js +53 -10
  97. package/dist/lib/types.d.ts +14 -4
  98. package/dist/lib/usage-refresh.d.ts +106 -0
  99. package/dist/lib/usage-refresh.js +238 -0
  100. package/dist/lib/usage.d.ts +152 -17
  101. package/dist/lib/usage.js +393 -79
  102. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { addHostOption } from '../lib/hosts/option.js';
2
2
  import chalk from 'chalk';
3
- import { visibleWidth, termLink } from '../lib/format.js';
3
+ import { termLink } from '../lib/format.js';
4
4
  import ora from 'ora';
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
@@ -31,9 +31,31 @@ import { renderHarnessDetail } from './harness.js';
31
31
  import { loadManifest, isStale } from '../lib/staleness/index.js';
32
32
  import { confirm } from '@inquirer/prompts';
33
33
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
34
- import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
34
+ import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
35
35
  /** Shared account identity formatter, re-exported for the view-specific tests. */
36
36
  export const accountColumnLabel = accountDisplayLabel;
37
+ /**
38
+ * Overview (`agents view` with no agent filter) caps compact usage windows so
39
+ * multi-meter agents (Antigravity's four model quotas, Droid's three buckets)
40
+ * cannot force every row to pad past the terminal width and wrap. Single-agent
41
+ * views leave the cap unset and show every blocking window.
42
+ */
43
+ const OVERVIEW_MAX_USAGE_WINDOWS = 2;
44
+ /** Fixed width for the last-active column ("just now", "8h ago", "2d ago"). */
45
+ const LAST_ACTIVE_COL_WIDTH = 10;
46
+ /**
47
+ * Join fixed view columns with a consistent two-space gutter. Empty trailing
48
+ * columns are dropped so a row without an auth chip does not grow a dangling
49
+ * gutter, but interior empties stay padded so later columns stay aligned.
50
+ */
51
+ export function joinViewColumns(cols) {
52
+ // Trim only pure-trailing empty strings so auth/status can be absent without
53
+ // shifting earlier columns for rows that do carry them.
54
+ let end = cols.length;
55
+ while (end > 0 && cols[end - 1] === '')
56
+ end--;
57
+ return cols.slice(0, end).join(' ');
58
+ }
37
59
  /**
38
60
  * Custom harnesses (the `~/.agents/profiles/*.yml` bundles), sorted by name and
39
61
  * optionally narrowed to the ones that run on one host agent. YAMLs that fail
@@ -246,6 +268,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
246
268
  : 'Checking installed agents...';
247
269
  const spinner = ora({ text: spinnerText, isSilent: !process.stdout.isTTY }).start();
248
270
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
271
+ // Overview caps meter count; single-agent view shows every blocking window.
272
+ const usageWindowCap = filterAgentId ? undefined : OVERVIEW_MAX_USAGE_WINDOWS;
249
273
  // A globally-installed CLI is superseded only by a NORMAL managed version — that
250
274
  // is when agents-cli owns the launcher and a "global" row would just be our own
251
275
  // shim reported back. `--isolated` promises the opposite: no default, no bare
@@ -265,7 +289,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
265
289
  const cliStates = Object.fromEntries(await Promise.all(agentsToShow
266
290
  .filter((agentId) => !hasNonIsolatedVersion(agentId))
267
291
  .map(async (agentId) => [agentId, await getUnmanagedCliState(agentId)])));
268
- spinner.stop();
269
292
  const showPaths = !!filterAgentId;
270
293
  const harnesses = getHarnesses(filterAgentId);
271
294
  // Auto-heal stale versioned aliases. Pre-v2 aliases (e.g. pre-CLAUDE_CONFIG_DIR
@@ -287,11 +310,15 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
287
310
  }
288
311
  }
289
312
  // Shim healing is silent — users don't need to know about internal repairs
290
- console.log(chalk.bold('Installed Agent CLIs\n'));
291
313
  const selfHost = machineId();
292
314
  // Read the auth-health cache once (not per version row — see the batching note above).
293
315
  const authCache = readAuthHealthCache();
294
- // Pre-fetch account info for all versions in parallel
316
+ // Pre-fetch account info for all versions in parallel. Spinner stays up through
317
+ // account + usage so a multi-account cold path doesn't leave a blank terminal
318
+ // after "Checking…" vanishes (the hang the screenshots caught).
319
+ spinner.text = filterAgentId
320
+ ? `Loading ${agentLabel(filterAgentId)} accounts...`
321
+ : 'Loading accounts and usage...';
295
322
  const infoFetches = [];
296
323
  const globalInfoFetches = [];
297
324
  for (const agentId of agentsToShow) {
@@ -329,6 +356,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
329
356
  // or org scope, not a specific installed version. Version homes cache those
330
357
  // values independently, so older installs can show stale values. Reuse the
331
358
  // freshest cache entry per stable usage identity and keep lastActive per version.
359
+ // Goes through the unified usage core (SWR cache + concurrency cap + timeout).
332
360
  const { canonicalByUsageKey, usageByKey } = await getUsageInfoByIdentity([
333
361
  ...infoResults.map(({ agentId, home, version, info }) => ({
334
362
  agentId,
@@ -342,6 +370,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
342
370
  info,
343
371
  })),
344
372
  ], { forceRefresh: viewOpts?.forceRefresh });
373
+ spinner.stop();
374
+ console.log(chalk.bold('Installed Agent CLIs\n'));
345
375
  const mergeCanonical = (info) => {
346
376
  const key = getUsageLookupKey(info);
347
377
  if (!key)
@@ -441,7 +471,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
441
471
  }
442
472
  }
443
473
  }
444
- // Second pass: compute max visible usage + status widths (now that maxPlanWidth is settled)
474
+ // Second pass: compute max visible usage + status widths (now that maxPlanWidth is settled).
475
+ // stringWidth (not String.length) so chalk + block-bar glyphs pad correctly.
445
476
  for (const agentId of versionManaged) {
446
477
  const versions = listInstalledVersions(agentId);
447
478
  for (const v of versions) {
@@ -451,10 +482,14 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
451
482
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
452
483
  const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot;
453
484
  const usageUnverified = !!usageInfo?.snapshot && !!usageInfo.error;
454
- const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable, unverified: usageUnverified });
455
- maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageStr));
485
+ const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
486
+ unavailable: usageUnavailable,
487
+ unverified: usageUnverified,
488
+ maxWindows: usageWindowCap,
489
+ });
490
+ maxUsageWidth = Math.max(maxUsageWidth, stringWidth(usageStr));
456
491
  const statusStr = formatUsageStatusBadge(info?.usageStatus);
457
- maxStatusWidth = Math.max(maxStatusWidth, visibleWidth(statusStr));
492
+ maxStatusWidth = Math.max(maxStatusWidth, stringWidth(statusStr));
458
493
  }
459
494
  }
460
495
  for (const agentId of versionManaged) {
@@ -496,18 +531,26 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
496
531
  const vInfo = rawInfo ? mergeCanonical(rawInfo) : undefined;
497
532
  const usageKey = getUsageLookupKey(vInfo);
498
533
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
499
- // Build columns, trimming trailing whitespace when columns are empty
534
+ // Fixed columns for every signed-in row so status / lastActive / auth
535
+ // stay vertically aligned across agents — even when this row has no
536
+ // usage bars or no rate-limit badge. Skipping empty columns mid-table
537
+ // was what made the multi-agent view look unjustified next to
538
+ // `agents view claude`.
500
539
  const parts = [` ${label}`];
501
540
  // Configured model — same priority as the version, right beside it.
502
541
  if (maxModelWidth > 0) {
503
542
  const model = modelByKey.get(`${agentId}:${version}`) ?? '';
504
- parts.push(chalk.yellow(model.padEnd(maxModelWidth)));
543
+ parts.push(chalk.yellow(padToWidth(model, maxModelWidth)));
505
544
  }
506
545
  const hasEmail = !!vInfo?.email;
507
546
  const signedIn = !!vInfo?.signedIn;
508
547
  const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
509
548
  const usageUnverified = !!usageInfo?.snapshot && !!usageInfo.error;
510
- const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable, unverified: usageUnverified });
549
+ const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
550
+ unavailable: usageUnavailable,
551
+ unverified: usageUnverified,
552
+ maxWindows: usageWindowCap,
553
+ });
511
554
  const hasUsage = usageStr.length > 0;
512
555
  // Only show lastActive for versions with an actual logged-in account.
513
556
  // Otherwise it reflects install time (misleading "just now" for fresh installs).
@@ -534,25 +577,21 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
534
577
  : '(logged out — log in with: ' + loginHint(agentId) + ')'));
535
578
  }
536
579
  else {
537
- if (hasEmail || hasUsage || hasActive || signedIn) {
538
- // Signed-in agents without a local email show their account id
539
- // (Kimi's user_id) or a "signed in" placeholder (Antigravity) so they
540
- // read as logged in, not blank.
541
- const display = accountColumnLabel(vInfo);
542
- const emailCol = display.padEnd(maxEmail);
543
- parts.push(display ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
544
- }
545
- if (hasUsage || hasActive) {
546
- const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageStr)));
547
- parts.push(usageStr + usagePad);
580
+ // Always emit account / usage / status / lastActive columns once any
581
+ // signed-in row exists in the table (widths are global). Empty cells
582
+ // are space-padded so later columns do not drift left.
583
+ const display = accountColumnLabel(vInfo);
584
+ parts.push(display ? chalk.cyan(padToWidth(display, maxEmail)) : ' '.repeat(maxEmail));
585
+ if (maxUsageWidth > 0) {
586
+ parts.push(padToWidth(usageStr, maxUsageWidth));
548
587
  }
549
- const statusStr = formatUsageStatusBadge(vInfo?.usageStatus);
550
588
  if (maxStatusWidth > 0) {
551
- const statusPad = ' '.repeat(Math.max(0, maxStatusWidth - visibleWidth(statusStr)));
552
- parts.push(statusStr + statusPad);
589
+ const statusStr = formatUsageStatusBadge(vInfo?.usageStatus);
590
+ parts.push(padToWidth(statusStr, maxStatusWidth));
553
591
  }
554
- if (hasActive)
555
- parts.push(activeStr);
592
+ // Fixed-width lastActive so the auth chip (●/○/◐) lines up even when
593
+ // some rows have no email-derived lastActive.
594
+ parts.push(hasActive ? padToWidth(activeStr, LAST_ACTIVE_COL_WIDTH) : ' '.repeat(LAST_ACTIVE_COL_WIDTH));
556
595
  }
557
596
  if (runDefaultBits.length > 0) {
558
597
  parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
@@ -560,7 +599,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
560
599
  const authChip = liveAuthChip(authCache, selfHost, agentId, version);
561
600
  if (authChip)
562
601
  parts.push(authChip);
563
- console.log(parts.join(' '));
602
+ console.log(joinViewColumns(parts));
564
603
  if (showPaths) {
565
604
  const versionDir = getVersionDir(agentId, version);
566
605
  console.log(chalk.gray(` ${versionDir}`));
@@ -586,18 +625,25 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
586
625
  const cliState = cliStates[agentId];
587
626
  return `${cliState?.version || 'installed'} (global)`.length;
588
627
  }));
589
- // Pre-pass: max badge width so rows with `lastActive` line up whether or
590
- // not THIS row carries a throttle badge. Without this, the row that DOES
591
- // have "out of credits" shifts every other row's `lastActive` left by
592
- // ~16 chars, exactly what the version-managed block at maxStatusWidth
593
- // already solves above.
628
+ // Pre-pass: max badge/usage/email widths so columns line up the same way
629
+ // the version-managed block does (stringWidth for chalk-aware padding).
594
630
  let gMaxStatusWidth = 0;
631
+ let gMaxUsageWidth = 0;
632
+ let gMaxEmail = 0;
595
633
  for (const agentId of globallyInstalled) {
596
634
  const gInfoRaw = globalInfoMap.get(agentId);
597
635
  const gInfo = gInfoRaw ? mergeCanonical(gInfoRaw) : undefined;
598
- const w = visibleWidth(formatUsageStatusBadge(gInfo?.usageStatus));
599
- if (w > gMaxStatusWidth)
600
- gMaxStatusWidth = w;
636
+ gMaxStatusWidth = Math.max(gMaxStatusWidth, stringWidth(formatUsageStatusBadge(gInfo?.usageStatus)));
637
+ const gUsageKey = getUsageLookupKey(gInfo);
638
+ const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
639
+ const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
640
+ unverified: !!gUsage?.snapshot && !!gUsage.error,
641
+ maxWindows: usageWindowCap,
642
+ });
643
+ gMaxUsageWidth = Math.max(gMaxUsageWidth, stringWidth(gUsageStr));
644
+ const gDisplay = accountColumnLabel(gInfo);
645
+ if (gDisplay)
646
+ gMaxEmail = Math.max(gMaxEmail, gDisplay.length);
601
647
  }
602
648
  for (const agentId of globallyInstalled) {
603
649
  const agent = AGENTS[agentId];
@@ -613,22 +659,21 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
613
659
  const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
614
660
  const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
615
661
  unverified: !!gUsage?.snapshot && !!gUsage.error,
662
+ maxWindows: usageWindowCap,
616
663
  });
617
664
  const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
618
665
  if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
619
666
  const gDisplay = accountColumnLabel(gInfo);
620
- parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
667
+ parts.push(gDisplay ? chalk.cyan(padToWidth(gDisplay, gMaxEmail)) : ' '.repeat(gMaxEmail));
621
668
  }
622
- if (gUsageStr || gActiveStr)
623
- parts.push(gUsageStr);
624
- const gStatusStr = formatUsageStatusBadge(gInfo?.usageStatus);
669
+ if (gMaxUsageWidth > 0)
670
+ parts.push(padToWidth(gUsageStr, gMaxUsageWidth));
625
671
  if (gMaxStatusWidth > 0) {
626
- const statusPad = ' '.repeat(Math.max(0, gMaxStatusWidth - visibleWidth(gStatusStr)));
627
- parts.push(gStatusStr + statusPad);
672
+ parts.push(padToWidth(formatUsageStatusBadge(gInfo?.usageStatus), gMaxStatusWidth));
628
673
  }
629
674
  if (gActiveStr)
630
- parts.push(gActiveStr);
631
- console.log(parts.join(' '));
675
+ parts.push(padToWidth(gActiveStr, LAST_ACTIVE_COL_WIDTH));
676
+ console.log(joinViewColumns(parts));
632
677
  if (showPaths && cliState?.path) {
633
678
  console.log(chalk.gray(` ${cliState.path}`));
634
679
  }
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, 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, loadActivity, 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';
@@ -197,6 +197,18 @@ program.hook('postAction', (_thisCommand, actionCommand) => {
197
197
  command,
198
198
  ...(durationMs !== undefined ? { durationMs } : {}),
199
199
  });
200
+ if (parts[0] === 'run') {
201
+ const agentName = actionCommand.args?.[0] ? String(actionCommand.args[0]).split('@')[0] : 'run';
202
+ void import('./lib/analytics/usage-db.js').then(({ recordUsage }) => {
203
+ recordUsage({
204
+ kind: 'agent',
205
+ name: agentName || 'run',
206
+ event: 'invoke',
207
+ source: 'cli',
208
+ meta: durationMs !== undefined ? { durationMs } : undefined,
209
+ });
210
+ }).catch(() => { });
211
+ }
200
212
  // Disposable perf warehouse — fail-soft spool append (no SQLite on this path).
201
213
  if (durationMs !== undefined && parts[0] !== 'perf') {
202
214
  void import('./lib/perf/spool.js').then(({ recordSample }) => {
@@ -933,6 +945,7 @@ async function registerAllEagerCommands() {
933
945
  await reg(loadUsage);
934
946
  await reg(loadCost);
935
947
  await reg(loadPerf);
948
+ await reg(loadTrends);
936
949
  await reg(loadOutput);
937
950
  await reg(loadBudget);
938
951
  await reg(loadAlias);
@@ -89,6 +89,12 @@ export interface ActivityEvent {
89
89
  tool?: string;
90
90
  /** One-line human summary (plan title, PR command, sub-agent role, status text). */
91
91
  detail?: string;
92
+ /**
93
+ * Short subject for deliberate status posts (`feed post --title`). Phone
94
+ * broadcasts put this on the first line; `detail` is the body. Optional on
95
+ * older events that only carried `detail`.
96
+ */
97
+ title?: string;
92
98
  /** Extracted URL when the event has one (e.g. the opened PR). */
93
99
  url?: string;
94
100
  /** Auto-stamped process identity for deliberate posts (from pid registry / env). */
@@ -141,6 +141,7 @@ function parseLine(line) {
141
141
  kind: parsed.kind,
142
142
  tool: parsed.tool,
143
143
  detail: parsed.detail,
144
+ title: typeof parsed.title === 'string' ? parsed.title : undefined,
144
145
  url: parsed.url,
145
146
  pid: typeof parsed.pid === 'number' ? parsed.pid : undefined,
146
147
  launchId: parsed.launchId,
@@ -398,6 +399,8 @@ export function formatProgressUpdate(ev, opts = {}) {
398
399
  ].filter((c) => Boolean(c));
399
400
  if (chips.length > 0)
400
401
  lines.push(` ${chalk.gray(chips.join(' · '))}`);
402
+ if (ev.title)
403
+ lines.push(` ${chalk.white.bold(ev.title)}`);
401
404
  if (ev.detail)
402
405
  lines.push(` ${chalk.white(`"${ev.detail}"`)}`);
403
406
  if (ev.attachments?.length) {
@@ -1754,8 +1754,8 @@ export async function getAccountInfo(agentId, home) {
1754
1754
  }
1755
1755
  }
1756
1756
  // Fresh window for the cached session walk. Matches USAGE_CACHE_FRESH_MS in
1757
- // usage.ts so a launch storm reuses both probes for the same period.
1758
- const LAST_ACTIVE_CACHE_FRESH_MS = 2 * 60 * 1000;
1757
+ // usage.ts (5 minutes) so a launch storm reuses both probes for the same period.
1758
+ const LAST_ACTIVE_CACHE_FRESH_MS = 5 * 60 * 1000;
1759
1759
  const getLastActiveCachePath = () => path.join(getCacheDir(), 'last-active.json');
1760
1760
  /**
1761
1761
  * Determine when the agent was last used by checking session file mtimes,
@@ -0,0 +1,11 @@
1
+ import { RECIPE_IDS, runRecipe, trendsWindow, type RecipeId, type RecipeSection, type TrendsWindow } from './recipes.js';
2
+ export interface TrendsDashboard {
3
+ window: TrendsWindow;
4
+ durationMs: number;
5
+ sections: RecipeSection[];
6
+ }
7
+ export declare function buildTrendsDashboard(opts?: {
8
+ days?: number;
9
+ ids?: RecipeId[];
10
+ }): TrendsDashboard;
11
+ export { trendsWindow, runRecipe, RECIPE_IDS, type RecipeId, type RecipeSection };
@@ -0,0 +1,31 @@
1
+ import { RECIPE_IDS, runRecipe, trendsWindow, } from './recipes.js';
2
+ const DEFAULT_ORDER = [
3
+ 'harness-mix',
4
+ 'model-mix',
5
+ 'session-volume',
6
+ 'token-ratio',
7
+ 'tools-per-session',
8
+ 'secrets-hot',
9
+ 'browser-activity',
10
+ 'resource-mix',
11
+ ];
12
+ export function buildTrendsDashboard(opts = {}) {
13
+ const t0 = Date.now();
14
+ const win = trendsWindow(opts.days ?? 7);
15
+ const ids = opts.ids ?? DEFAULT_ORDER;
16
+ const sections = [];
17
+ for (const id of ids) {
18
+ if (!RECIPE_IDS.includes(id))
19
+ continue;
20
+ const section = runRecipe(id, win);
21
+ if (section.empty)
22
+ continue;
23
+ sections.push(section);
24
+ }
25
+ return {
26
+ window: win,
27
+ durationMs: Date.now() - t0,
28
+ sections,
29
+ };
30
+ }
31
+ export { trendsWindow, runRecipe, RECIPE_IDS };
@@ -0,0 +1,32 @@
1
+ import { type UsageKind } from './usage-db.js';
2
+ export interface TrendsWindow {
3
+ days: number;
4
+ sinceIso: string;
5
+ }
6
+ export interface RecipeSection {
7
+ id: string;
8
+ title: string;
9
+ store: 'sessions' | 'usage';
10
+ rows: Array<Record<string, string | number | null>>;
11
+ empty?: boolean;
12
+ }
13
+ export declare function trendsWindow(days: number): TrendsWindow;
14
+ export declare function recipeHarnessMix(win: TrendsWindow): RecipeSection;
15
+ export declare function recipeModelMix(win: TrendsWindow): RecipeSection;
16
+ export declare function recipeToolsPerSession(win: TrendsWindow): RecipeSection;
17
+ export declare function recipeTokenRatio(win: TrendsWindow): RecipeSection;
18
+ export declare function recipeSessionVolume(win: TrendsWindow): RecipeSection;
19
+ export declare function recipeSecretsHot(win: TrendsWindow): RecipeSection;
20
+ export declare function recipeBrowserActivity(win: TrendsWindow): RecipeSection;
21
+ export declare function recipeResourceMix(win: TrendsWindow): RecipeSection;
22
+ export type RecipeId = 'harness-mix' | 'model-mix' | 'tools-per-session' | 'token-ratio' | 'session-volume' | 'secrets-hot' | 'browser-activity' | 'resource-mix';
23
+ export declare const RECIPE_IDS: readonly RecipeId[];
24
+ export declare function runRecipe(id: RecipeId, win: TrendsWindow): RecipeSection;
25
+ export declare function listRecipes(): Array<{
26
+ id: RecipeId;
27
+ title: string;
28
+ store: 'sessions' | 'usage';
29
+ }>;
30
+ export declare function hasSessionsInWindow(win: TrendsWindow): boolean;
31
+ export declare function usageKindsPresent(win: TrendsWindow): UsageKind[];
32
+ export declare function usageEventCount(kind: UsageKind, win: TrendsWindow): number;