@phnx-labs/agents-cli 1.22.114 → 1.22.116

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 (243) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +210 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +214 -148
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/feed.js +65 -14
  19. package/dist/commands/focus.js +1 -1
  20. package/dist/commands/go.d.ts +0 -17
  21. package/dist/commands/go.js +2 -19
  22. package/dist/commands/logs.js +1 -1
  23. package/dist/commands/mcp.js +8 -83
  24. package/dist/commands/memory.js +4 -47
  25. package/dist/commands/message.js +4 -4
  26. package/dist/commands/plugins.js +1 -93
  27. package/dist/commands/repo.js +0 -44
  28. package/dist/commands/resume.js +4 -1
  29. package/dist/commands/secrets-passthrough.js +2 -2
  30. package/dist/commands/send.d.ts +4 -4
  31. package/dist/commands/send.js +6 -51
  32. package/dist/commands/sessions-backup-setup.js +1 -1
  33. package/dist/commands/sessions-picker.d.ts +11 -0
  34. package/dist/commands/sessions-picker.js +88 -7
  35. package/dist/commands/sessions-resume.js +0 -1
  36. package/dist/commands/sessions-share.d.ts +5 -7
  37. package/dist/commands/sessions-share.js +98 -49
  38. package/dist/commands/sessions.d.ts +21 -2
  39. package/dist/commands/sessions.js +158 -11
  40. package/dist/commands/setup-browser.js +18 -2
  41. package/dist/commands/setup-computer.js +20 -6
  42. package/dist/commands/setup-secrets.d.ts +2 -2
  43. package/dist/commands/setup-secrets.js +24 -4
  44. package/dist/commands/setup-term.d.ts +24 -0
  45. package/dist/commands/setup-term.js +70 -0
  46. package/dist/commands/setup-terminal.d.ts +3 -0
  47. package/dist/commands/setup-terminal.js +22 -0
  48. package/dist/commands/setup.d.ts +1 -1
  49. package/dist/commands/setup.js +29 -11
  50. package/dist/commands/skills.js +0 -8
  51. package/dist/commands/ssh.d.ts +6 -0
  52. package/dist/commands/ssh.js +93 -302
  53. package/dist/commands/sync.js +14 -5
  54. package/dist/commands/teams.js +1 -1
  55. package/dist/commands/traces.js +1 -1
  56. package/dist/lib/accounting/rotate.d.ts +63 -1
  57. package/dist/lib/accounting/rotate.js +56 -0
  58. package/dist/lib/accounts/add.d.ts +0 -5
  59. package/dist/lib/accounts/add.js +3 -9
  60. package/dist/lib/accounts/slots.js +32 -2
  61. package/dist/lib/answer-router.d.ts +11 -2
  62. package/dist/lib/answer-router.js +26 -2
  63. package/dist/lib/artifacts-client.d.ts +20 -0
  64. package/dist/lib/artifacts-client.js +46 -0
  65. package/dist/lib/auth-mint.d.ts +5 -4
  66. package/dist/lib/auth-mint.js +6 -5
  67. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  68. package/dist/lib/browser/runtime-state.d.ts +55 -0
  69. package/dist/lib/browser/runtime-state.js +99 -18
  70. package/dist/lib/browser/service.d.ts +10 -0
  71. package/dist/lib/browser/service.js +229 -37
  72. package/dist/lib/browser/types.d.ts +18 -0
  73. package/dist/lib/cli-resources.js +3 -1
  74. package/dist/lib/cloud/dispatch.js +1 -1
  75. package/dist/lib/cloudflare/creds.d.ts +10 -0
  76. package/dist/lib/cloudflare/creds.js +46 -0
  77. package/dist/lib/cloudflare/provision.d.ts +35 -0
  78. package/dist/lib/cloudflare/provision.js +144 -0
  79. package/dist/lib/computer/sessions-list.d.ts +55 -0
  80. package/dist/lib/computer/sessions-list.js +168 -1
  81. package/dist/lib/config-keys.d.ts +1 -1
  82. package/dist/lib/config-keys.js +5 -0
  83. package/dist/lib/daemon/daemon.js +8 -1
  84. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  85. package/dist/lib/daemon/feed-stream-service.js +40 -0
  86. package/dist/lib/daemon-services.d.ts +1 -1
  87. package/dist/lib/daemon-services.js +5 -0
  88. package/dist/lib/device-config.js +61 -0
  89. package/dist/lib/devices/connect.d.ts +49 -5
  90. package/dist/lib/devices/connect.js +169 -21
  91. package/dist/lib/devices/doctor-findings.js +2 -6
  92. package/dist/lib/feed/answer.d.ts +153 -4
  93. package/dist/lib/feed/answer.js +716 -105
  94. package/dist/lib/feed/envelope.d.ts +79 -0
  95. package/dist/lib/feed/envelope.js +23 -0
  96. package/dist/lib/feed/events.d.ts +6 -0
  97. package/dist/lib/feed/events.js +8 -0
  98. package/dist/lib/feed/feed.d.ts +61 -1
  99. package/dist/lib/feed/feed.js +226 -14
  100. package/dist/lib/feed/hub-server.d.ts +141 -0
  101. package/dist/lib/feed/hub-server.js +586 -0
  102. package/dist/lib/feed/hub.d.ts +95 -0
  103. package/dist/lib/feed/hub.js +255 -0
  104. package/dist/lib/feed/pr-status.d.ts +8 -0
  105. package/dist/lib/feed/pr-status.js +9 -1
  106. package/dist/lib/feed/tool-activity.d.ts +108 -0
  107. package/dist/lib/feed/tool-activity.js +313 -0
  108. package/dist/lib/feed/tools.d.ts +198 -0
  109. package/dist/lib/feed/tools.js +265 -0
  110. package/dist/lib/feed/watch.d.ts +50 -50
  111. package/dist/lib/feed/watch.js +147 -16
  112. package/dist/lib/feed-outcome.d.ts +1 -1
  113. package/dist/lib/feed-outcome.js +9 -2
  114. package/dist/lib/feed-policy.js +9 -3
  115. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  116. package/dist/lib/fleet/auth-sync.js +2 -89
  117. package/dist/lib/format.d.ts +1 -1
  118. package/dist/lib/format.js +1 -1
  119. package/dist/lib/git.d.ts +0 -16
  120. package/dist/lib/git.js +0 -58
  121. package/dist/lib/harness-auth-capabilities.js +7 -2
  122. package/dist/lib/helper-versions.js +1 -1
  123. package/dist/lib/hosts/dispatch.d.ts +20 -1
  124. package/dist/lib/hosts/dispatch.js +52 -30
  125. package/dist/lib/hosts/remote-cmd.d.ts +72 -1
  126. package/dist/lib/hosts/remote-cmd.js +151 -10
  127. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  128. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  129. package/dist/lib/mailbox.d.ts +12 -0
  130. package/dist/lib/mailbox.js +16 -2
  131. package/dist/lib/mcp.js +17 -11
  132. package/dist/lib/menubar/snapshot.d.ts +51 -0
  133. package/dist/lib/menubar/snapshot.js +42 -3
  134. package/dist/lib/open-url.js +2 -2
  135. package/dist/lib/probe.d.ts +4 -1
  136. package/dist/lib/probe.js +5 -2
  137. package/dist/lib/projects.d.ts +23 -0
  138. package/dist/lib/projects.js +78 -0
  139. package/dist/lib/pwsh.d.ts +33 -0
  140. package/dist/lib/pwsh.js +56 -0
  141. package/dist/lib/redact.d.ts +8 -0
  142. package/dist/lib/redact.js +11 -0
  143. package/dist/lib/refresh.d.ts +6 -2
  144. package/dist/lib/refresh.js +92 -72
  145. package/dist/lib/secrets-cli.d.ts +11 -0
  146. package/dist/lib/secrets-cli.js +30 -0
  147. package/dist/lib/secrets-client.js +3 -2
  148. package/dist/lib/session/active.d.ts +1 -0
  149. package/dist/lib/session/active.js +8 -0
  150. package/dist/lib/session/db.d.ts +67 -3
  151. package/dist/lib/session/db.js +381 -126
  152. package/dist/lib/session/detached.d.ts +7 -0
  153. package/dist/lib/session/detached.js +29 -0
  154. package/dist/lib/session/prompt.d.ts +23 -7
  155. package/dist/lib/session/prompt.js +46 -8
  156. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  157. package/dist/lib/session/remote/peer-stream.js +33 -6
  158. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  159. package/dist/lib/session/remote/remote-list.js +22 -6
  160. package/dist/lib/session/remote/watch.d.ts +12 -0
  161. package/dist/lib/session/remote/watch.js +9 -0
  162. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  163. package/dist/lib/session/remote-preview-cache.js +373 -0
  164. package/dist/lib/session/sync/backend.d.ts +3 -3
  165. package/dist/lib/session/sync/backend.js +3 -3
  166. package/dist/lib/session/sync/provision.d.ts +1 -1
  167. package/dist/lib/session/sync/provision.js +2 -2
  168. package/dist/lib/session/tail.d.ts +50 -0
  169. package/dist/lib/session/tail.js +219 -0
  170. package/dist/lib/sessions-client.js +0 -3
  171. package/dist/lib/setup-tool-install.d.ts +3 -0
  172. package/dist/lib/setup-tool-install.js +27 -0
  173. package/dist/lib/setup-tool-status.d.ts +22 -0
  174. package/dist/lib/setup-tool-status.js +220 -0
  175. package/dist/lib/share-runtime.d.ts +11 -0
  176. package/dist/lib/share-runtime.js +63 -0
  177. package/dist/lib/signin-badge.d.ts +19 -4
  178. package/dist/lib/signin-badge.js +29 -11
  179. package/dist/lib/smart-launch.d.ts +1 -5
  180. package/dist/lib/smart-launch.js +3 -11
  181. package/dist/lib/ssh-exec.d.ts +44 -0
  182. package/dist/lib/ssh-exec.js +119 -0
  183. package/dist/lib/startup/command-registry.d.ts +6 -4
  184. package/dist/lib/startup/command-registry.js +10 -7
  185. package/dist/lib/state.js +2 -2
  186. package/dist/lib/storage/selection.d.ts +2 -2
  187. package/dist/lib/storage/selection.js +2 -2
  188. package/dist/lib/sync-umbrella.d.ts +5 -0
  189. package/dist/lib/sync-umbrella.js +18 -10
  190. package/dist/lib/term-driver.d.ts +24 -0
  191. package/dist/lib/term-driver.js +36 -0
  192. package/dist/lib/terminal/index.d.ts +1 -1
  193. package/dist/lib/terminal/index.js +1 -1
  194. package/dist/lib/terminal/inject.d.ts +38 -0
  195. package/dist/lib/terminal/inject.js +55 -9
  196. package/dist/lib/terminal/transport.d.ts +15 -5
  197. package/dist/lib/terminal/transport.js +61 -11
  198. package/dist/lib/traces/backend.d.ts +1 -2
  199. package/dist/lib/traces/backend.js +1 -2
  200. package/dist/lib/traces/provision.d.ts +1 -1
  201. package/dist/lib/traces/provision.js +2 -2
  202. package/dist/lib/types.d.ts +8 -6
  203. package/package.json +2 -3
  204. package/dist/commands/artifacts-setup.d.ts +0 -53
  205. package/dist/commands/artifacts-setup.js +0 -161
  206. package/dist/commands/artifacts.d.ts +0 -18
  207. package/dist/commands/artifacts.js +0 -58
  208. package/dist/commands/attach.d.ts +0 -12
  209. package/dist/commands/attach.js +0 -86
  210. package/dist/commands/auth-mint.d.ts +0 -12
  211. package/dist/commands/auth-mint.js +0 -108
  212. package/dist/commands/reconnect.d.ts +0 -46
  213. package/dist/commands/reconnect.js +0 -115
  214. package/dist/commands/share.d.ts +0 -293
  215. package/dist/commands/share.js +0 -1424
  216. package/dist/lib/fleet/remote-login.d.ts +0 -170
  217. package/dist/lib/fleet/remote-login.js +0 -568
  218. package/dist/lib/share/analytics.d.ts +0 -13
  219. package/dist/lib/share/analytics.js +0 -45
  220. package/dist/lib/share/backend.d.ts +0 -120
  221. package/dist/lib/share/backend.js +0 -176
  222. package/dist/lib/share/capture.d.ts +0 -31
  223. package/dist/lib/share/capture.js +0 -174
  224. package/dist/lib/share/config.d.ts +0 -72
  225. package/dist/lib/share/config.js +0 -211
  226. package/dist/lib/share/delete.d.ts +0 -123
  227. package/dist/lib/share/delete.js +0 -173
  228. package/dist/lib/share/html.d.ts +0 -20
  229. package/dist/lib/share/html.js +0 -88
  230. package/dist/lib/share/http-error.d.ts +0 -53
  231. package/dist/lib/share/http-error.js +0 -65
  232. package/dist/lib/share/og.d.ts +0 -26
  233. package/dist/lib/share/og.js +0 -84
  234. package/dist/lib/share/provision.d.ts +0 -127
  235. package/dist/lib/share/provision.js +0 -285
  236. package/dist/lib/share/publish.d.ts +0 -379
  237. package/dist/lib/share/publish.js +0 -818
  238. package/dist/lib/share/worker-template.d.ts +0 -27
  239. package/dist/lib/share/worker-template.js +0 -2424
  240. package/dist/lib/storage/index.d.ts +0 -14
  241. package/dist/lib/storage/index.js +0 -14
  242. package/dist/lib/storage/visibility.d.ts +0 -82
  243. package/dist/lib/storage/visibility.js +0 -99
