@phnx-labs/agents-cli 1.22.24 → 1.22.26

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 (143) hide show
  1. package/CHANGELOG.md +350 -1
  2. package/README.md +31 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/browser.js +14 -4
  5. package/dist/commands/apply.js +52 -8
  6. package/dist/commands/browser.js +35 -0
  7. package/dist/commands/cloud.js +9 -5
  8. package/dist/commands/doctor.d.ts +24 -0
  9. package/dist/commands/doctor.js +108 -9
  10. package/dist/commands/exec.js +19 -16
  11. package/dist/commands/feed.d.ts +5 -0
  12. package/dist/commands/feed.js +21 -1
  13. package/dist/commands/focus.js +2 -2
  14. package/dist/commands/insights.d.ts +25 -19
  15. package/dist/commands/insights.js +107 -33
  16. package/dist/commands/menubar.js +8 -0
  17. package/dist/commands/reconnect.d.ts +46 -0
  18. package/dist/commands/reconnect.js +109 -0
  19. package/dist/commands/routines.d.ts +3 -0
  20. package/dist/commands/routines.js +72 -49
  21. package/dist/commands/run-cloud.js +1 -1
  22. package/dist/commands/secrets.d.ts +2 -8
  23. package/dist/commands/secrets.js +29 -105
  24. package/dist/commands/sessions-browser.d.ts +1 -1
  25. package/dist/commands/sessions-browser.js +27 -7
  26. package/dist/commands/sessions-resume.js +3 -2
  27. package/dist/commands/sessions.d.ts +13 -1
  28. package/dist/commands/sessions.js +28 -2
  29. package/dist/commands/setup-secrets.d.ts +1 -0
  30. package/dist/commands/setup-secrets.js +1 -1
  31. package/dist/commands/setup-watchdog.js +5 -10
  32. package/dist/commands/setup.d.ts +26 -3
  33. package/dist/commands/setup.js +106 -47
  34. package/dist/commands/teams.d.ts +6 -0
  35. package/dist/commands/teams.js +46 -2
  36. package/dist/commands/trends.d.ts +8 -0
  37. package/dist/commands/trends.js +10 -156
  38. package/dist/commands/watchdog.d.ts +3 -4
  39. package/dist/commands/watchdog.js +26 -66
  40. package/dist/index.js +37 -2
  41. package/dist/lib/agents.d.ts +11 -0
  42. package/dist/lib/agents.js +155 -23
  43. package/dist/lib/analytics/dashboard.d.ts +10 -6
  44. package/dist/lib/analytics/dashboard.js +6 -4
  45. package/dist/lib/analytics/mix-commands.d.ts +53 -0
  46. package/dist/lib/analytics/mix-commands.js +229 -0
  47. package/dist/lib/analytics/recipes.d.ts +19 -14
  48. package/dist/lib/analytics/recipes.js +4 -2
  49. package/dist/lib/browser/ipc.d.ts +26 -0
  50. package/dist/lib/browser/ipc.js +139 -24
  51. package/dist/lib/browser/profiles.d.ts +11 -0
  52. package/dist/lib/browser/profiles.js +1 -1
  53. package/dist/lib/browser/stream.d.ts +14 -0
  54. package/dist/lib/browser/stream.js +71 -0
  55. package/dist/lib/channels/owner-sink.d.ts +27 -0
  56. package/dist/lib/channels/owner-sink.js +93 -0
  57. package/dist/lib/cloud/cursor.d.ts +79 -0
  58. package/dist/lib/cloud/cursor.js +228 -0
  59. package/dist/lib/cloud/registry.js +2 -0
  60. package/dist/lib/cloud/types.d.ts +7 -2
  61. package/dist/lib/cloud/types.js +14 -0
  62. package/dist/lib/crabbox/cli.d.ts +2 -2
  63. package/dist/lib/crabbox/config.d.ts +7 -8
  64. package/dist/lib/crabbox/config.js +14 -14
  65. package/dist/lib/crabbox/lease.d.ts +11 -4
  66. package/dist/lib/crabbox/lease.js +40 -8
  67. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  68. package/dist/lib/crabbox/setup-copy.js +17 -1
  69. package/dist/lib/daemon.js +27 -1
  70. package/dist/lib/device-config.js +7 -0
  71. package/dist/lib/devices/doctor-findings.d.ts +13 -1
  72. package/dist/lib/devices/doctor-findings.js +73 -2
  73. package/dist/lib/events.d.ts +9 -0
  74. package/dist/lib/events.js +58 -0
  75. package/dist/lib/exec.d.ts +3 -3
  76. package/dist/lib/exec.js +24 -10
  77. package/dist/lib/feed-outcome.d.ts +3 -0
  78. package/dist/lib/feed-outcome.js +18 -10
  79. package/dist/lib/feed.d.ts +4 -0
  80. package/dist/lib/fleet/apply.d.ts +59 -3
  81. package/dist/lib/fleet/apply.js +183 -6
  82. package/dist/lib/fleet/types.d.ts +21 -2
  83. package/dist/lib/hooks/cache.js +15 -0
  84. package/dist/lib/hosts/passthrough.d.ts +44 -0
  85. package/dist/lib/hosts/passthrough.js +84 -12
  86. package/dist/lib/hosts/ready.d.ts +2 -0
  87. package/dist/lib/hosts/ready.js +10 -1
  88. package/dist/lib/hosts/reconnect.d.ts +14 -12
  89. package/dist/lib/hosts/reconnect.js +41 -40
  90. package/dist/lib/mcp.js +5 -1
  91. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  92. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  93. package/dist/lib/menubar/snapshot.d.ts +15 -0
  94. package/dist/lib/menubar/snapshot.js +40 -0
  95. package/dist/lib/plugins.js +13 -1
  96. package/dist/lib/resources/mcp.js +3 -0
  97. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  98. package/dist/lib/routine-process-cleanup.js +73 -0
  99. package/dist/lib/routines.js +14 -2
  100. package/dist/lib/runner.d.ts +0 -3
  101. package/dist/lib/runner.js +6 -19
  102. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  103. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  104. package/dist/lib/secrets/bundles.js +1 -20
  105. package/dist/lib/secrets/filestore.d.ts +2 -0
  106. package/dist/lib/secrets/filestore.js +13 -0
  107. package/dist/lib/secrets/push.d.ts +94 -0
  108. package/dist/lib/secrets/push.js +145 -0
  109. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  110. package/dist/lib/secrets/rc-hygiene.js +14 -1
  111. package/dist/lib/secrets/reaper.d.ts +15 -1
  112. package/dist/lib/secrets/reaper.js +30 -3
  113. package/dist/lib/session/active.d.ts +4 -0
  114. package/dist/lib/session/db.d.ts +21 -3
  115. package/dist/lib/session/db.js +229 -15
  116. package/dist/lib/session/discover.d.ts +1 -0
  117. package/dist/lib/session/discover.js +115 -19
  118. package/dist/lib/session/insights.d.ts +18 -0
  119. package/dist/lib/session/insights.js +143 -1
  120. package/dist/lib/session/remote-list.d.ts +2 -0
  121. package/dist/lib/session/remote-list.js +1 -0
  122. package/dist/lib/session/session-cache.d.ts +4 -4
  123. package/dist/lib/session/session-cache.js +4 -4
  124. package/dist/lib/session/tool-index.js +133 -22
  125. package/dist/lib/session/tool-store.d.ts +26 -2
  126. package/dist/lib/session/tool-store.js +36 -17
  127. package/dist/lib/shims.js +21 -1
  128. package/dist/lib/signin-badge.js +2 -0
  129. package/dist/lib/ssh-exec.js +8 -2
  130. package/dist/lib/startup/command-registry.d.ts +16 -0
  131. package/dist/lib/startup/command-registry.js +46 -0
  132. package/dist/lib/teams/agents.d.ts +13 -0
  133. package/dist/lib/teams/agents.js +76 -8
  134. package/dist/lib/teams/parsers.d.ts +1 -1
  135. package/dist/lib/teams/placement-probe.d.ts +21 -0
  136. package/dist/lib/teams/placement-probe.js +135 -0
  137. package/dist/lib/teams/scheduler.d.ts +74 -1
  138. package/dist/lib/teams/scheduler.js +187 -10
  139. package/dist/lib/types.d.ts +1 -1
  140. package/dist/lib/versions.js +16 -1
  141. package/dist/lib/watchdog/service.d.ts +17 -0
  142. package/dist/lib/watchdog/service.js +39 -0
  143. package/package.json +1 -1
