@phnx-labs/agents-cli 1.22.114 → 1.22.115

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 (176) hide show
  1. package/CHANGELOG.md +110 -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 +199 -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 +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
@@ -0,0 +1,198 @@
1
+ import type { BrowserSessionRow, ArtifactKind } from '../browser/sessions-list.js';
2
+ import type { ComputerRunRow } from '../computer/sessions-list.js';
3
+ /** Newest captures retained per row. A task with hundreds of screenshots must
4
+ * not turn one stream envelope into a megabyte. */
5
+ export declare const TOOL_CAPTURE_LIMIT = 20;
6
+ /** Newest actions retained per computer row, for the same reason. */
7
+ export declare const TOOL_ACTION_LIMIT = 50;
8
+ export type ToolKind = 'browser' | 'computer';
9
+ /** Why a row carries no owning session: `linked` resolved one, `unresolved` has
10
+ * an id that no indexed session matches, `unlinked` never had one. Mirrors the
11
+ * status both source modules already publish. */
12
+ export type ToolLinkStatus = 'linked' | 'unresolved' | 'unlinked';
13
+ /** One artifact a tool produced. Path only — never contents. */
14
+ export interface ToolCapture {
15
+ kind: ArtifactKind;
16
+ name: string;
17
+ /**
18
+ * Absolute path ON {@link ToolCapture.host}. A capture lives on the machine
19
+ * whose browser produced it, so a fleet reader on another box must not treat
20
+ * this as a local path — it has to open it through that host. Carrying the host
21
+ * per capture is what makes the path meaningful off-box at all.
22
+ */
23
+ path: string;
24
+ /** The device that holds this file. */
25
+ host: string;
26
+ bytes?: number;
27
+ atMs: number;
28
+ }
29
+ /**
30
+ * One tab a browser task has open, with the id the task itself addresses it by.
31
+ *
32
+ * `id` is the task's SHORT tab id (the key of `Task.tabs`), not the underlying
33
+ * CDP target id: the short id is what `agents browser tab focus <id>` accepts
34
+ * and what stays stable for the life of the tab, while the target id is an
35
+ * engine-internal handle that a reconnect can change.
36
+ */
37
+ export interface ToolTab {
38
+ id: string;
39
+ url?: string;
40
+ title?: string;
41
+ /** The tab URL-less verbs act on. */
42
+ current?: boolean;
43
+ /**
44
+ * The task drives this tab but did not open it, so no close path touches it.
45
+ * Surfaced so a UI does not offer to close someone else's tab.
46
+ */
47
+ borrowed?: boolean;
48
+ }
49
+ /**
50
+ * An argv a consumer may run verbatim, plus the device it must run ON.
51
+ *
52
+ * `runOn` is load-bearing and is NOT a `--device` flag. A browser task is bound
53
+ * to its device at `start`, and every later verb resolves that binding from the
54
+ * local task index — `agents browser done --task x --device y` is explicitly
55
+ * REJECTED (`browser/task-index.ts` `REJECT_DEVICE_MESSAGE`). So the way to act
56
+ * on a task observed from another box is to run the plain argv on the host that
57
+ * holds the binding, which is what this names.
58
+ */
59
+ export interface ToolCommand {
60
+ command: 'agents';
61
+ args: string[];
62
+ runOn: string;
63
+ }
64
+ /** The agent session that drove the tool, as a click-through target. */
65
+ export interface ToolOwner {
66
+ sessionId: string;
67
+ device: string;
68
+ label?: string;
69
+ agent?: string;
70
+ }
71
+ interface ToolRowBase {
72
+ kind: ToolKind;
73
+ /** Opaque, stable identity for this row within one device scope. */
74
+ rowKey: string;
75
+ /** The observing device that reported the row. */
76
+ scope: string;
77
+ /** The DRIVEN machine — for `--device` runs this differs from `scope`. */
78
+ device: string;
79
+ /** Is this a resource that still exists and can be acted on? */
80
+ live: boolean;
81
+ task?: string;
82
+ sessionId?: string;
83
+ launchId?: string;
84
+ agent?: string;
85
+ owner?: ToolOwner;
86
+ linkStatus: ToolLinkStatus;
87
+ startedAtMs: number;
88
+ updatedAtMs: number;
89
+ /** Newest first, bounded by {@link TOOL_CAPTURE_LIMIT}. */
90
+ captures: ToolCapture[];
91
+ captureCounts: Record<string, number>;
92
+ }
93
+ export interface BrowserToolRow extends ToolRowBase {
94
+ kind: 'browser';
95
+ profile: string;
96
+ /** Redacted by {@link redactToolUrl}; absent when the binding recorded none. */
97
+ url?: string;
98
+ /**
99
+ * Tabs the task has open, newest-known first. Absent — not empty — when this
100
+ * host holds no live task record to read them from (a task bound to another
101
+ * device, or one whose run already ended leaving only captures). Absent means
102
+ * "unknown here", `[]` means "genuinely none".
103
+ */
104
+ tabs?: ToolTab[];
105
+ /**
106
+ * Focus one of this task's tabs. Present only while `live` and only for a tab
107
+ * the task actually owns.
108
+ */
109
+ showCommand?: ToolCommand;
110
+ /**
111
+ * The command that closes this task. Present ONLY while `live` — a task the
112
+ * index no longer binds has nothing left to close, and offering the command
113
+ * anyway would fail loud at the operator instead of at this boundary.
114
+ */
115
+ closeCommand?: ToolCommand;
116
+ }
117
+ export interface ComputerToolRow extends ToolRowBase {
118
+ kind: 'computer';
119
+ /** Always false: see the module docblock. A run is history, not a session. */
120
+ live: false;
121
+ bundle?: string;
122
+ /** Newest first, bounded by {@link TOOL_ACTION_LIMIT}. */
123
+ actions: {
124
+ verb: string;
125
+ atMs: number;
126
+ host?: string;
127
+ bundle?: string;
128
+ }[];
129
+ actionCounts: Record<string, number>;
130
+ /** Total actions for a row whose per-verb detail the ledger already pruned. */
131
+ recoveredActionCount?: number;
132
+ }
133
+ export type ToolRow = BrowserToolRow | ComputerToolRow;
134
+ /**
135
+ * A task the local browser holds LIVE state for, read from its `tasks.json`.
136
+ *
137
+ * This is the only authority on a task's tabs: `tasks.json` is rewritten from
138
+ * the live task map, so an entry here means the task exists right now, and its
139
+ * `tabs` map is what `agents browser tab focus` addresses. A task whose run has
140
+ * ended is absent even though its captures remain, which is exactly the
141
+ * distinction `live` on a row reports.
142
+ */
143
+ export interface LiveBrowserTask {
144
+ task: string;
145
+ profile?: string;
146
+ label?: string;
147
+ tabs?: ToolTab[];
148
+ /** `Task.createdAt` — when the task was opened. */
149
+ startedAtMs?: number;
150
+ /** `Task.lastActionAt` — refreshed by every task-scoped action. */
151
+ lastActionAtMs?: number;
152
+ sessionId?: string;
153
+ launchId?: string;
154
+ /** `Task.actor` — who launched it, when the record carries one. */
155
+ actor?: string;
156
+ }
157
+ /** Stable identity for one tool row within one device scope. */
158
+ export declare function toolRowKey(scope: string, kind: ToolKind, identity: string): string;
159
+ /**
160
+ * A task URL safe to publish: userinfo removed and credential-shaped query
161
+ * parameters replaced. A string that does not parse as a URL is dropped
162
+ * entirely rather than published unexamined — it cannot be redacted safely.
163
+ */
164
+ export declare function redactToolUrl(raw: string | undefined): string | undefined;
165
+ /**
166
+ * A browser task the task index binds but that has produced no capture yet.
167
+ *
168
+ * `buildBrowserSessionRows` derives its rows from captures on disk, so a task
169
+ * that was just opened — bound, live, drivable, closable — had no row at all
170
+ * until its first screenshot landed. That is the window an operator most wants
171
+ * to see, and it is exactly when a Sessions pane showed nothing. This
172
+ * synthesizes the zero-capture row so it goes through the SAME projection rather
173
+ * than a second parallel one.
174
+ */
175
+ export declare function boundBrowserRow(task: string, binding: {
176
+ profile?: string;
177
+ }): BrowserSessionRow;
178
+ /**
179
+ * One browser task (or a profile's downloads bucket) as a tool row. Pure: the
180
+ * liveness verdict is passed in, because only the machine running the browser
181
+ * daemon holds the task index that answers it.
182
+ */
183
+ export declare function projectBrowserToolRow(scope: string, row: BrowserSessionRow, binding?: {
184
+ device?: string;
185
+ profile?: string;
186
+ url?: string;
187
+ createdAt?: number;
188
+ sessionId?: string;
189
+ launchId?: string;
190
+ }, live?: LiveBrowserTask): BrowserToolRow;
191
+ /**
192
+ * One computer run as a tool row. `live` is pinned false and no close/stop
193
+ * command is produced: the emitting CLI process is already gone.
194
+ */
195
+ export declare function projectComputerToolRow(scope: string, row: ComputerRunRow): ComputerToolRow;
196
+ /** Newest first across both kinds — the order a consumer renders. */
197
+ export declare function sortToolRows(rows: ToolRow[]): ToolRow[];
198
+ export {};
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Canonical tool activity on the feed stream — browser tasks and computer runs
3
+ * projected into one row contract, alongside the agent and attention rows.
4
+ *
5
+ * WHY A PROJECTION AND NOT A NEW STORE. Both sources already exist and already
6
+ * have an owner: `agents browser sessions` groups a profile's captures into
7
+ * task-first rows (`browser/sessions-list.ts`), and `agents computer sessions`
8
+ * groups `computer.action` ledger entries into run rows
9
+ * (`computer/sessions-list.ts`). A menu-bar or Fleet view that wanted the same
10
+ * information had only one way to get it: shell out to those two commands, per
11
+ * tool, per device, on a timer. That is the per-tool polling this module
12
+ * removes — the rows ride the ONE stream `agents feed watch --json` already
13
+ * holds open, so a consumer switching its All/Agents/Browser/Computer filter
14
+ * spawns zero commands.
15
+ *
16
+ * WHAT IS DELIBERATELY DIFFERENT BETWEEN THE TWO KINDS. A browser task is a
17
+ * LIVE resource: it is bound in the task index while it exists, it can be
18
+ * driven, and it can be closed (`agents browser done --task <name>`). A
19
+ * computer run is a LEDGER ENTRY: the actions already happened, the CLI process
20
+ * that performed them has exited, and there is nothing to stop. So
21
+ * {@link ComputerToolRow} pins `live: false` and carries no close/stop command
22
+ * at all — inventing one would offer an operator a control that cannot work.
23
+ *
24
+ * PRIVACY. Rows carry capture PATHS and names, never contents, and a task URL
25
+ * is redacted ({@link redactToolUrl}) before it leaves this process: userinfo
26
+ * is stripped and credential-shaped query parameters are replaced. The feed is
27
+ * read by the menu bar, the extension, and any `--json` consumer; a bearer
28
+ * token in a `?access_token=` is exactly the value that must not ride it.
29
+ */
30
+ import { createHash } from 'node:crypto';
31
+ import { normalizeHost } from '../machine-id.js';
32
+ import { sessionHeadline } from '../session/title.js';
33
+ /** Newest captures retained per row. A task with hundreds of screenshots must
34
+ * not turn one stream envelope into a megabyte. */
35
+ export const TOOL_CAPTURE_LIMIT = 20;
36
+ /** Newest actions retained per computer row, for the same reason. */
37
+ export const TOOL_ACTION_LIMIT = 50;
38
+ /** Stable identity for one tool row within one device scope. */
39
+ export function toolRowKey(scope, kind, identity) {
40
+ return createHash('sha256').update(`${scope}\0${kind}\0${identity}`).digest('base64url').slice(0, 22);
41
+ }
42
+ /** Query parameters whose value is a credential, not a locator. */
43
+ const CREDENTIAL_PARAM = /(token|secret|password|passwd|pwd|api[-_]?key|auth|session|signature|sig|code)/i;
44
+ const REDACTED = '<redacted>';
45
+ /**
46
+ * A task URL safe to publish: userinfo removed and credential-shaped query
47
+ * parameters replaced. A string that does not parse as a URL is dropped
48
+ * entirely rather than published unexamined — it cannot be redacted safely.
49
+ */
50
+ export function redactToolUrl(raw) {
51
+ if (!raw)
52
+ return undefined;
53
+ let url;
54
+ try {
55
+ url = new URL(raw);
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ url.username = '';
61
+ url.password = '';
62
+ for (const key of [...url.searchParams.keys()]) {
63
+ if (CREDENTIAL_PARAM.test(key))
64
+ url.searchParams.set(key, REDACTED);
65
+ }
66
+ // A fragment is where OAuth implicit flows put the token, and nothing on a
67
+ // status row needs it.
68
+ if (url.hash)
69
+ url.hash = '';
70
+ return url.toString();
71
+ }
72
+ /**
73
+ * The owning agent session, or nothing.
74
+ *
75
+ * `sessionId` is passed in as the EFFECTIVE identity rather than read off the
76
+ * capture row: a task's session is recorded in the live task record and in the
77
+ * device binding as well as in the durable capture history, and a row assembled
78
+ * from only one of those had an owner on some paths and not others. Resolving the
79
+ * effective id first is what makes the owner link consistent for one task however
80
+ * its row was assembled.
81
+ *
82
+ * Returns undefined when no id is known. `recordBrowserSession`/`emit` are
83
+ * pre-existing no-ops for some paths, so closed history genuinely lacks an owner
84
+ * sometimes — and an invented link would be worse than an absent one.
85
+ */
86
+ function ownerOf(sessionId, linkedSession, scope) {
87
+ if (!sessionId)
88
+ return undefined;
89
+ const session = linkedSession ?? undefined;
90
+ // One headline ladder for the whole CLI (SES-14c): a row's own `label`, then the
91
+ // daemon-generated title, then the first-prompt topic. Re-deriving it here would
92
+ // silently drop the generated-title rung and show a different name for the same
93
+ // session than `agents sessions` does.
94
+ const label = session ? sessionHeadline(session) : undefined;
95
+ return {
96
+ sessionId,
97
+ device: normalizeHost(session?.machine ?? scope),
98
+ ...(label ? { label } : {}),
99
+ ...(session?.agent ? { agent: session.agent } : {}),
100
+ };
101
+ }
102
+ /** A machine name, or undefined for the `unknown` not-identified sentinel. */
103
+ function knownMachine(machine) {
104
+ return machine && machine !== 'unknown' ? machine : undefined;
105
+ }
106
+ function countBy(items, key) {
107
+ const counts = {};
108
+ for (const item of items)
109
+ counts[key(item)] = (counts[key(item)] ?? 0) + 1;
110
+ return counts;
111
+ }
112
+ /**
113
+ * A browser task the task index binds but that has produced no capture yet.
114
+ *
115
+ * `buildBrowserSessionRows` derives its rows from captures on disk, so a task
116
+ * that was just opened — bound, live, drivable, closable — had no row at all
117
+ * until its first screenshot landed. That is the window an operator most wants
118
+ * to see, and it is exactly when a Sessions pane showed nothing. This
119
+ * synthesizes the zero-capture row so it goes through the SAME projection rather
120
+ * than a second parallel one.
121
+ */
122
+ export function boundBrowserRow(task, binding) {
123
+ return {
124
+ kind: 'task', task, profile: binding.profile ?? '',
125
+ linkStatus: 'unlinked', artifacts: [],
126
+ counts: { screenshot: 0, pdf: 0, recording: 0, download: 0 },
127
+ latestMtimeMs: 0,
128
+ };
129
+ }
130
+ /**
131
+ * One browser task (or a profile's downloads bucket) as a tool row. Pure: the
132
+ * liveness verdict is passed in, because only the machine running the browser
133
+ * daemon holds the task index that answers it.
134
+ */
135
+ export function projectBrowserToolRow(scope, row, binding, live) {
136
+ const host = normalizeHost(scope);
137
+ // Live means the task still EXISTS: either this host's browser holds a live
138
+ // record for it, or the task index still routes it (which is the case for a
139
+ // task whose browser runs on another device).
140
+ const isLive = Boolean(row.task) && (live !== undefined || binding !== undefined);
141
+ const captures = row.artifacts.slice(0, TOOL_CAPTURE_LIMIT).map((artifact) => ({
142
+ kind: artifact.kind, name: artifact.name, path: artifact.path, host, bytes: artifact.bytes, atMs: artifact.mtimeMs,
143
+ }));
144
+ const oldest = row.artifacts.length > 0 ? row.artifacts[row.artifacts.length - 1].mtimeMs : row.latestMtimeMs;
145
+ const url = redactToolUrl(binding?.url);
146
+ // One effective identity for the row AND its owner link, resolved before either
147
+ // is built: durable capture history first (it survives the task), then the live
148
+ // record, then the device binding.
149
+ const sessionId = row.sessionId ?? live?.sessionId ?? binding?.sessionId;
150
+ const launchId = row.launchId ?? live?.launchId ?? binding?.launchId;
151
+ const owner = ownerOf(sessionId, row.linkedSession, host);
152
+ // Tabs are only knowable from a LIVE task record on THIS host; a row projected
153
+ // from captures alone, or from a binding pointing at another device, genuinely
154
+ // does not know them and says so by omitting the field.
155
+ const tabs = live?.tabs;
156
+ const showTab = tabs?.find((tab) => tab.current && !tab.borrowed)?.id
157
+ ?? tabs?.find((tab) => !tab.borrowed)?.id;
158
+ const startedAtMs = live?.startedAtMs ?? binding?.createdAt ?? oldest;
159
+ // A task with no captures has no capture mtime to sort by. `lastActionAt` is
160
+ // refreshed by every task-scoped action, so it is the honest freshness key;
161
+ // its own start is the fallback. Reporting 0 would sort a brand-new live task
162
+ // to the very bottom of a newest-first list.
163
+ const updatedAtMs = Math.max(row.latestMtimeMs, live?.lastActionAtMs ?? 0) || startedAtMs;
164
+ return {
165
+ kind: 'browser',
166
+ // Keyed on the TASK, never on the profile. A task with no captures yet is
167
+ // discovered from the task index, where the profile may not be recorded, so
168
+ // folding the profile into the identity would give the same task two
169
+ // different keys and render it twice the moment its first capture landed.
170
+ // Task names are unique within a machine's task index, which is the scope
171
+ // this key is already qualified by. The downloads bucket has no task, so it
172
+ // keys on its profile — one such row per profile, which is what it is.
173
+ rowKey: toolRowKey(host, 'browser', row.task ? `task\0${row.task}` : `downloads\0${row.profile}`),
174
+ scope: host,
175
+ device: normalizeHost(binding?.device ?? host),
176
+ live: isLive,
177
+ ...(row.task ? { task: row.task } : {}),
178
+ ...(sessionId ? { sessionId } : {}),
179
+ ...(launchId ? { launchId } : {}),
180
+ ...(row.linkedSession?.agent ? { agent: row.linkedSession.agent } : {}),
181
+ ...(owner ? { owner } : {}),
182
+ linkStatus: row.linkedSession ? 'linked' : sessionId ? 'unresolved' : 'unlinked',
183
+ startedAtMs,
184
+ updatedAtMs,
185
+ captures,
186
+ captureCounts: countBy(row.artifacts, (artifact) => artifact.kind),
187
+ profile: row.profile || live?.profile || '',
188
+ ...(url ? { url } : {}),
189
+ ...(tabs ? { tabs } : {}),
190
+ // `runOn` is the OBSERVING host, not `device`: the binding that resolves the
191
+ // task's device lives here, and passing `--device` to a later verb is
192
+ // refused outright. See {@link ToolCommand}.
193
+ ...(isLive && showTab ? { showCommand: { command: 'agents', args: ['browser', 'tab', 'focus', showTab, '--task', row.task], runOn: host } } : {}),
194
+ ...(isLive ? { closeCommand: { command: 'agents', args: ['browser', 'done', '--task', row.task], runOn: host } } : {}),
195
+ };
196
+ }
197
+ /**
198
+ * One computer run as a tool row. `live` is pinned false and no close/stop
199
+ * command is produced: the emitting CLI process is already gone.
200
+ */
201
+ export function projectComputerToolRow(scope, row) {
202
+ const host = normalizeHost(scope);
203
+ const owner = ownerOf(row.sessionId, row.linkedSession, host);
204
+ const agent = row.agent ?? row.linkedSession?.agent;
205
+ // Captures come ONLY from a `capture` the producer recorded after the file was
206
+ // written. A screenshot action from before the producer carried that field has
207
+ // no path to report, and this deliberately reports none rather than guessing one
208
+ // from an output flag — a fabricated path is worse than an honest absence.
209
+ //
210
+ // `host` is the INVOKING machine, never `remoteHost`. The engine's helper RPC
211
+ // returns the image as base64 and the invoking process resolves `--out` and
212
+ // writes the file locally, so even a remote-desktop run leaves its screenshot
213
+ // on the box that ran the command. Naming the driven host would send a consumer
214
+ // looking for the file on a machine it was never written to.
215
+ const captures = [];
216
+ const captureCounts = {};
217
+ for (const action of row.actions) {
218
+ if (!action.capture)
219
+ continue;
220
+ captureCounts[action.capture.kind] = (captureCounts[action.capture.kind] ?? 0) + 1;
221
+ if (captures.length >= TOOL_CAPTURE_LIMIT)
222
+ continue;
223
+ captures.push({
224
+ kind: action.capture.kind, name: action.capture.name, path: action.capture.path,
225
+ host,
226
+ ...(action.capture.bytes !== undefined ? { bytes: action.capture.bytes } : {}),
227
+ atMs: action.tsMs,
228
+ });
229
+ }
230
+ const actions = row.actions.slice(0, TOOL_ACTION_LIMIT).map((action) => ({
231
+ verb: action.verb, atMs: action.tsMs,
232
+ ...(action.host ? { host: action.host } : {}),
233
+ ...(action.bundle ? { bundle: action.bundle } : {}),
234
+ }));
235
+ return {
236
+ kind: 'computer',
237
+ rowKey: toolRowKey(host, 'computer', row.invocationId ?? `${row.machine}\0${row.pid ?? ''}\0${row.startMs}`),
238
+ scope: host,
239
+ // `groupIntoComputerRuns` writes the literal 'unknown' when no record in the
240
+ // run identified a machine, which is a sentinel for "not identified" and not a
241
+ // device name. Publishing it made a locally-run action unaddressable and made
242
+ // it disappear under a device filter, so it resolves to the observing host —
243
+ // which is the machine whose ledger the run was read from.
244
+ device: normalizeHost(row.remoteHost ?? knownMachine(row.machine) ?? host),
245
+ live: false,
246
+ ...(row.task ? { task: row.task } : {}),
247
+ ...(row.sessionId ? { sessionId: row.sessionId } : {}),
248
+ ...(row.launchId ? { launchId: row.launchId } : {}),
249
+ ...(agent ? { agent } : {}),
250
+ ...(owner ? { owner } : {}),
251
+ linkStatus: row.linkStatus,
252
+ startedAtMs: row.startMs,
253
+ updatedAtMs: row.endMs,
254
+ captures,
255
+ captureCounts,
256
+ ...(row.bundle ? { bundle: row.bundle } : {}),
257
+ actions,
258
+ actionCounts: { ...row.counts },
259
+ ...(row.recoveredActionCount !== undefined ? { recoveredActionCount: row.recoveredActionCount } : {}),
260
+ };
261
+ }
262
+ /** Newest first across both kinds — the order a consumer renders. */
263
+ export function sortToolRows(rows) {
264
+ return [...rows].sort((a, b) => b.updatedAtMs - a.updatedAtMs);
265
+ }
@@ -1,62 +1,29 @@
1
- import { type SessionWatchEnvelope, type SessionWatchRow, type SessionWatchScopeStatus, type WatchLocalOptions } from '../session/watch.js';
2
- import { type AttentionItem } from './attention.js';
3
- import { type ActivityEvent } from './activity.js';
1
+ import { type SessionWatchEnvelope, type WatchLocalOptions } from '../session/watch.js';
2
+ import { watchToolActivity } from './tool-activity.js';
3
+ import { type ToolRow } from './tools.js';
4
+ import { FeedWatchState, type FeedWatchEnvelope } from './envelope.js';
5
+ import { FeedHub } from './hub.js';
6
+ import { getCachedToolSetup, subscribeToolSetup, type ToolSetupRow } from '../setup-tool-status.js';
4
7
  import type { GhExec } from '../github/pr-mergeable.js';
5
- type Base = {
6
- v: 1;
7
- type: string;
8
- streamId: string;
9
- sequence: number;
10
- scope: string;
11
- };
12
- export type FeedWatchEnvelope = Base & {
13
- type: 'reset';
14
- capturedAt: number;
15
- agents: SessionWatchRow[];
16
- attention: AttentionItem[];
17
- } | Base & {
18
- type: 'agent.upsert';
19
- rowKey: string;
20
- agent: SessionWatchRow;
21
- } | Base & {
22
- type: 'agent.remove';
23
- rowKey: string;
24
- } | Base & {
25
- type: 'attention.upsert';
26
- rowKey: string;
27
- attention: AttentionItem;
28
- } | Base & {
29
- type: 'attention.remove';
30
- rowKey: string;
31
- } | Base & {
32
- type: 'activity.append';
33
- event: ActivityEvent;
34
- } | Base & {
35
- type: 'scope';
36
- capturedAt: number;
37
- status: SessionWatchScopeStatus;
38
- reason?: string;
39
- } | Base & {
40
- type: 'heartbeat';
41
- capturedAt: number;
42
- };
43
- type FeedWatchPayload = FeedWatchEnvelope extends infer Envelope ? Envelope extends FeedWatchEnvelope ? Omit<Envelope, 'v' | 'streamId' | 'sequence'> : never : never;
44
- export declare class FeedWatchState {
45
- readonly streamId: string;
46
- private sequence;
47
- constructor(streamId?: `${string}-${string}-${string}-${string}-${string}`);
48
- emit(event: FeedWatchPayload): FeedWatchEnvelope;
49
- }
8
+ export { FeedWatchState, type FeedWatchEnvelope, type FeedWatchPayload } from './envelope.js';
50
9
  /** Reuse session ownership reconciliation for the combined operator stream. */
51
10
  export declare class FeedSessionProjection {
52
11
  private readonly state;
53
12
  private readonly sessions;
54
13
  private readonly observations;
55
14
  private projectedAttention;
15
+ /**
16
+ * Tool rows per observing scope. A reset replaces one scope's rows and must
17
+ * not touch another's — the same per-scope rule the agent rows follow, and
18
+ * the reason a peer reconnecting cannot erase the local machine's tasks.
19
+ */
20
+ private readonly toolsByScope;
21
+ /** Whole-set per scope; see the `setup.snapshot` docblock in `envelope.ts`. */
22
+ private readonly setupByScope;
56
23
  constructor(state?: FeedWatchState);
57
24
  apply(event: FeedWatchEnvelope): FeedWatchEnvelope[];
58
25
  }
59
- export declare function projectSessionEnvelope(event: SessionWatchEnvelope, state: FeedWatchState, gh?: GhExec): Promise<FeedWatchEnvelope[]>;
26
+ export declare function projectSessionEnvelope(event: SessionWatchEnvelope, state: FeedWatchState, gh?: GhExec, tools?: ToolRow[], setup?: ToolSetupRow[]): Promise<FeedWatchEnvelope[]>;
60
27
  interface WatchLocalFeedOptions {
61
28
  scope: string;
62
29
  signal: AbortSignal;
@@ -69,11 +36,44 @@ interface WatchLocalFeedOptions {
69
36
  sessions?: Pick<WatchLocalOptions, 'readCache' | 'readPrevious' | 'journalPath' | 'journalPollMs' | 'heartbeatMs'>;
70
37
  /** The `gh` runner behind PR status; a test passes a recorded table. */
71
38
  gh?: GhExec;
39
+ /** Tool-activity inputs, forwarded verbatim to {@link watchToolActivity}. */
40
+ tools?: Pick<Parameters<typeof watchToolActivity>[0], 'sweepMs' | 'roots' | 'sources'>;
41
+ /**
42
+ * Tool-setup readers. The setup teammate owns DETECTION
43
+ * (`lib/setup-tool-status.ts`); this stream only publishes what its cache
44
+ * already holds, and never triggers a probe of its own.
45
+ */
46
+ setup?: {
47
+ read?: typeof getCachedToolSetup;
48
+ subscribe?: typeof subscribeToolSetup;
49
+ };
72
50
  }
73
51
  export declare function watchLocalFeed(options: WatchLocalFeedOptions): Promise<void>;
52
+ /**
53
+ * The one ingress for an envelope produced by ANOTHER agents-cli.
54
+ *
55
+ * `tools` was added to the `reset` payload within protocol v1, so a peer on an
56
+ * older CLI is a correct v1 producer that simply reports no tool rows — which is
57
+ * true of it, not a data defect to paper over. Normalizing HERE, at the single
58
+ * place a foreign envelope enters, is what keeps every consumer downstream able
59
+ * to treat the field as present instead of each re-checking it. A mixed-version
60
+ * fleet is the normal state during a rollout, so an absent field must not take
61
+ * the whole fan-out down.
62
+ */
63
+ export declare function normalizePeerEnvelope(event: FeedWatchEnvelope): FeedWatchEnvelope;
64
+ /** The shared local collector, constructed on first use. Exposed for tests. */
65
+ export declare function sharedLocalFeedHub(): FeedHub;
66
+ /** Reset the shared collector. Tests only — it must not leak across cases. */
67
+ export declare function resetSharedLocalFeedHub(): Promise<void>;
68
+ /**
69
+ * Subscribe to this machine's rows through the shared collector, until abort.
70
+ */
71
+ export declare function subscribeSharedLocalFeed(options: {
72
+ signal: AbortSignal;
73
+ emit: (event: FeedWatchEnvelope) => void;
74
+ }): Promise<void>;
74
75
  export declare function watchFleetFeed(options: {
75
76
  signal: AbortSignal;
76
77
  emit: (event: FeedWatchEnvelope) => void;
77
78
  reconnectMs?: number;
78
79
  }): Promise<void>;
79
- export {};