@@ -35,7 +35,9 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
35
35
  import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
36
36
  import { inferSessionState } from '../lib/session/state.js';
37
37
  import { discoverSessions, queryIndexedSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, looksLikeSessionId, searchContentIndex, getSessionRoots, scopeToManaged } from '../lib/session/discover.js';
38
- import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview } from '../lib/session/db.js';
38
+ import { findSessionsById, querySessions, readSessionContent, readArchivedSessionPreview, readSessionTimelineAny } from '../lib/session/db.js';
39
+ import { foldTimeline, emptyTimelineState, projectTimeline, projectSessionFiles } from '../lib/session/timeline.js';
40
+ import { readSessionTail } from '../lib/session/tail.js';
39
41
  import { liveSessionMetas, fleetExecutionMachineById, reconcileLiveMetaMachine } from '../lib/session/live-metadata.js';
40
42
  import { sessionHeadline } from '../lib/session/title.js';
41
43
  import { filterTeamSessions, shouldShowTeamSessions, safeTeamText, groupSessionsByTeam, NO_TEAM_GROUP_KEY, } from '../lib/session/team-filter.js';
@@ -64,12 +66,9 @@ import { registerSessionsResumeCommand } from './sessions-resume.js';
64
66
  import { registerSessionsForkCommand } from './fork.js';