@@ -16,7 +16,7 @@ import fs from 'node:fs';
16
16
  import chalk from 'chalk';
17
17
  import { confirm } from '@inquirer/prompts';
18
18
  import { gatherLiveTargets, pickLiveTarget, pickLiveTargets, jumpTo, probeAttachRail, refuseFallback } from './go.js';
19
- import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
19
+ import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, isRunningLiveSession, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
20
20
  import { resolveBackend, CONFIRM_THRESHOLD } from './sessions-resume.js';
21
21
  import { runOnPeer } from '../lib/session/remote-list.js';
22
22
  import { discoverSessions } from '../lib/session/discover.js';
@@ -276,7 +276,7 @@ export async function focusAction(id, opts) {
276
276
  }
277
277
  /** A retained pane is not attachable merely because tmux can still display it. */
278
278
  export function isAttachableLiveSession(session) {
279
- return session.pidAlive !== false && session.status !== 'closed' && session.status !== 'crashed';
279
+ return isRunningLiveSession(session);
280
280
  }
281
281
  const FOCUS_AGENT_SHORTHANDS = ['claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode'];
282
282
  function focusOptionAgent(opts) {
@@ -1,23 +1,26 @@
1
1
  /**
2
- * Insights command — how you actually work, split by the account that did the work.
3
- *
4
- * The behavioural sibling of the existing rollups, and deliberately not a duplicate of
5
- * any of them:
6
- *
7
- * agents cost what you spent ($ and duration)
8
- * agents output what shipped (burn vs PRs and commits)
9
- * agents usage live quota headroom (rate-limit windows, right now)
10
- * agents trends aggregate distributions (harness mix, tools-per-session, token ratios)
11
- * agents sessions browse individual work (search, resume, render)
12
- * agents insights HOW you work (tools, friction, rhythm, per account)
13
- *
14
- * The closest neighbour is `agents trends`, and the boundary is the data path: trends
15
- * reads counters `tool_scan_ledger` call counts and the analytics warehouse — to
16
- * produce distributions ("how many tool calls per session, by harness"). This reads
17
- * transcript CONTENT through `parseSession` to produce behaviour ("which tools, which
18
- * languages, where it went wrong, when you were working"), and splits all of it by
19
- * account, a dimension trends does not have. They overlap in spirit on tool and model
20
- * mix; they do not read the same store or answer the same question.
2
+ * Insights command — one observe verb for "how work looks".
3
+ *
4
+ * Two data paths under one name (do not re-split into peer top-level commands):
5
+ *
6
+ * agents insights HOW you work (transcript content: tools, friction,
7
+ * rhythm, edits) split by Claude account by default
8
+ * agents insights mix COUNTERS (sessions index + usage.db recipes:
9
+ * harness/model mix, token ratios, secrets, browser)
10
+ * agents insights <recipe> One baked mix recipe (harness-mix, tools-per-session, )
11
+ * agents insights query Raw usage.db rows
12
+ *
13
+ * Sibling observe verbs (stay separate — different questions):
14
+ *
15
+ * agents cost what you spent ($ and duration)
16
+ * agents output what shipped (burn vs PRs and commits)
17
+ * agents usage live quota headroom
18
+ * agents perf latency (hooks, CLI commands, agent.run) not popularity
19
+ * agents sessions stats which skills/slash-commands were explicitly invoked
20
+ *
21
+ * Why mix lives here (not a second top-level `trends`): two abstract "analytics"
22
+ * nouns taught agents and humans to guess. One verb, two engines — cheap SQL mix
23
+ * vs transcript facets. Latency stays on `perf` so it is never confused with mix.
21
24
  *
22
25
  * Modelled on Claude Code's `/insights`, with the difference that motivated it: that
23
26
  * command reads one account's directory, while `balanced` rotation sprays sessions
@@ -27,6 +30,9 @@
27
30
  * The deterministic report makes zero network calls. `--narrative` is opt-in and adds
28
31
  * the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
29
32
  * `claude -p`.
33
+ *
34
+ * `agents trends` is a thin deprecated alias of the mix tree only (see commands/trends.ts).
30
35
  */
31
36
  import type { Command } from 'commander';
32
37
  export declare function registerInsightsCommand(program: Command): void;
38
+ export declare function registerSessionsInsightsCommand(sessions: Command): void;
@@ -1,23 +1,26 @@
1
1
  /**
2
- * Insights command — how you actually work, split by the account that did the work.
2
+ * Insights command — one observe verb for "how work looks".
3
3
  *
4
- * The behavioural sibling of the existing rollups, and deliberately not a duplicate of
5
- * any of them:
4
+ * Two data paths under one name (do not re-split into peer top-level commands):
6
5
  *
7
- * agents cost what you spent ($ and duration)
8
- * agents output what shipped (burn vs PRs and commits)
9
- * agents usage live quota headroom (rate-limit windows, right now)
10
- * agents trends aggregate distributions (harness mix, tools-per-session, token ratios)
11
- * agents sessions browse individual work (search, resume, render)
12
- * agents insights HOW you work (tools, friction, rhythm, per account)
6
+ * agents insights HOW you work (transcript content: tools, friction,
7
+ * rhythm, edits) split by Claude account by default
8
+ * agents insights mix COUNTERS (sessions index + usage.db recipes:
9
+ * harness/model mix, token ratios, secrets, browser)
10
+ * agents insights <recipe> One baked mix recipe (harness-mix, tools-per-session, )
11
+ * agents insights query Raw usage.db rows
13
12
  *
14
- * The closest neighbour is `agents trends`, and the boundary is the data path: trends
15
- * reads counters — `tool_scan_ledger` call counts and the analytics warehouse — to
16
- * produce distributions ("how many tool calls per session, by harness"). This reads
17
- * transcript CONTENT through `parseSession` to produce behaviour ("which tools, which
18
- * languages, where it went wrong, when you were working"), and splits all of it by
19
- * account, a dimension trends does not have. They overlap in spirit on tool and model
20
- * mix; they do not read the same store or answer the same question.
13
+ * Sibling observe verbs (stay separate different questions):
14
+ *
15
+ * agents cost what you spent ($ and duration)
16
+ * agents output what shipped (burn vs PRs and commits)
17
+ * agents usage live quota headroom
18
+ * agents perf latency (hooks, CLI commands, agent.run) not popularity
19
+ * agents sessions stats which skills/slash-commands were explicitly invoked
20
+ *
21
+ * Why mix lives here (not a second top-level `trends`): two abstract "analytics"
22
+ * nouns taught agents and humans to guess. One verb, two engines — cheap SQL mix
23
+ * vs transcript facets. Latency stays on `perf` so it is never confused with mix.
21
24
  *
22
25
  * Modelled on Claude Code's `/insights`, with the difference that motivated it: that
23
26
  * command reads one account's directory, while `balanced` rotation sprays sessions
@@ -27,6 +30,8 @@
27
30
  * The deterministic report makes zero network calls. `--narrative` is opt-in and adds
28
31
  * the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
29
32
  * `claude -p`.
33
+ *
34
+ * `agents trends` is a thin deprecated alias of the mix tree only (see commands/trends.ts).
30
35
  */
31
36
  import * as fs from 'fs';
32
37
  import chalk from 'chalk';
@@ -37,11 +42,25 @@ import { setHelpSections } from '../lib/help.js';
37
42
  import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
38
43
  import { querySessions, readSessionInsights, writeSessionInsights, clearSessionInsights, } from '../lib/session/db.js';
39
44
  import { parseSession } from '../lib/session/parse.js';
40
- import { computeInsightFacets, mergeFacets, newFacetAccumulator, detectOverlap, percentile, bucketGaps, topEntries, } from '../lib/session/insights.js';
45
+ import { computeInsightFacets, mergeFacets, newFacetAccumulator, detectOverlap, percentile, bucketGaps, topEntries, buildInsightActions, } from '../lib/session/insights.js';
41
46
  import { formatUsd } from '../lib/pricing/index.js';
42
47
  import { formatDuration } from '../lib/session/render.js';
43
48
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
49
+ import { registerMixCommands } from '../lib/analytics/mix-commands.js';
44
50
  const execFileAsync = promisify(execFile);
51
+ function collectAgent(value, previous) {
52
+ return [...previous, value];
53
+ }
54
+ function agentsFromArgv(argv) {
55
+ const values = [];
56
+ for (let i = 0; i < argv.length; i++) {
57
+ if (argv[i] === '--agent' && argv[i + 1])
58
+ values.push(argv[++i]);
59
+ else if (argv[i].startsWith('--agent='))
60
+ values.push(argv[i].slice('--agent='.length));
61
+ }
62
+ return values;
63
+ }
45
64
  function resolveGroup(by) {
46
65
  if (by === undefined)
47
66
  return 'account';
@@ -187,7 +206,7 @@ function renderHours(hours, out) {
187
206
  out.push(` ${chalk.cyan(spark)}`);
188
207
  out.push(` ${chalk.gray('0h'.padEnd(6))}${chalk.gray('6h'.padEnd(6))}${chalk.gray('12h'.padEnd(6))}${chalk.gray('18h'.padEnd(5))}${chalk.gray('23h')}`);
189
208
  }
190
- function renderReport(groups, dim, meta) {
209
+ function renderReport(groups, dim, meta, actions, harnesses) {
191
210
  const out = [];
192
211
  const scope = meta.since ? `last ${meta.since}` : 'all time';
193
212
  out.push(chalk.bold('Insights') + chalk.gray(` ${scope} · ${meta.analyzed} of ${meta.scanned} sessions`));
@@ -232,6 +251,22 @@ function renderReport(groups, dim, meta) {
232
251
  for (const e of errs)
233
252
  out.push(` ${chalk.gray('·')} ${padToWidth(e.name, 16)} ${chalk.gray(String(e.count))}`);
234
253
  }
254
+ renderCounts('Friction / thrash', topEntries(all.frictionSignals, 10), out);
255
+ renderCounts('Dissatisfaction / corrections', topEntries(all.correctionSignals, 10), out);
256
+ renderCounts('Automatable repeats', topEntries(all.automationSignals, 10), out);
257
+ renderCounts('Harness split', harnesses, out);
258
+ out.push('');
259
+ out.push(chalk.bold('Actions'));
260
+ if (actions.length === 0) {
261
+ out.push(chalk.gray(' No repeated action pattern met the evidence threshold in this window.'));
262
+ }
263
+ else {
264
+ out.push(chalk.gray(' pri category evidence sample sessions action'));
265
+ for (const action of actions.slice(0, 12)) {
266
+ out.push(` ${padToWidth(action.priority, 7)} ${padToWidth(action.category, 11)} ` +
267
+ `${String(action.evidenceCount).padStart(8)} ${padToWidth(action.sampleSessionIds.join(', '), 25)} ${action.action}`);
268
+ }
269
+ }
235
270
  // Output
236
271
  out.push('');
237
272
  out.push(chalk.bold('What you changed'));
@@ -351,11 +386,11 @@ async function insightsAction(options) {
351
386
  if (options.refresh)
352
387
  clearSessionInsights();
353
388
  const filter = { sinceMs };
354
- if (options.agent)
355
- filter.agent = options.agent;
356
389
  const scanned = querySessions(filter);
357
390
  const wanted = options.account?.toLowerCase();
358
391
  const inScope = scanned.filter((m) => {
392
+ if (options.agent?.length && !options.agent.includes(m.agent))
393
+ return false;
359
394
  if (!wanted)
360
395
  return true;
361
396
  return [m.accountKey, m.account, m.accountOrg]
@@ -381,6 +416,15 @@ async function insightsAction(options) {
381
416
  });
382
417
  const overlap = detectOverlap(spans);
383
418
  const groups = buildGroups(substantive, facetsById, dim);
419
+ const evidence = substantive.flatMap((m) => {
420
+ const facets = facetsById.get(m.id);
421
+ return facets ? [{ id: m.id, facets }] : [];
422
+ });
423
+ const actions = buildInsightActions(evidence);
424
+ const harnesses = topEntries(substantive.reduce((counts, row) => {
425
+ counts[row.agent] = (counts[row.agent] ?? 0) + 1;
426
+ return counts;
427
+ }, {}), 20);
384
428
  if (options.json) {
385
429
  const payload = {
386
430
  generatedAt: new Date().toISOString(),
@@ -392,6 +436,8 @@ async function insightsAction(options) {
392
436
  minMessages,
393
437
  by: dim,
394
438
  overlap,
439
+ actions,
440
+ harnesses,
395
441
  groups: groups.map((g) => ({
396
442
  key: g.key,
397
443
  label: g.label,
@@ -420,7 +466,7 @@ async function insightsAction(options) {
420
466
  unreadable,
421
467
  minMessages,
422
468
  overlap,
423
- });
469
+ }, actions, harnesses);
424
470
  if (options.narrative) {
425
471
  await renderNarrative(groups.map((g) => ({
426
472
  account: g.label, sessions: g.sessions, costUsd: g.costUsd,
@@ -434,49 +480,77 @@ async function insightsAction(options) {
434
480
  })));
435
481
  }
436
482
  }
437
- export function registerInsightsCommand(program) {
438
- const cmd = addHostOption(program.command('insights'))
439
- .description('How you work — tools, friction, and rhythm, split by the account that did the work')
483
+ function configureInsightsCommand(cmd) {
484
+ addHostOption(cmd)
485
+ .description('How work looks behavioural report (default) or counter mix (`mix`, recipes)')
440
486
  .option('--json', 'Output the full report as JSON')
441
487
  .option('--since <time>', 'Window: 7d, 4w, 3mo, an ISO date, or "all" (default 30d)')
442
488
  .option('--all', 'Every session ever indexed. Alias for --since all')
443
489
  .option('--by <dimension>', 'Group by: account (default), agent, project, or day')
444
490
  .option('--account <match>', 'Only sessions whose account key, email, or org contains this')
445
- .option('--agent <id>', 'Only one harness (claude, codex, droid, )')
491
+ .option('--agent <id>', 'Only these harnesses; repeat for more than one', collectAgent, [])
446
492
  .option('--min-messages <n>', 'Skip sessions under this many messages, both roles counted (default 2)')
447
493
  .option('--refresh', 'Discard cached facets and re-read every transcript')
448
494
  .option('--narrative', 'Add a written read on the numbers via a headless `claude -p`')
449
495
  .action(async (options) => {
450
- await insightsAction(options);
496
+ const inherited = cmd.parent?.name() === 'sessions'
497
+ ? cmd.parent.opts()
498
+ : {};
499
+ const inheritedAgent = typeof inherited.agent === 'string' ? [inherited.agent] : [];
500
+ const rawAgents = agentsFromArgv(process.argv.slice(2));
501
+ await insightsAction({
502
+ ...inherited,
503
+ ...options,
504
+ agent: rawAgents.length > 0 ? rawAgents : [...inheritedAgent, ...(options.agent ?? [])],
505
+ json: options.json ?? inherited.json,
506
+ since: options.since ?? inherited.since,
507
+ });
451
508
  });
509
+ // Cheap counter recipes (former top-level `agents trends`) — same parent, no peer verb.
510
+ registerMixCommands(cmd);
452
511
  setHelpSections(cmd, {
453
512
  examples: `
454
- # Last 30 days, split by Claude account — the default
513
+ # Behavioural report — last 30 days, split by Claude account (default)
455
514
  agents insights
456
515
 
457
516
  # Which repo is eating the time
458
517
  agents insights --by project --since 90d
459
518
 
519
+ # Counter mix board (harness/model/token/secrets recipes) — former agents trends
520
+ agents insights mix
521
+ agents insights mix --days 30
522
+ agents insights harness-mix --json
523
+ agents insights query --kind secret --days 7
524
+
460
525
  # One account only, all of its history
461
526
  agents insights --account "Turing Labs" --all
462
527
 
463
528
  # Machine-readable, for a dashboard or a slash command
464
- agents insights --json
529
+ agents sessions insights --agent claude --agent codex --json
465
530
 
466
531
  # Add a written read on what to change
467
532
  agents insights --narrative
468
533
  `,
469
534
  notes: `
470
- Answers "how do you work". For "what did it cost" use \`agents cost\`, for "what
471
- shipped" use \`agents output\`, for live quota use \`agents usage\`.
535
+ Two paths under one verb:
536
+ bare \`agents insights\` — transcript behaviour (tools, friction, rhythm, by account)
537
+ \`agents insights mix\` — cheap counters from sessions.db + usage.db
538
+ Latency is \`agents perf\` (not mix). Quota is \`agents usage\`. Skill/slash popularity
539
+ is \`agents sessions stats\`. \`agents trends\` is a deprecated alias of the mix tree.
472
540
 
473
- The first run parses every in-scope transcript and caches the result; later runs
541
+ The behavioural report parses in-scope transcripts once and caches facets; later runs
474
542
  re-read only files that changed. \`--refresh\` forces a full re-read.
475
543
 
476
- Account attribution is Claude-only today. Sessions from other harnesses group
477
- under \`unattributed:<agent>\`.
544
+ \`agents insights\` is the top-level alias of \`agents sessions insights\`.
545
+ Repeat \`--agent\` to compare several harnesses in one report.
478
546
 
479
547
  Everything except \`--narrative\` is local and makes no network calls.
480
548
  `,
481
549
  });
482
550
  }
551
+ export function registerInsightsCommand(program) {
552
+ configureInsightsCommand(program.command('insights'));
553
+ }
554
+ export function registerSessionsInsightsCommand(sessions) {
555
+ configureInsightsCommand(sessions.command('insights'));
556
+ }
@@ -137,6 +137,14 @@ export function registerMenubarCommands(program) {
137
137
  again, run \`agents menubar disable\`.
138
138
  `,
139
139
  });
140
+ menubar
141
+ .command('snapshot', { hidden: true })
142
+ .description('Emit the consolidated AGI Menu polling snapshot.')
143
+ .option('--json', 'Emit machine-readable JSON')
144
+ .action(async () => {
145
+ const { computeMenubarSnapshot } = await import('../lib/menubar/snapshot.js');
146
+ process.stdout.write(`${JSON.stringify(await computeMenubarSnapshot())}\n`);
147
+ });
140
148
  menubar
141
149
  .command('enable')
142
150
  .description('Install and start AGI Menu (launches at login)')
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `agents reconnect [session-id]` — get me back into an agent terminal whose ssh
3
+ * link dropped.
4
+ *
5
+ * The auto-reconnect in lib/hosts/reconnect.ts already re-attaches a live remote
6
+ * pane over its own bounded backoff on a transient blink. This is the MANUAL
7
+ * recovery for after it gave up — a sustained outage, or a VS Code terminal tab
8
+ * that closed with the dead ssh client. One verb that always tries hardest to put
9
+ * you back into that agent: attach the live pane if it still exists, else resume
10
+ * the session; live pane > resumed copy > a clear message about what was lost.
11
+ *
12
+ * It delegates to `focus`, which already does exactly attach-else-resume for a
13
+ * resolved id ({@link focusAction} → `focusResolvedSession`), so there is no second
14
+ * recovery path to keep in sync. What `reconnect` adds is the recovery framing and,
15
+ * with no id, targeting the MOST RECENT session in this shell's directory — the one
16
+ * that most likely just dropped — instead of opening the full fleet picker.
17
+ */
18
+ import type { Command } from 'commander';
19
+ import type { SessionMeta } from '../lib/session/types.js';
20
+ /**
21
+ * The recency signal a no-id reconnect targets: last activity if the scan
22
+ * computed one, else the creation time. A row with neither parseable sorts last.
23
+ * Pure so target selection is unit-tested without the scanner.
24
+ */
25
+ export declare function sessionRecency(s: Pick<SessionMeta, 'lastActivity' | 'timestamp'>): number;
26
+ /**
27
+ * Pick the session to reconnect when the user gave no id: the most recently
28
+ * active one in scope — the terminal that most likely just dropped. Pure; returns
29
+ * undefined for an empty scope so the caller can print guidance.
30
+ */
31
+ export declare function pickMostRecentSession(sessions: SessionMeta[]): SessionMeta | undefined;
32
+ /**
33
+ * Resolve the no-id target: prefer the most recent session started from THIS
34
+ * directory (the shell the user is reconnecting from), falling back to the most
35
+ * recent session anywhere when the current directory has none. Best-effort — a
36
+ * discovery failure yields undefined, never throws. Injectable `discover` keeps
37
+ * the two-step scope preference testable without touching the real scanner.
38
+ */
39
+ export declare function resolveRecentTarget(cwd: string, discover?: (opts: {
40
+ cwd?: string;
41
+ all?: boolean;
42
+ since?: string;
43
+ limit?: number;
44
+ }) => Promise<SessionMeta[]>): Promise<SessionMeta | undefined>;
45
+ export declare function reconnectAction(id: string | undefined): Promise<void>;
46
+ export declare function registerReconnectCommand(program: Command): void;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `agents reconnect [session-id]` — get me back into an agent terminal whose ssh
3
+ * link dropped.
4
+ *
5
+ * The auto-reconnect in lib/hosts/reconnect.ts already re-attaches a live remote
6
+ * pane over its own bounded backoff on a transient blink. This is the MANUAL
7
+ * recovery for after it gave up — a sustained outage, or a VS Code terminal tab
8
+ * that closed with the dead ssh client. One verb that always tries hardest to put
9
+ * you back into that agent: attach the live pane if it still exists, else resume
10
+ * the session; live pane > resumed copy > a clear message about what was lost.
11
+ *
12
+ * It delegates to `focus`, which already does exactly attach-else-resume for a
13
+ * resolved id ({@link focusAction} → `focusResolvedSession`), so there is no second
14
+ * recovery path to keep in sync. What `reconnect` adds is the recovery framing and,
15
+ * with no id, targeting the MOST RECENT session in this shell's directory — the one
16
+ * that most likely just dropped — instead of opening the full fleet picker.
17
+ */
18
+ import chalk from 'chalk';
19
+ import { focusAction } from './focus.js';
20
+ import { discoverSessions } from '../lib/session/discover.js';
21
+ import { setHelpSections } from '../lib/help.js';
22
+ /**
23
+ * The recency signal a no-id reconnect targets: last activity if the scan
24
+ * computed one, else the creation time. A row with neither parseable sorts last.
25
+ * Pure so target selection is unit-tested without the scanner.
26
+ */
27
+ export function sessionRecency(s) {
28
+ const t = Date.parse(s.lastActivity ?? s.timestamp ?? '');
29
+ return Number.isFinite(t) ? t : Number.NEGATIVE_INFINITY;
30
+ }
31
+ /**
32
+ * Pick the session to reconnect when the user gave no id: the most recently
33
+ * active one in scope — the terminal that most likely just dropped. Pure; returns
34
+ * undefined for an empty scope so the caller can print guidance.
35
+ */
36
+ export function pickMostRecentSession(sessions) {
37
+ let best;
38
+ for (const s of sessions) {
39
+ if (!best || sessionRecency(s) > sessionRecency(best))
40
+ best = s;
41
+ }
42
+ return best;
43
+ }
44
+ /**
45
+ * Resolve the no-id target: prefer the most recent session started from THIS
46
+ * directory (the shell the user is reconnecting from), falling back to the most
47
+ * recent session anywhere when the current directory has none. Best-effort — a
48
+ * discovery failure yields undefined, never throws. Injectable `discover` keeps
49
+ * the two-step scope preference testable without touching the real scanner.
50
+ */
51
+ export async function resolveRecentTarget(cwd, discover = discoverSessions) {
52
+ try {
53
+ const here = await discover({ cwd, since: '7d', limit: 200 });
54
+ const local = pickMostRecentSession(here);
55
+ if (local)
56
+ return local;
57
+ const any = await discover({ all: true, since: '7d', limit: 200 });
58
+ return pickMostRecentSession(any);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ export async function reconnectAction(id) {
65
+ if (id) {
66
+ // focus already attaches a live pane or recovers a dropped one for a resolved
67
+ // id — on the origin device for a remote session. Reconnect is that path with
68
+ // a recovery-first name; no reason to duplicate the resolution/attach logic.
69
+ await focusAction(id, {});
70
+ return;
71
+ }
72
+ const recent = await resolveRecentTarget(process.cwd());
73
+ if (!recent) {
74
+ console.log(chalk.gray('No recent session to reconnect from here.'));
75
+ console.log(chalk.gray(' reconnect a specific one: agents reconnect <session-id>'));
76
+ console.log(chalk.gray(' or pick from the fleet: agents sessions'));
77
+ return;
78
+ }
79
+ const where = recent.machine ? ` on ${recent.machine}` : '';
80
+ console.log(chalk.gray(`Reconnecting ${recent.shortId}${where} (most recent here)…`));
81
+ await focusAction(recent.id, {});
82
+ }
83
+ export function registerReconnectCommand(program) {
84
+ const cmd = program
85
+ .command('reconnect')
86
+ .argument('[session-id]', 'Session id/prefix to reconnect (default: the most recent session started here)')
87
+ .description('Re-enter a dropped agent terminal: attach the live pane if it survived, else resume the session')
88
+ .action(async (id) => {
89
+ await reconnectAction(id);
90
+ });
91
+ setHelpSections(cmd, {
92
+ examples: `
93
+ # Reconnect the session that just dropped in this shell (most recent here)
94
+ agents reconnect
95
+
96
+ # Reconnect one specific agent terminal by id/prefix
97
+ agents reconnect 74b13b0b
98
+
99
+ # Same, spelled under the sessions group
100
+ agents sessions reconnect 74b13b0b
101
+ `,
102
+ notes: `
103
+ - Best-effort recovery: a living remote tmux pane is JOINED (a second client, no fork); a dropped one is RESUMED on its origin device (a copy if it was mid-run, a /continue if idle).
104
+ - With no id, targets the most recent session started from this directory (the terminal that most likely just dropped), not the full fleet picker. Falls back to the most recent session anywhere when this directory has none.
105
+ - This is the manual companion to the automatic reconnect that runs during a live 'agents run --device <box>' when the network blinks; use it after that gave up or the terminal tab closed.
106
+ - Related: 'agents sessions focus' (attach/recover with a picker) and 'agents sessions resume' (multi-select history -> tabs).
107
+ `,
108
+ });
109
+ }
@@ -46,5 +46,8 @@ export declare function groupRoutineJobsByDevice(jobs: JobConfig[], registry: De
46
46
  */
47
47
  export declare function groupRoutineJobsByProject(jobs: JobConfig[], knownProjectNames: Set<string>): RoutineListGroup[];
48
48
  export declare function buildRunsJson(runs: RunMeta[]): Record<string, unknown>[];
49
+ /** Build the exact structured routine rows shared by `routines list --json`
50
+ * and the one-process AGI Menu snapshot. */
51
+ export declare function buildRoutineListJson(): Record<string, unknown>[];
49
52
  /** Register the `agents routines` command tree. */
50
53
  export declare function registerRoutinesCommands(program: Command): void;