@phnx-labs/agents-cli 1.20.91 → 1.20.93

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 (105) hide show
  1. package/CHANGELOG.md +276 -0
  2. package/README.md +1 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +72 -6
  5. package/dist/commands/activity.js +198 -49
  6. package/dist/commands/beta.js +1 -0
  7. package/dist/commands/doctor.js +4 -2
  8. package/dist/commands/events.js +91 -1
  9. package/dist/commands/exec.d.ts +14 -0
  10. package/dist/commands/exec.js +144 -14
  11. package/dist/commands/projects.d.ts +22 -0
  12. package/dist/commands/projects.js +539 -0
  13. package/dist/commands/secrets.d.ts +17 -0
  14. package/dist/commands/secrets.js +198 -7
  15. package/dist/commands/send.d.ts +14 -12
  16. package/dist/commands/send.js +105 -35
  17. package/dist/commands/sessions-picker.d.ts +15 -0
  18. package/dist/commands/sessions-picker.js +37 -12
  19. package/dist/commands/sessions-resume.d.ts +2 -0
  20. package/dist/commands/sessions-resume.js +9 -1
  21. package/dist/commands/sessions.d.ts +10 -5
  22. package/dist/commands/sessions.js +65 -27
  23. package/dist/commands/sync.js +9 -3
  24. package/dist/commands/view.js +4 -0
  25. package/dist/index.js +18 -1
  26. package/dist/lib/activity.d.ts +77 -12
  27. package/dist/lib/activity.js +424 -74
  28. package/dist/lib/beta.d.ts +1 -1
  29. package/dist/lib/beta.js +1 -1
  30. package/dist/lib/channels/send.d.ts +83 -0
  31. package/dist/lib/channels/send.js +112 -0
  32. package/dist/lib/devices/registry.d.ts +14 -0
  33. package/dist/lib/devices/registry.js +37 -0
  34. package/dist/lib/events-ingest.d.ts +46 -0
  35. package/dist/lib/events-ingest.js +182 -0
  36. package/dist/lib/events.d.ts +15 -3
  37. package/dist/lib/events.js +55 -3
  38. package/dist/lib/feed-post.js +8 -2
  39. package/dist/lib/hosts/remote-cmd.js +4 -0
  40. package/dist/lib/linear-project-counts.d.ts +62 -0
  41. package/dist/lib/linear-project-counts.js +122 -0
  42. package/dist/lib/linear-projects.d.ts +50 -0
  43. package/dist/lib/linear-projects.js +114 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  45. package/dist/lib/menubar/install-menubar.d.ts +14 -4
  46. package/dist/lib/menubar/install-menubar.js +20 -6
  47. package/dist/lib/menubar/notify-desktop.d.ts +17 -2
  48. package/dist/lib/menubar/notify-desktop.js +8 -2
  49. package/dist/lib/project-key.d.ts +44 -0
  50. package/dist/lib/project-key.js +79 -0
  51. package/dist/lib/project-probe.d.ts +75 -0
  52. package/dist/lib/project-probe.js +160 -0
  53. package/dist/lib/project-resources.d.ts +8 -0
  54. package/dist/lib/project-resources.js +31 -3
  55. package/dist/lib/project-root.js +16 -0
  56. package/dist/lib/project-status.d.ts +100 -0
  57. package/dist/lib/project-status.js +182 -0
  58. package/dist/lib/projects.d.ts +144 -0
  59. package/dist/lib/projects.js +313 -0
  60. package/dist/lib/remote-agents-json.d.ts +9 -0
  61. package/dist/lib/remote-agents-json.js +11 -5
  62. package/dist/lib/routine-notify.d.ts +11 -0
  63. package/dist/lib/routine-notify.js +22 -0
  64. package/dist/lib/run-notify.js +3 -0
  65. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  66. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  67. package/dist/lib/secrets/audit.d.ts +1 -1
  68. package/dist/lib/secrets/audit.js +53 -10
  69. package/dist/lib/secrets/list-filter.d.ts +20 -5
  70. package/dist/lib/secrets/list-filter.js +22 -6
  71. package/dist/lib/secrets/usage-db.d.ts +106 -0
  72. package/dist/lib/secrets/usage-db.js +236 -0
  73. package/dist/lib/session/bash-command.d.ts +53 -0
  74. package/dist/lib/session/bash-command.js +364 -0
  75. package/dist/lib/session/digest.d.ts +6 -0
  76. package/dist/lib/session/digest.js +19 -0
  77. package/dist/lib/session/relative-time.d.ts +23 -0
  78. package/dist/lib/session/relative-time.js +60 -8
  79. package/dist/lib/session/remote-active.d.ts +5 -1
  80. package/dist/lib/session/remote-active.js +4 -1
  81. package/dist/lib/session/remote-list.js +5 -2
  82. package/dist/lib/session/render.d.ts +2 -9
  83. package/dist/lib/session/render.js +25 -56
  84. package/dist/lib/sqlite.js +28 -1
  85. package/dist/lib/ssh-exec.d.ts +6 -0
  86. package/dist/lib/ssh-exec.js +10 -1
  87. package/dist/lib/startup/command-registry.d.ts +1 -0
  88. package/dist/lib/startup/command-registry.js +2 -0
  89. package/dist/lib/state.d.ts +14 -0
  90. package/dist/lib/state.js +19 -0
  91. package/dist/lib/terminal/backends/index.d.ts +10 -2
  92. package/dist/lib/terminal/backends/index.js +14 -2
  93. package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
  94. package/dist/lib/terminal/backends/terminal-app.js +73 -0
  95. package/dist/lib/terminal/index.d.ts +2 -1
  96. package/dist/lib/terminal/index.js +2 -1
  97. package/dist/lib/terminal/preferred.d.ts +89 -0
  98. package/dist/lib/terminal/preferred.js +87 -0
  99. package/dist/lib/terminal/run-surface.d.ts +82 -0
  100. package/dist/lib/terminal/run-surface.js +146 -0
  101. package/dist/lib/terminal/types.d.ts +1 -1
  102. package/dist/lib/types.d.ts +6 -5
  103. package/dist/lib/versions.d.ts +6 -0
  104. package/dist/lib/versions.js +6 -4
  105. package/package.json +2 -1