65
67
  import { registerSessionsBookmarkCommand } from './sessions-bookmark.js';
66
68
  import { isBookmarked, listBookmarks } from '../lib/session/bookmarks.js';
67
- import { registerGoCommand } from './go.js';
68
69
  import { registerFocusCommand } from './focus.js';
69
- import { registerReconnectCommand } from './reconnect.js';
70
70
  import { registerDetachCommand } from './detach.js';
71
71
  import { registerSessionsStopCommand } from './sessions-stop.js';
72
- import { registerAttachCommand } from './attach.js';
73
72
  import { registerSessionsInjectCommand } from './sessions-inject.js';
74
73
  import { registerSessionsExportCommand } from './sessions-export.js';
75
74
  import { registerSessionsRenderCommand } from './sessions-render.js';
@@ -1985,9 +1984,151 @@ function canonicalSessionsCommand(query, options) {
1985
1984
  a.push(JSON.stringify(q));
1986
1985
  return 'ag ' + a.join(' ');
1987
1986
  }
1987
+ /** Bound on the `details.messages` array and on each message's `text` — this
1988
+ * rides a cached JSON payload (and, for the remote fast path, an SSH capture
1989
+ * with its own byte cap), so it is capped independently of either. */
1990
+ const SESSION_DETAIL_MAX_MESSAGES = 8;
1991
+ const SESSION_DETAIL_MESSAGE_MAX_CHARS = 4_000;
1992
+ /**
1993
+ * The additive `details` block on `sessions preview --json` (PHNX-3999):
1994
+ * `request`/`timeline`/`files` are read FIRST from the existing
1995
+ * `session_timelines` projection (`readSessionTimelineAny` — the daemon's own
1996
+ * bounded, incrementally-folded cache, `SessionTimelineProjection` in
1997
+ * `lib/session/db.ts`), so a session the daemon has already reached costs
1998
+ * nothing extra here. When that projection is missing (daemon hasn't reached
1999
+ * this session yet), this does an ON-DEMAND fold using the EXACT same pure,
2000
+ * already-redacting pipeline the daemon uses (`foldTimeline` /
2001
+ * `projectTimeline` / `projectSessionFiles`, `timeline.ts`) over whatever
2002
+ * events are cheaply available: the fresh-parse `events` from
2003
+ * `loadSessionPreviewDigest` when there is one, else a bounded 128 KiB tail
2004
+ * read (`readSessionTail`, Claude/Codex only — the same bounded reader the
2005
+ * local preview's over-cap fallback uses) so a WARM cache hit (no fresh parse)
2006
+ * still gets real, on-demand content instead of falling back to a 2-message
2007
+ * digest summary. This is genuinely bounded either way: it folds only events
2008
+ * already in hand or a fixed 128 KiB window, never a fresh whole-file parse.
2009
+ * `partial`/`reason` say when this happened (`daemon-pending: on-demand
2010
+ * bounded fold`) versus a session with no transcript at all.
2011
+ */
2012
+ function sessionTranscriptStamp(session) {
2013
+ try {
2014
+ if (!session.filePath)
2015
+ return undefined;
2016
+ const stat = fs.statSync(session.filePath);
2017
+ return { fileMtimeMs: stat.mtimeMs, fileSize: stat.size };
2018
+ }
2019
+ catch {
2020
+ return undefined;
2021
+ }
2022
+ }
2023
+ export function buildSessionDetailBlock(session, digest, events, sourceStamp) {
2024
+ const bound = (text) => redactSecrets(sanitizeForTerminal(text)).slice(0, SESSION_DETAIL_MESSAGE_MAX_CHARS);
2025
+ const stamp = sourceStamp ?? sessionTranscriptStamp(session);
2026
+ const canDateEvents = sourceStamp !== undefined || events.length === 0;
2027
+ const daemonProjection = stamp ? readSessionTimelineAny(session.id, stamp) : undefined;
2028
+ // A bounded tail read backs `messages` on EVERY warm cache hit (no fresh
2029
+ // parse this call), REGARDLESS of whether the daemon projection is already
2030
+ // present — this is what fixes a real bug review caught: a warm digest hit
2031
+ // whose daemon timeline HAD landed still fell through to the 2-message
2032
+ // digest summary below, because the tail-read used to be gated on the
2033
+ // on-demand-fold branch only. `messages` and the on-demand fold are two
2034
+ // independent uses of the same cheap, bounded events window.
2035
+ let foldEvents = events;
2036
+ if (foldEvents.length === 0 && session.filePath) {
2037
+ foldEvents = readSessionTail(session.filePath, session.agent);
2038
+ }
2039
+ let projection = daemonProjection;
2040
+ let onDemand = false;
2041
+ if (!projection && foldEvents.length > 0) {
2042
+ const state = foldTimeline(foldEvents, emptyTimelineState());
2043
+ projection = {
2044
+ request: state.request,
2045
+ timeline: projectTimeline(state, undefined),
2046
+ files: projectSessionFiles(state),
2047
+ };
2048
+ onDemand = true;
2049
+ }
2050
+ let messages;
2051
+ if (foldEvents.length > 0) {
2052
+ messages = foldEvents
2053
+ .filter((e) => e.type === 'message' && !e._synthetic && Boolean(e.content) && (e.role === 'user' || e.role === 'assistant'))
2054
+ .slice(-SESSION_DETAIL_MAX_MESSAGES)
2055
+ .map(e => ({ role: e.role, text: bound(e.content), at: e.timestamp ?? null }));
2056
+ }
2057
+ else {
2058
+ messages = [];
2059
+ if (digest?.firstUser)
2060
+ messages.push({ role: 'user', text: bound(digest.firstUser), at: session.timestamp ?? null });
2061
+ if (digest?.lastAssistant)
2062
+ messages.push({ role: 'assistant', text: bound(digest.lastAssistant), at: session.lastActivity ?? null });
2063
+ }
2064
+ const endStamp = sessionTranscriptStamp(session);
2065
+ const unchanged = canDateEvents && stamp !== undefined && endStamp !== undefined
2066
+ && stamp.fileMtimeMs === endStamp.fileMtimeMs && stamp.fileSize === endStamp.fileSize;
2067
+ const partial = Boolean(digest?.partial) || !daemonProjection || !unchanged;
2068
+ const reason = digest?.partialReason
2069
+ ?? (onDemand
2070
+ ? 'background timeline pass has not reached this session yet; request/timeline/files below are an on-demand bounded fold, not the full-history daemon projection'
2071
+ : (!projection ? 'no transcript available to fold request/timeline/files for this session' : null));
2072
+ return {
2073
+ request: projection?.request ?? null,
2074
+ timeline: projection?.timeline ?? null,
2075
+ files: projection?.files ?? null,
2076
+ messages,
2077
+ sourceRevision: unchanged ? new Date(stamp.fileMtimeMs).toISOString() : null,
2078
+ partial,
2079
+ reason,
2080
+ };
2081
+ }
1988
2082
  /** Resolve a session by id/query globally and print its compact preview (no pager).
1989
2083
  * Backs `--preview` — the fast path for the "peek before resume" hot loop. */
