@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.
- package/CHANGELOG.md +110 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +199 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +2 -7
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.js +1 -8
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +20 -10
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +92 -233
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +1 -7
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.js +2 -2
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.js +21 -1
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/hub-server.d.ts +86 -0
- package/dist/lib/feed/hub-server.js +334 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/remote-cmd.d.ts +51 -1
- package/dist/lib/hosts/remote-cmd.js +125 -8
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +26 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +215 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- 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
|
+
}
|
package/dist/lib/feed/watch.d.ts
CHANGED
|
@@ -1,62 +1,29 @@
|
|
|
1
|
-
import { type SessionWatchEnvelope, type
|
|
2
|
-
import {
|
|
3
|
-
import { type
|
|
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
|
|
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 {};
|