@@ -146,7 +146,7 @@ function displayAgent(agent) {
146
146
  const DOT = chalk.gray(' · ');
147
147
  function formatHeader(session, events) {
148
148
  const model = extractModel(events) || session.model;
149
- const { startedAgo, duration } = extractTiming(events);
149
+ const { createdAgo, lastActiveAgo, duration } = extractTiming(session, events);
150
150
  const totalMessages = session.messageCount ?? countMessages(events);
151
151
  const totalTokens = session.tokenCount;
152
152
  // Line 1: Agent v version · shortId · model · account
@@ -158,7 +158,7 @@ function formatHeader(session, events) {
158
158
  line1.push(chalk.bold.white(shortenModel(model)));
159
159
  if (session.account)
160
160
  line1.push(chalk.gray(session.account));
161
- // Line 2: cwd · project · branch · started X ago · lasted Y
161
+ // Line 2: cwd · project · branch · created X ago · last active Y ago · lasted Z
162
162
  // Project is clickable: Linear issue URL is for tickets (line 4); for the
163
163
  // project name prefer a GitHub repo URL derived from the checkout path.
164
164
  const line2 = [];
@@ -172,8 +172,10 @@ function formatHeader(session, events) {
172
172
  }
173
173
  if (session.gitBranch)
174
174
  line2.push(chalk.cyan(session.gitBranch));
175
- if (startedAgo)
176
- line2.push(chalk.gray('started ') + chalk.white(startedAgo + ' ago'));
175
+ if (createdAgo)
176
+ line2.push(chalk.gray('created ') + chalk.white(createdAgo + ' ago'));
177
+ if (lastActiveAgo)
178
+ line2.push(chalk.gray('last active ') + chalk.white(lastActiveAgo + ' ago'));
177
179
  if (duration)
178
180
  line2.push(chalk.gray('lasted ') + chalk.white(duration));
179
181
  // Line 3: N msgs · T tokens · [label ·] uuid
@@ -292,16 +294,39 @@ function extractModel(events) {
292
294
  }
293
295
  return undefined;
294
296
  }
295
- function extractTiming(events) {
296
- if (events.length === 0)
297
- return {};
298
- const firstMs = Date.parse(events[0].timestamp);
299
- const lastMs = Date.parse(events[events.length - 1].timestamp);
297
+ /** Shortest span worth reporting as a separate last-activity field — matches the
298
+ * listing's rule, so the pane and the row agree on what counts as a one-shot. */
299
+ const TIMING_SPAN_MIN_MS = 60_000;
300
+ /**
301
+ * The three timing facts the header reports: when the session was created, when
302
+ * it was last active, and how long it ran. Reads the parsed transcript when
303
+ * there is one and otherwise the indexed `SessionMeta`, so a remote or
304
+ * unindexed session — which has no local transcript to parse — still reports
305
+ * them instead of silently dropping the whole line.
306
+ *
307
+ * `lastActive` and `lasted` are omitted for a session whose whole life was under
308
+ * a minute: there they just restate `created`.
309
+ */
310
+ export function extractTiming(session, events) {
311
+ const firstMs = Date.parse(events[0]?.timestamp ?? session.timestamp);
300
312
  if (Number.isNaN(firstMs))
301
313
  return {};
302
- const ago = humanDuration(Math.max(0, Date.now() - firstMs));
303
- const dur = Number.isNaN(lastMs) ? undefined : humanDuration(Math.max(0, lastMs - firstMs));
304
- return { startedAgo: ago, duration: dur };
314
+ const createdAgo = humanDuration(Math.max(0, Date.now() - firstMs));
315
+ const lastMs = Date.parse(events[events.length - 1]?.timestamp ?? session.lastActivity ?? session.timestamp);
316
+ if (Number.isNaN(lastMs))
317
+ return { createdAgo };
318
+ // A scan-persisted duration beats the timestamp subtraction when there are no
319
+ // events to read, because `lastActivity` may have fallen back to file mtime.
320
+ const spanMs = events.length === 0 && session.durationMs !== undefined
321
+ ? session.durationMs
322
+ : Math.max(0, lastMs - firstMs);
323
+ if (spanMs < TIMING_SPAN_MIN_MS)
324
+ return { createdAgo };
325
+ return {
326
+ createdAgo,
327
+ lastActiveAgo: humanDuration(Math.max(0, Date.now() - lastMs)),
328
+ duration: humanDuration(spanMs),
329
+ };
305
330
  }
306
331
  function countMessages(events) {
307
332
  return events.filter(e => e.type === 'message').length;
@@ -11,6 +11,8 @@ interface ResumeOptions {
11
11
  ghostty?: boolean;
12
12
  tmux?: boolean;
13
13
  vscodium?: boolean;
14
+ /** --terminal-app: force macOS Terminal.app. Named to avoid reading as `run --terminal`. */
15
+ terminalApp?: boolean;
14
16
  splits?: boolean;
15
17
  }
16
18
  export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
@@ -36,6 +36,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
36
36
  .option('--ghostty', 'Force the Ghostty backend')
37
37
  .option('--tmux', 'Force the tmux backend')
38
38
  .option('--vscodium', 'Force the VSCodium agent-terminal backend (swarm-ext)')
39
+ .option('--terminal-app', 'Force macOS Terminal.app (no split support — panes become tabs)')
39
40
  .option('--splits', 'Pack two sessions side by side per tab (default: one tab per session)');
40
41
  setHelpSections(cmd, {
41
42
  examples: `
@@ -156,6 +157,12 @@ async function sessionsResumeAction(query, options) {
156
157
  // recovery; callers can explicitly opt into pairs of side-by-side panes.
157
158
  const packing = resolveResumePacking(options);
158
159
  const where = options.host ? `${backend} on ${options.host}` : backend;
160
+ // Terminal.app has no scriptable split, so its buildSplit opens a tab. Say so
161
+ // when the user actually asked for panes — the layout silently not happening
162
+ // is worse than one line of warning.
163
+ if (backend === 'terminal' && packing === 'two-per-tab') {
164
+ console.log(chalk.yellow('Terminal.app cannot split panes — opening one tab per session instead.'));
165
+ }
159
166
  console.log(chalk.gray(`Opening ${items.length} session${items.length === 1 ? '' : 's'} in ${where} (${packing})…`));
160
167
  const results = await openSurfaces(items.map((it) => ({ cwd: it.cwd, command: it.command })), { backend, host: options.host, packing });
161
168
  let opened = 0;
@@ -185,7 +192,8 @@ async function resolveBackend(options, ctx, count) {
185
192
  : options.ghostty ? 'ghostty'
186
193
  : options.tmux ? 'tmux'
187
194
  : options.vscodium ? 'vscodium-agent'
188
- : undefined;
195
+ : options.terminalApp ? 'terminal'
196
+ : undefined;
189
197
  if (forced)
190
198
  return forced;
191
199
  // Remote defaults to tmux (headless, no GUI session assumptions); override with a backend flag.
@@ -1,4 +1,5 @@
1
1
  import { type Command } from 'commander';
2
+ import { type ProjectDef } from '../lib/projects.js';
2
3
  import type { SessionAgentId, SessionMeta, ViewMode } from '../lib/session/types.js';
3
4
  import { type ActiveSession } from '../lib/session/active.js';
4
5
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
@@ -351,11 +352,15 @@ export declare function teamBadge(session: SessionMeta): {
351
352
  * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
352
353
  export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns, favorite?: boolean): string;
353
354
  /**
354
- * Group key for the overview: prefer the indexed project name; else fold the cwd
355
- * to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
356
- * repo, and a monorepo subdir falls back to its leaf dir basename. Pure.
355
+ * Group key for the overview: resolve the cwd through the same canonical
356
+ * resolver the `agents activity` timeline groups by — a defined project's name
357
+ * when `defs` contains the cwd (multi-repo projects read as one group), else
358
+ * the repo-level key, so a monorepo subdir (`<repo>/apps/cli`) reads as
359
+ * `<repo>` in both views. Falls back to the indexed project name (stamped at
360
+ * scan time) when the cwd carries nothing usable, e.g. a remote path this
361
+ * machine cannot see still folds by basename through the same resolver.
357
362
  */
358
- export declare function overviewProjectKey(s: Pick<SessionMeta, 'project' | 'cwd'>): string;
363
+ export declare function overviewProjectKey(s: Pick<SessionMeta, 'project' | 'cwd'>, defs?: ProjectDef[]): string;
359
364
  export interface OverviewGroup {
360
365
  key: string;
361
366
  total: number;
@@ -369,7 +374,7 @@ export interface OverviewGroup {
369
374
  * are ordered by their most-recent session so the newest-active project leads.
370
375
  * `perProjectCap = Infinity` expands every group. Pure — unit-tested.
371
376
  */
372
- export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap: number): {
377
+ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap: number, defs?: ProjectDef[]): {
373
378
  groups: OverviewGroup[];
374
379
  projectCount: number;
375
380
  };
@@ -14,6 +14,8 @@ import { spawn } from 'child_process';
14
14
  import { Option } from 'commander';
15
15
  import chalk from 'chalk';
16
16
  import { truncate, padRight } from '../lib/format.js';
17
+ import { resolveProjectKey } from '../lib/project-key.js';
18
+ import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
17
19
  import ora from 'ora';
18
20
  import { SESSION_AGENTS } from '../lib/session/types.js';
19
21
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
@@ -32,7 +34,7 @@ import { findSessionsById, querySessions } from '../lib/session/db.js';
32
34
  import { filterTeamSessions, safeTeamText } from '../lib/session/team-filter.js';
33
35
  import { parseSession } from '../lib/session/parse.js';
34
36
  import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
35
- import { formatRelativeTime } from '../lib/session/relative-time.js';
37
+ import { formatRelativeTime, sessionAgeParts } from '../lib/session/relative-time.js';
36
38
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
37
39
  import { linearIssueUrl } from '../lib/session/linear.js';
38
40
  import { renderMarkdown } from '../lib/markdown.js';
@@ -1768,6 +1770,33 @@ function metaSignals(s) {
1768
1770
  ticket: s.ticketId ? { id: s.ticketId } : undefined,
1769
1771
  };
1770
1772
  }
1773
+ /**
1774
+ * Narrowest topic a row is willing to render. The time cell drops its creation
1775
+ * field rather than squeeze the topic past this — a row that wraps is worse than
1776
+ * a row missing one field.
1777
+ */
1778
+ const MIN_TOPIC_W = 16;
1779
+ /**
1780
+ * The trailing time cell, as two fields: `3d → 2 hours ago` — when the session
1781
+ * was created, then when it was last active. One label answers neither "is this
1782
+ * the old session I'm looking for" nor "how long did it run", since a session
1783
+ * touched an hour ago may have started last week.
1784
+ *
1785
+ * Collapses to last-activity alone when the session ran under a minute (both
1786
+ * halves would name the same moment) or when `topicSlack` — the columns the row
1787
+ * has left for its topic — cannot spare the extra width. `extraW` is what the
1788
+ * creation field cost, for the caller to take off the topic budget.
1789
+ */
1790
+ function timeCell(age, topicSlack) {
1791
+ const lastOnly = { plain: age.last, text: chalk.gray(age.last), extraW: 0 };
1792
+ if (!age.created)
1793
+ return lastOnly;
1794
+ const prefix = `${age.created} → `;
1795
+ const extraW = stringWidth(prefix);
1796
+ if (topicSlack - extraW < MIN_TOPIC_W)
1797
+ return lastOnly;
1798
+ return { plain: prefix + age.last, text: chalk.dim(prefix) + chalk.gray(age.last), extraW };
1799
+ }
1771
1800
  /** One flat table row:
1772
1801
  * shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
1773
1802
  * `doing` is the live preview when running, else the topic. The `ticket` column
@@ -1776,7 +1805,7 @@ function metaSignals(s) {
1776
1805
  * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
1777
1806
  export function flatSessionRow(session, live, showTicket = false, cols = {}, favorite = false) {
1778
1807
  const agentColor = colorAgent(session.agent);
1779
- const when = formatRelativeTime(session.lastActivity ?? session.timestamp);
1808
+ const age = sessionAgeParts(session.timestamp, session.lastActivity);
1780
1809
  const project = session.project || '-';
1781
1810
  const tag = originTag(session) || teamTag(session);
1782
1811
  const label = session.label;
@@ -1813,12 +1842,15 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, fav
1813
1842
  // Same conditional 2 cells as the picker's marker, for the same reason.
1814
1843
  const favW = cols.showFavorite ? 2 : 0;
1815
1844
  const favCell = cols.showFavorite ? (favorite ? chalk.yellow('★ ') : ' ') : '';
1816
- const fixedW = favW + (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(when) + 1;
1817
- const modelSlack = width - fixedW - 16;
1845
+ // Sized against the last-activity label alone, so the creation field is an
1846
+ // additive decision the row makes only once it knows what space is left.
1847
+ const fixedW = favW + (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(age.last) + 1;
1848
+ const modelSlack = width - fixedW - MIN_TOPIC_W;
1818
1849
  const modelW = requestedModelW <= modelSlack
1819
1850
  ? requestedModelW
1820
1851
  : modelSlack >= PICKER_MODEL_MIN ? modelSlack : 0;
1821
- const topicW = Math.max(16, width - fixedW - modelW);
1852
+ const when = timeCell(age, width - fixedW - modelW);
1853
+ const topicW = Math.max(MIN_TOPIC_W, width - fixedW - modelW - when.extraW);
1822
1854
  return (favCell +
1823
1855
  chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
1824
1856
  agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
@@ -1832,12 +1864,12 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, fav
1832
1864
  renderTopicCell(label, doing, '', topicW, topicW) +
1833
1865
  ticketCell +
1834
1866
  (wt ? wt + ' ' : '') +
1835
- chalk.gray(when));
1867
+ when.text);
1836
1868
  }
1837
1869
  /** One tree-mode row (grouped under a dir header): id · agent · badges · topic · time. No version/project column. */
1838
1870
  function treeSessionRow(session, live) {
1839
1871
  const agentColor = colorAgent(session.agent);
1840
- const when = formatRelativeTime(session.lastActivity ?? session.timestamp);
1872
+ const age = sessionAgeParts(session.timestamp, session.lastActivity);
1841
1873
  const tag = originTag(session) || teamTag(session);
1842
1874
  const label = session.label;
1843
1875
  const { glyph, preview } = liveGlyphAndPreview(live);
@@ -1853,7 +1885,9 @@ function treeSessionRow(session, live) {
1853
1885
  const head = label ? `${label} · ${topic}` : topic;
1854
1886
  const { cell: statusCell, width: statusW } = liveStatusCell(live);
1855
1887
  const glyphW = glyph ? 2 : 0;
1856
- const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - statusW - badgeW - team.width - stringWidth(when) - 1);
1888
+ const baseTopicW = terminalWidth() - (2 + 9 + 8) - glyphW - statusW - badgeW - team.width - stringWidth(age.last) - 1;
1889
+ const when = timeCell(age, baseTopicW);
1890
+ const topicW = Math.max(12, baseTopicW - when.extraW);
1857
1891
  return (' ' +
1858
1892
  chalk.dim(padToWidth(session.shortId, 9)) +
1859
1893
  agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
@@ -1862,7 +1896,7 @@ function treeSessionRow(session, live) {
1862
1896
  statusCell +
1863
1897
  teamSeg +
1864
1898
  padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
1865
- ' ' + chalk.gray(when));
1899
+ ' ' + when.text);
1866
1900
  }
1867
1901
  /**
1868
1902
  * Live-session index for enriching the default listing, or undefined when
@@ -1884,21 +1918,21 @@ async function maybeLiveIndex(options) {
1884
1918
  }
1885
1919
  }
1886
1920
  /**
1887
- * Group key for the overview: prefer the indexed project name; else fold the cwd
1888
- * to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
1889
- * repo, and a monorepo subdir falls back to its leaf dir basename. Pure.
1921
+ * Group key for the overview: resolve the cwd through the same canonical
1922
+ * resolver the `agents activity` timeline groups by — a defined project's name
1923
+ * when `defs` contains the cwd (multi-repo projects read as one group), else
1924
+ * the repo-level key, so a monorepo subdir (`<repo>/apps/cli`) reads as
1925
+ * `<repo>` in both views. Falls back to the indexed project name (stamped at
1926
+ * scan time) when the cwd carries nothing usable, e.g. a remote path this
1927
+ * machine cannot see still folds by basename through the same resolver.
1890
1928
  */
1891
- export function overviewProjectKey(s) {
1929
+ export function overviewProjectKey(s, defs) {
1930
+ const resolved = defs?.length ? resolveProjectNameForCwd(s.cwd, defs) : resolveProjectKey(s.cwd);
1931
+ if (resolved)
1932
+ return resolved;
1892
1933
  if (s.project && s.project.trim())
1893
1934
  return s.project.trim();
1894
- const cwd = (s.cwd ?? '').replace(/\/+$/, '');
1895
- if (!cwd)
1896
- return '(no project)';
1897
- const wt = cwd.match(/\/([^/]+)\/\.agents\/worktrees\//);
1898
- if (wt)
1899
- return wt[1];
1900
- const parts = cwd.split('/');
1901
- return parts[parts.length - 1] || cwd;
1935
+ return '(no project)';
1902
1936
  }
1903
1937
  /**
1904
1938
  * Turn a recency-descending pool into project groups: each group shows its
@@ -1906,10 +1940,10 @@ export function overviewProjectKey(s) {
1906
1940
  * are ordered by their most-recent session so the newest-active project leads.
1907
1941
  * `perProjectCap = Infinity` expands every group. Pure — unit-tested.
1908
1942
  */
1909
- export function buildOverviewGroups(pool, perProjectCap) {
1943
+ export function buildOverviewGroups(pool, perProjectCap, defs) {
1910
1944
  const byKey = new Map();
1911
1945
  for (const s of pool) {
1912
- const k = overviewProjectKey(s);
1946
+ const k = overviewProjectKey(s, defs);
1913
1947
  (byKey.get(k) ?? byKey.set(k, []).get(k)).push(s);
1914
1948
  }
1915
1949
  const cap = Math.max(1, perProjectCap);
@@ -1927,7 +1961,7 @@ export function buildOverviewGroups(pool, perProjectCap) {
1927
1961
  * `· N more` per project and a `+N more projects` when the list is capped.
1928
1962
  */
1929
1963
  function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
1930
- const { groups } = buildOverviewGroups(pool, opts.expand ? Infinity : opts.perProjectCap);
1964
+ const { groups } = buildOverviewGroups(pool, opts.expand ? Infinity : opts.perProjectCap, listProjectDefs());
1931
1965
  const shownGroups = opts.expand ? groups : groups.slice(0, OVERVIEW_MAX_PROJECTS);
1932
1966
  const hiddenProjects = groups.length - shownGroups.length;
1933
1967
  const total = pool.length;
@@ -2252,7 +2286,7 @@ export function liveHostLabel(a) {
2252
2286
  }
2253
2287
  export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite = false, live) {
2254
2288
  const agentColor = colorAgent(s.agent);
2255
- const when = formatRelativeTime(s.lastActivity ?? s.timestamp);
2289
+ const age = sessionAgeParts(s.timestamp, s.lastActivity);
2256
2290
  const project = s.project || '-';
2257
2291
  // SSH-launch origin (live rows only): mirrors the flat listing's `ssh←<device>`
2258
2292
  // badge. Rendered as its OWN red segment before the topic cell — folding it into
@@ -2306,7 +2340,11 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
2306
2340
  const status = cols.showStatus ? liveStatusCell(live) : { cell: '', width: 0 };
2307
2341
  const statusW = cols.showStatus ? LIVE_STATUS_W : 0;
2308
2342
  const statusCell = cols.showStatus ? (status.cell || ' '.repeat(LIVE_STATUS_W)) : '';
2309
- const topicW = Math.max(16, terminalWidth() - gutter - favW - statusW - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(when) - 1);
2343
+ // Sized against the last-activity label alone; the creation field is then an
2344
+ // additive decision made against whatever width is left (see the flat listing).
2345
+ const baseTopicW = terminalWidth() - gutter - favW - statusW - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(age.last) - 1;
2346
+ const when = timeCell(age, baseTopicW);
2347
+ const topicW = Math.max(MIN_TOPIC_W, baseTopicW - when.extraW);
2310
2348
  return (favCell +
2311
2349
  // Truncated, not just padded: an indexed shortId is always 8 chars, but a
2312
2350
  // live row with no session id is named by its pid or cloud task, which can
@@ -2323,7 +2361,7 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
2323
2361
  renderTopicCell(label, topic, query, topicW, topicW) +
2324
2362
  ticketCell +
2325
2363
  (wt ? wt + ' ' : '') +
2326
- chalk.gray(when));
2364
+ when.text);
2327
2365
  }
2328
2366
  /** Hints rotated above the picker so the flags/features stay discoverable. */
2329
2367
  const PICKER_TIPS = [
@@ -37,6 +37,7 @@ import { capableAgents } from '../lib/capabilities.js';
37
37
  import { parseHookManifest, registerHooksToSettings } from '../lib/hooks.js';
38
38
  import { compileRulesForProject } from '../lib/rules/compile.js';
39
39
  import { runLaunchSync } from '../lib/project-launch.js';
40
+ import { formatKeptProjectResources } from '../lib/project-resources.js';
40
41
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
41
42
  import { runUmbrellaSync } from '../lib/sync-umbrella.js';
42
43
  import { addHostOption } from '../lib/hosts/option.js';
@@ -516,8 +517,11 @@ function printSyncDetail(result, agent, version, cwd) {
516
517
  lines.push({ kind: 'plugins', items: result.plugins });
517
518
  if (result.workflows.length > 0)
518
519
  lines.push({ kind: 'workflows', items: result.workflows });
520
+ const kept = formatKeptProjectResources(result.projectSkipped);
519
521
  if (lines.length === 0) {
520
522
  console.log(chalk.gray(`Already in sync — ${agentLabel(agent)}@${version}`));
523
+ if (kept)
524
+ console.log(chalk.gray(kept));
521
525
  return;
522
526
  }
523
527
  console.log(chalk.green(`Synced to ${agentLabel(agent)}@${version}:`));
@@ -531,6 +535,8 @@ function printSyncDetail(result, agent, version, cwd) {
531
535
  const count = chalk.cyan(`(${sorted.length})`.padStart(5));
532
536
  console.log(` ${chalk.bold(padded)} ${count} ${chalk.gray(preview)}${more}`);
533
537
  }
538
+ if (kept)
539
+ console.log(chalk.gray(kept));
534
540
  }
535
541
  function runLaunchMode(agent, version, cwd, quiet) {
536
542
  let result;
@@ -561,7 +567,7 @@ function runLaunchMode(agent, version, cwd, quiet) {
561
567
  else {
562
568
  console.log(chalk.green(`Launch sync: ${bits.join(', ')}`));
563
569
  }
564
- if (result.workspaceSkipped.length > 0) {
565
- console.log(chalk.yellow(`Skipped (user-owned, not overwritten): ${result.workspaceSkipped.join(', ')}`));
566
- }
570
+ const kept = formatKeptProjectResources(result.workspaceSkipped);
571
+ if (kept)
572
+ console.log(chalk.gray(kept));
567
573
  }
@@ -11,6 +11,7 @@ import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth
11
11
  import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
12
12
  import { readManifest } from '../lib/manifest.js';
13
13
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
14
+ import { formatKeptProjectResources } from '../lib/project-resources.js';
14
15
  import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
15
16
  import { getAgentResources } from '../lib/resources.js';
16
17
  import { renderMergedResources } from '../lib/merged-resources.js';
@@ -722,6 +723,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
722
723
  if (synced.length > 0) {
723
724
  console.log(chalk.green(`\nSynced to ${agentLabel(filterAgentId)}@${defaultVersion}: ${synced.join(', ')}`));
724
725
  }
726
+ const kept = formatKeptProjectResources(result.projectSkipped);
727
+ if (kept)
728
+ console.log(chalk.gray(kept));
725
729
  }
726
730
  }
727
731
  catch (err) {
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, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, 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, loadHq, 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, loadDrive, 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, loadHq, 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';
@@ -147,11 +147,25 @@ function auditCommandPath(cmd) {
147
147
  return parts;
148
148
  }
149
149
  const auditStarts = new WeakMap();
150
+ /**
151
+ * Commands that WRITE the event stream, so recording their own invocation would
152
+ * add records to the log they are writing into. `events emit` is batched — one
153
+ * flush every few seconds per open editor window — so auditing it would bury the
154
+ * real events under two `command.*` records per flush. `_internal friction`
155
+ * exists for the same reason (shell guards fire before any `agents` process
156
+ * exists, so they cannot emit in-process) and had the same self-logging bug.
157
+ */
158
+ const AUDIT_EXEMPT_COMMANDS = new Set([
159
+ 'events emit',
160
+ '_internal friction',
161
+ ]);
150
162
  program.hook('preAction', (_thisCommand, actionCommand) => {
151
163
  try {
152
164
  const parts = auditCommandPath(actionCommand);
153
165
  if (parts.length === 0)
154
166
  return;
167
+ if (AUDIT_EXEMPT_COMMANDS.has(parts.join(' ')))
168
+ return;
155
169
  auditStarts.set(actionCommand, Date.now());
156
170
  emit('command.start', {
157
171
  module: parts[0],
@@ -172,6 +186,8 @@ program.hook('postAction', (_thisCommand, actionCommand) => {
172
186
  const parts = auditCommandPath(actionCommand);
173
187
  if (parts.length === 0)
174
188
  return;
189
+ if (AUDIT_EXEMPT_COMMANDS.has(parts.join(' ')))
190
+ return;
175
191
  const started = auditStarts.get(actionCommand);
176
192
  const durationMs = started !== undefined ? Date.now() - started : undefined;
177
193
  const command = parts.join(' ');
@@ -871,6 +887,7 @@ async function registerAllEagerCommands() {
871
887
  await reg(loadPackages);
872
888
  await reg(loadRoutines);
873
889
  await reg(loadMonitors);
890
+ await reg(loadProjects);
874
891
  await reg(loadRun);
875
892
  await reg(loadFork);
876
893
  await reg(loadDefaults);