1990
2084
  export async function renderSessionPreview(query, scope) {
2085
+ // Exact ID + exactly one named, non-local device: the canonical bounded
2086
+ // preview loader (PHNX-3999) answers in ONE ssh hop plus a durable local
2087
+ // cache, instead of the general resolver's metadata fan-out followed by a
2088
+ // second render hop. Scoped tightly on purpose — a short id/label is not a
2089
+ // stable cache key across devices, and this never touches local discovery.
2090
+ //
2091
+ // Gated on `isCompleteSessionId`, NOT a bare-UUID regex: measured over this
2092
+ // fleet's own index, a "full session id" is a bare UUID for most harnesses
2093
+ // but `session_<uuid>` for kimi/rush and `ses_<ulid>` for opencode
2094
+ // (discover.ts). A bare-UUID-only check would silently skip this fast path
2095
+ // (falling through to the slower general resolver, never wrong, but wrong
2096
+ // to claim as "the fast path" for those harnesses) for a real, exact,
2097
+ // caller-supplied full id on those three.
2098
+ if (scope.json && !scope.local && scope.hosts?.length === 1
2099
+ && scope.hosts[0] !== machineId() && isCompleteSessionId(query.trim())) {
2100
+ const { getRemoteSessionPreview } = await import('../lib/session/remote-preview-cache.js');
2101
+ const result = await getRemoteSessionPreview(query.trim(), scope.hosts[0], {
2102
+ refresh: scope.refresh,
2103
+ revision: scope.revision,
2104
+ });
2105
+ const envelope = result.envelope;
2106
+ console.log(JSON.stringify({
2107
+ schemaVersion: 1,
2108
+ session: envelope?.session ?? null,
2109
+ // `active` is a live snapshot the PEER computed at the moment it ran
2110
+ // `sessions preview --local --json` — meaningful only for that instant.
2111
+ // When this response is served from the durable cache (`cache.source ===
2112
+ // 'cache'`, which can be up to 45s old, or indefinitely old under a
2113
+ // matched --revision), presenting that stale snapshot under the same
2114
+ // `active` key would read as the session's CURRENT working/idle status
2115
+ // when it may no longer be. Null it out rather than relabel it with an
2116
+ // age stamp: the caller already has `cache.fetchedAt`/`cache.stale` to
2117
+ // reason about staleness, and getting a fresh `active` read costs
2118
+ // another SSH round trip this fast path exists specifically to avoid.
2119
+ active: result.cache.source === 'live' ? (envelope?.active ?? null) : null,
2120
+ preview: envelope?.preview ?? null,
2121
+ error: envelope?.error ?? (envelope ? null : result.cache.reason),
2122
+ // The peer's own `sessions preview --local --json` already computed
2123
+ // `details` (request/timeline/files/messages) with its own bounded reader
2124
+ // (see buildSessionDetailBlock) — this hop just forwards it verbatim,
2125
+ // never re-derives it, so a metadata-only/no-transcript peer row's
2126
+ // explicit unavailable reason survives the hop unchanged.
2127
+ details: envelope?.details ?? null,
2128
+ cache: result.cache,
2129
+ }));
2130
+ return;
2131
+ }
1991
2132
  let outcome = await resolveSessionMetadataValue(query, scope);
1992
2133
  // resolveSessionMetadataValue already consults the live registry for a running
1993
2134
  // session with no transcript row (RUSH-2682), so a live session resolves above.
@@ -2062,7 +2203,8 @@ export async function renderSessionPreview(query, scope) {
2062
2203
  }
2063
2204
  catch { /* plain preview on any probe failure */ }
2064
2205
  if (scope.json) {
2065
- const { digest, error } = loadSessionPreviewDigest(session);
2206
+ const sourceStamp = sessionTranscriptStamp(session);
2207
+ const { digest, error, events } = loadSessionPreviewDigest(session);
2066
2208
  console.log(JSON.stringify({
2067
2209
  schemaVersion: 1,
2068
2210
  session: {
@@ -2098,6 +2240,7 @@ export async function renderSessionPreview(query, scope) {
2098
2240
  } : null,
2099
2241
  preview: digest ?? null,
2100
2242
  error: error ?? null,
2243
+ details: buildSessionDetailBlock(session, digest, events, sourceStamp),
2101
2244
  }));
2102
2245
  return;
2103
2246
  }
@@ -3963,7 +4106,7 @@ async function resumeOnOwnerIfRemote(session) {
3963
4106
  export async function resumeSessionInPlace(session) {
3964
4107
  // This function is the LOCAL takeover, and every caller is responsible for
3965
4108
  // routing a peer-owned session before it gets here (the picker above,
3966
- // `agents sessions resume`, `sessions attach`). Reaching it with one anyway means a
4109
+ // `agents sessions resume`). Reaching it with one anyway means a
3967
4110
  // caller skipped that step, so refuse rather than start the harness against
3968
4111
  // state this box does not have — the `fs.existsSync(session.cwd)` fallback
3969
4112
  // just below is exactly how that used to pass unnoticed, quietly swapping in
@@ -5317,7 +5460,6 @@ export function registerSessionsCommands(program) {
5317
5460
  resume <id> --attach-only attach only; never fork a copy
5318
5461
  resume multi-select history -> open tabs
5319
5462
  detach <id> the other direction: interactive -> headless
5320
- (retired, still working for one release: sessions attach, sessions go, reconnect)
5321
5463
  - The interactive listing and every live-status flag fold in your other online machines automatically (live over SSH, no sync) — each row is labelled by host, this machine first. Use --local to skip the fan-out; single-id lookups stay local.
5322
5464
  - --all is not a device flag: it widens historical directory and time filters. Fleet collection is already the default. A status flag (--working/--idle/--waiting/--orphan/--crashed/--closed/--abandoned/--queued/--unknown) implies --active; combine status flags for a union.
5323
5465
  - --version <version> requires --agent and is equivalent to --agent <agent@version>.
@@ -5374,7 +5516,9 @@ export function registerSessionsCommands(program) {
5374
5516
  .option('-p, --project <name>', 'Narrow the ID to one project')
5375
5517
  .option('--local', 'Only this machine; do not resolve the ID across the fleet')
5376
5518
  .option('-D, --device <target...>', 'Resolve only on the named device(s)')
5377
- .option('--json', 'Output the session preview as JSON');
5519
+ .option('--json', 'Output the session preview as JSON')
5520
+ .option('--refresh', 'Bypass the durable remote-preview cache and negative backoff for one bounded fetch (full ID + single --device only)')
5521
+ .option('--revision <cursor>', 'Opaque caller-owned activity cursor (any stable value YOU track, e.g. your own feed\'s lastActivityMs) -- passing the SAME value as your last call confirms nothing changed and serves the cache with zero SSH indefinitely; a different value fetches once, still subject to backoff unless --refresh is also set (full ID + single --device only)');
5378
5522
  setHelpSections(previewCmd, {
5379
5523
  examples: `
5380
5524
  # Preview by the 8-character ID shown in agents sessions
@@ -5386,11 +5530,15 @@ export function registerSessionsCommands(program) {
5386
5530
  # Stay on this machine or restrict the authoritative lookup to one peer
5387
5531
  agents sessions preview 407b8dd5 --local
5388
5532
  agents sessions preview 407b8dd5 --device zion
5533
+
5534
+ # Full ID + one --device: durable-cached fast path (PHNX-3999); force a fresh fetch
5535
+ agents sessions preview c70ecdea-6210-4039-9845-246a3a7a9942 --device zion --json --refresh
5389
5536
  `,
5390
5537
  notes: `
5391
5538
  - Full UUIDs are globally unique and may stop the fleet lookup at the first exact hit.
5392
5539
  - Short IDs wait for every selected device so ambiguity is never hidden.
5393
5540
  - Active status is refreshed through the bounded live-state TTL; transcript-derived details use the durable session index.
5541
+ - A full UUID with exactly one --device and --json is served from a local durable cache (~45s fresh window); the JSON envelope's "cache" field reports fresh/stale/offline state. --refresh forces one bounded re-fetch.
5394
5542
  `,
5395
5543
  });
5396
5544
  previewCmd.action(async (id) => {
@@ -5402,18 +5550,17 @@ export function registerSessionsCommands(program) {
5402
5550
  local: options.local,
5403
5551
  hosts: hosts.length > 0 ? hosts : undefined,
5404
5552
  json: options.json,
5553
+ refresh: options.refresh,
5554
+ revision: options.revision,
5405
5555
  });
5406
5556
  });
5407
5557
  registerSessionsTailCommand(sessionsCmd);
5408
5558
  registerSessionsResumeCommand(sessionsCmd);
5409
5559
  registerSessionsForkCommand(sessionsCmd);
5410
5560
  registerSessionsBookmarkCommand(sessionsCmd);
5411
- registerGoCommand(sessionsCmd);
5412
5561
  registerFocusCommand(sessionsCmd);
5413
- registerReconnectCommand(sessionsCmd);
5414
5562
  registerDetachCommand(sessionsCmd);
5415
5563
  registerSessionsStopCommand(sessionsCmd);
5416
- registerAttachCommand(sessionsCmd);
5417
5564
  registerSessionsInjectCommand(sessionsCmd);
5418
5565
  registerSessionsExportCommand(sessionsCmd);
5419
5566
  registerSessionsRenderCommand(sessionsCmd);
@@ -7,6 +7,7 @@
7
7
  * Idempotent: re-running shows the current default profile and offers to change
8
8
  * the pinned browser or re-point the device default.
9
9
  */
10
+ import { openSetupTerminal } from './setup-terminal.js';
10
11
  import chalk from 'chalk';
11
12
  import { setConfigValue } from '../lib/device-config.js';
12
13
  import { listInstalledBrowsers } from '../lib/browser/chrome.js';
@@ -14,6 +15,8 @@ import { DEFAULT_BROWSER_PROFILE_NAME, getAutoDetectedProfile, createProfile, fi
14
15
  import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
15
16
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
16
17
  import { defaultBrowserChoice } from './setup-preferences.js';
18
+ import { installSetupTool } from '../lib/setup-tool-install.js';
19
+ import { refreshToolSetup } from '../lib/setup-tool-status.js';
17
20
  const INSTALL_HINT = 'Install one of: Google Chrome, Brave, Microsoft Edge, Chromium, Comet, or Arc, then re-run `agents setup browser`.\n' +
18
21
  '(This wizard picks a Chromium-family browser. An installed Firefox is not auto-picked here; its profiles are ' +
19
22
  'discovered from profiles.ini and selected with `agents browser use firefox-<name>`. Safari is not supported.)';
@@ -116,9 +119,22 @@ export function registerSetupBrowserCommand(setupCmd) {
116
119
  setupCmd
117
120
  .command('browser')
118
121
  .description('Set up `agents browser` — detect an installed browser and create the default profile.')
119
- .action(async () => {
122
+ .option('--install-only', 'Install the standalone Browser CLI without changing profiles or starting a browser')
123
+ .option('--terminal [backend]', 'Open interactive setup in a detected or selected terminal')
124
+ .action(async (options) => {
120
125
  try {
121
- await runBrowserWizard();
126
+ if (options.terminal !== undefined) {
127
+ await openSetupTerminal('browser', options.terminal, options.installOnly);
128
+ return;
129
+ }
130
+ if (options.installOnly) {
131
+ if (!(await installSetupTool('browser')))
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+ if (!(await runBrowserWizard()))
136
+ process.exitCode = 1;
137
+ await refreshToolSetup('browser');
122
138
  }
123
139
  catch (err) {
124
140
  if (isPromptCancelled(err)) {
@@ -8,12 +8,15 @@
8
8
  * this wizard chains them with the permission hand-holding a fresh machine needs.
9
9
  * Idempotent: re-running re-installs the current helper and re-checks trust.
10
10
  */
11
+ import { openSetupTerminal } from './setup-terminal.js';
11
12
  import os from 'node:os';
12
- import { execFileSync, spawnSync } from 'node:child_process';
13
+ import { execFileSync } from 'node:child_process';
13
14
  import chalk from 'chalk';
14
15
  import { installComputerHelperMacLocal, activateComputerHelperMacLocal, probeComputerTrust, } from './computer.js';
15
16
  import { resolveComputerBin, ComputerClientError } from '../lib/computer-client.js';
16
17
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
18
+ import { installSetupTool } from '../lib/setup-tool-install.js';
19
+ import { refreshToolSetup } from '../lib/setup-tool-status.js';
17
20
  const ACCESSIBILITY_PANE = 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility';
18
21
  const SCREEN_PANE = 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture';
19
22
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -43,9 +46,7 @@ export async function runComputerWizard() {
43
46
  catch (error) {
44
47
  if (!(error instanceof ComputerClientError) || error.code !== 'COMPUTER_BIN_MISSING')
45
48
  throw error;
46
- console.log('Installing @phnx-labs/computer-cli@0.1.2…');
47
- const installed = spawnSync('npm', ['install', '-g', '@phnx-labs/computer-cli@0.1.2'], { stdio: 'inherit' });
48
- if (installed.status !== 0)
49
+ if (!(await installSetupTool('computer')))
49
50
  return false;
50
51
  try {
51
52
  resolveComputerBin();
@@ -136,9 +137,22 @@ export function registerSetupComputerCommand(setupCmd) {
136
137
  setupCmd
137
138
  .command('computer')
138
139
  .description('Set up `agents computer` (macOS) — install the signed helper and grant control permissions.')
139
- .action(async () => {
140
+ .option('--install-only', 'Install the standalone Computer CLI without starting the helper or changing permissions')
141
+ .option('--terminal [backend]', 'Open interactive setup in a detected or selected terminal')
142
+ .action(async (options) => {
140
143
  try {
141
- await runComputerWizard();
144
+ if (options.terminal !== undefined) {
145
+ await openSetupTerminal('computer', options.terminal, options.installOnly);
146
+ return;
147
+ }
148
+ if (options.installOnly) {
149
+ if (!(await installSetupTool('computer')))
150
+ process.exitCode = 1;
151
+ return;
152
+ }
153
+ if (!(await runComputerWizard()))
154
+ process.exitCode = 1;
155
+ await refreshToolSetup('computer');
142
156
  }
143
157
  catch (err) {
144
158
  if (isPromptCancelled(err)) {
@@ -8,8 +8,8 @@
8
8
  * `npm i -g`. Users do not set extra env vars.
9
9
  */
10
10
  import type { Command } from 'commander';
11
- export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.4";
12
- export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.4";
11
+ export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.5";
12
+ export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.5";
13
13
  export declare function setupSecretsPrefsPath(): string;
14
14
  /** True when the standalone `secrets` executable resolves ($SECRETS_BIN or PATH). */
15
15
  export declare function isSecretsCliInstalled(): boolean;
@@ -7,6 +7,7 @@
7
7
  * `agents clis install secrets` (system `clis/secrets.yaml`) then a pinned
8
8
  * `npm i -g`. Users do not set extra env vars.
9
9
  */
10
+ import { openSetupTerminal } from './setup-terminal.js';
10
11
  import chalk from 'chalk';
11
12
  import * as fs from 'fs';
12
13
  import * as path from 'path';
@@ -14,7 +15,12 @@ import { spawnSync } from 'node:child_process';
14
15
  import { getHistoryDir } from '../lib/state.js';
15
16
  import { resolveSecretsBin, invocation, SecretsClientError, _resetSecretsClientForTest } from '../lib/secrets-client.js';
16
17
  import { installCli, resolveCliManifest } from '../lib/cli-resources.js';
17
- export const SECRETS_CLI_PACKAGE = '@phnx-labs/secrets-cli@0.1.4';
18
+ import { SECRETS_CLI_SPEC } from '../lib/secrets-cli.js';
19
+ import { refreshToolSetup } from '../lib/setup-tool-status.js';
20
+ import { execFileShellSpec } from '../lib/platform/exec.js';
21
+ // Re-exported for back-compat; the canonical pin lives in `secrets-cli.ts` so a
22
+ // version bump touches ONE place (PHNX-3989 consolidation).
23
+ export const SECRETS_CLI_PACKAGE = SECRETS_CLI_SPEC;
18
24
  export const INSTALL_HINT = `agents clis install secrets # or: npm i -g ${SECRETS_CLI_PACKAGE}`;
19
25
  export function setupSecretsPrefsPath() {
20
26
  return path.join(getHistoryDir(), 'setup', 'secrets.json');
@@ -56,7 +62,8 @@ export function installSecretsCli() {
56
62
  console.error(chalk.gray(result.error));
57
63
  }
58
64
  console.log(chalk.gray(`Installing ${SECRETS_CLI_PACKAGE}…`));
59
- const r = spawnSync('npm', ['install', '-g', SECRETS_CLI_PACKAGE], { stdio: 'inherit' });
65
+ const npm = execFileShellSpec('npm', ['install', '-g', SECRETS_CLI_PACKAGE]);
66
+ const r = spawnSync(npm.command, npm.args, { stdio: 'inherit', shell: npm.shell });
60
67
  _resetSecretsClientForTest();
61
68
  if (r.error) {
62
69
  console.error(chalk.red(`npm install failed: ${r.error.message}`));
@@ -93,8 +100,21 @@ export function registerSetupSecretsCommand(setupCmd) {
93
100
  setupCmd
94
101
  .command('secrets')
95
102
  .description('Install the standalone `secrets` CLI if missing, then run its `secrets migrate` onboarding.')
96
- .action(async () => {
97
- if (!(await runSecretsSetupWizard()))
103
+ .option('--install-only', 'Install the standalone Secrets CLI without migrating or unlocking secrets')
104
+ .option('--terminal [backend]', 'Open interactive setup in a detected or selected terminal')
105
+ .action(async (options) => {
106
+ if (options.terminal !== undefined) {
107
+ try {
108
+ await openSetupTerminal('secrets', options.terminal, options.installOnly);
109
+ }
110
+ catch (error) {
111
+ console.error(error instanceof Error ? error.message : String(error));
112
+ process.exitCode = 1;
113
+ }
114
+ return;
115
+ }
116
+ if (!(options.installOnly ? installSecretsCli() : await runSecretsSetupWizard()))
98
117
  process.exitCode = 1;
118
+ await refreshToolSetup('secrets');
99
119
  });
100
120
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `agents setup term` — install the standalone `term` CLI if missing (PHNX-4092).
3
+ *
4
+ * The PTY engine lives in `@phnx-labs/term-cli` (extracted PHNX-4091);
5
+ * agents-cli never rebundles it. The OAuth device-code driver (`agents fleet
6
+ * login`, `agents auth mint`) spawns `term` on demand and fails loud when it is
7
+ * absent, so onboarding installs it here like every other standalone tool.
8
+ *
9
+ * Unlike browser/computer/secrets there is nothing to configure — no profile,
10
+ * no OS permission, no migrate step. A missing binary is a routine install
11
+ * (`agents clis install term` via the system `clis/term.yaml`, then a pinned
12
+ * `npm i -g`, both handled by installSetupTool), and once it is on PATH the
13
+ * tool is ready.
14
+ */
15
+ import type { Command } from 'commander';
16
+ /** True when the standalone `term` executable resolves ($TERM_BIN or PATH). */
17
+ export declare function isTermCliInstalled(): boolean;
18
+ /**
19
+ * Install the standalone `term` CLI if missing. Returns whether `term` is on
20
+ * PATH afterwards. There is no further onboarding — presence is readiness.
21
+ */
22
+ export declare function runTermWizard(): Promise<boolean>;
23
+ /** Register `agents setup term` under the parent `setup` command. */
24
+ export declare function registerSetupTermCommand(setupCmd: Command): void;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `agents setup term` — install the standalone `term` CLI if missing (PHNX-4092).
3
+ *
4
+ * The PTY engine lives in `@phnx-labs/term-cli` (extracted PHNX-4091);
5
+ * agents-cli never rebundles it. The OAuth device-code driver (`agents fleet
6
+ * login`, `agents auth mint`) spawns `term` on demand and fails loud when it is
7
+ * absent, so onboarding installs it here like every other standalone tool.
8
+ *
9
+ * Unlike browser/computer/secrets there is nothing to configure — no profile,
10
+ * no OS permission, no migrate step. A missing binary is a routine install
11
+ * (`agents clis install term` via the system `clis/term.yaml`, then a pinned
12
+ * `npm i -g`, both handled by installSetupTool), and once it is on PATH the
13
+ * tool is ready.
14
+ */
15
+ import chalk from 'chalk';
16
+ import { isPromptCancelled } from './utils.js';
17
+ import { openSetupTerminal } from './setup-terminal.js';
18
+ import { resolveTermBin } from '../lib/term-client.js';
19
+ import { installSetupTool } from '../lib/setup-tool-install.js';
20
+ import { refreshToolSetup } from '../lib/setup-tool-status.js';
21
+ const INSTALL_HINT = 'agents clis install term # or: npm i -g @phnx-labs/term-cli';
22
+ /** True when the standalone `term` executable resolves ($TERM_BIN or PATH). */
23
+ export function isTermCliInstalled() {
24
+ return resolveTermBin() !== null;
25
+ }
26
+ /**
27
+ * Install the standalone `term` CLI if missing. Returns whether `term` is on
28
+ * PATH afterwards. There is no further onboarding — presence is readiness.
29
+ */
30
+ export async function runTermWizard() {
31
+ if (isTermCliInstalled()) {
32
+ console.log(chalk.green('The standalone `term` CLI is installed.'));
33
+ return true;
34
+ }
35
+ if (await installSetupTool('term')) {
36
+ console.log(chalk.green('Installed the standalone `term` CLI.'));
37
+ return true;
38
+ }
39
+ console.log(chalk.yellow('The standalone `term` CLI is not installed.'));
40
+ console.log(chalk.gray('Install it, then re-run `agents setup term`:'));
41
+ console.log(chalk.cyan(` ${INSTALL_HINT}`));
42
+ return false;
43
+ }
44
+ /** Register `agents setup term` under the parent `setup` command. */
45
+ export function registerSetupTermCommand(setupCmd) {
46
+ setupCmd
47
+ .command('term')
48
+ .description('Install the standalone `term` CLI (the PTY engine fleet login and auth mint spawn) if missing.')
49
+ .option('--install-only', 'Install the standalone term CLI (identical to the wizard; term needs no further setup)')
50
+ .option('--terminal [backend]', 'Open interactive setup in a detected or selected terminal')
51
+ .action(async (options) => {
52
+ try {
53
+ if (options.terminal !== undefined) {
54
+ await openSetupTerminal('term', options.terminal, options.installOnly);
55
+ return;
56
+ }
57
+ if (!(await runTermWizard()))
58
+ process.exitCode = 1;
59
+ await refreshToolSetup('term');
60
+ }
61
+ catch (err) {
62
+ if (isPromptCancelled(err)) {
63
+ console.log(chalk.yellow('\nCancelled'));
64
+ return;
65
+ }
66
+ console.error(chalk.red(err.message));
67
+ process.exitCode = 1;
68
+ }
69
+ });
70
+ }
@@ -0,0 +1,3 @@
1
+ import type { SetupTool } from '../lib/setup-tool-status.js';
2
+ /** The GUI starts a terminal; the terminal child owns wizard completion. */
3
+ export declare function openSetupTerminal(tool: SetupTool, terminal: boolean | string, installOnly?: boolean): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import os from 'node:os';
2
+ import { currentContext, openRunInTerminal, parseTerminalFlag } from '../lib/terminal/index.js';
3
+ /** The GUI starts a terminal; the terminal child owns wizard completion. */
4
+ export async function openSetupTerminal(tool, terminal, installOnly = false) {
5
+ if (installOnly)
6
+ throw new Error('--terminal and --install-only are separate setup modes.');
7
+ const parsed = parseTerminalFlag(terminal);
8
+ if (parsed.error)
9
+ throw new Error(parsed.error);
10
+ const { readActiveSessionsCache } = await import('../lib/session/session-cache.js');
11
+ const sessions = parsed.backend ? [] : (readActiveSessionsCache('local')?.sessions ?? []).map(({ host, lastActivityMs, startedAtMs }) => ({ host, lastActivityMs, startedAtMs }));
12
+ const result = await openRunInTerminal({
13
+ argv: ['setup', tool],
14
+ forced: parsed.backend,
15
+ cwd: os.homedir(),
16
+ sessions,
17
+ ctx: currentContext(),
18
+ });
19
+ if (!result.ok)
20
+ throw new Error(result.error ?? 'Could not open a terminal for setup.');
21
+ console.log(result.description ?? 'Setup opened in a terminal.');
22
+ }
@@ -31,7 +31,7 @@ export declare function ensureInitialized(program: Command): Promise<void>;
31
31
  * wizard. Never throws — a cancel or an optional wizard's error just skips the
32
32
  * rest and lets core setup complete.
33
33
  */
34
- type SetupPhase = 'browser' | 'computer' | 'share' | 'secrets' | 'accounts' | 'fleet' | 'watchdog' | 'preferences';
34
+ type SetupPhase = 'browser' | 'computer' | 'secrets' | 'term' | 'accounts' | 'fleet' | 'watchdog' | 'preferences';
35
35
  type SetupStatusState = 'ready' | 'missing' | 'n/a';
36
36
  interface SetupStatusRow {
37
37
  phase: 'core' | SetupPhase;