@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
@@ -0,0 +1,46 @@
1
+ // Cloudflare API credential glue, shared by the commands that provision or read
2
+ // Cloudflare-backed endpoints (`agents traces`, `agents sessions backup`).
3
+ //
4
+ // This used to live in `lib/share/config.ts` alongside the embedded artifact
5
+ // share engine. The share engine moved out to `@phnx-labs/artifacts-cli`
6
+ // (PHNX-3992); `readCloudflareCreds` stayed because it is a generic Cloudflare
7
+ // util that `traces`/`sessions backup` borrow, unrelated to sharing.
8
+ import { bundleExistsSync as bundleExists, readAndResolveBundleEnvSync as readAndResolveBundleEnv, } from '../secrets-client.js';
9
+ export const DEFAULT_CF_BUNDLE = 'cloudflare';
10
+ /** Cloudflare API credentials for provisioning, read from `cloudflare` (or a
11
+ * user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
12
+ export function readCloudflareCreds(bundle = DEFAULT_CF_BUNDLE, override) {
13
+ // Explicit --token/--account bypass the bundle entirely (robust escape hatch).
14
+ if (override?.apiToken) {
15
+ return { apiToken: override.apiToken, accountId: override.accountId ?? '' };
16
+ }
17
+ // Check existence first: resolving a missing bundle through the process client
18
+ // surfaces an opaque transport code (e.g. LOCKED), never naming the bundle the
19
+ // user typed — so name it here.
20
+ if (!bundleExists(bundle)) {
21
+ throw new Error(`The '${bundle}' bundle does not exist. ` +
22
+ `Pass credentials directly with --token <t> [--account <id>], or create it: ` +
23
+ `agents secrets add ${bundle} CLOUDFLARE_API_TOKEN`);
24
+ }
25
+ const { env } = readAndResolveBundleEnv(bundle, {
26
+ caller: 'cloudflare',
27
+ // Setup is still a read; only `agents secrets unlock` may authenticate.
28
+ agentOnly: true,
29
+ });
30
+ const find = (re) => {
31
+ for (const [k, v] of Object.entries(env))
32
+ if (re.test(k) && v)
33
+ return v;
34
+ return '';
35
+ };
36
+ const apiToken = env.CLOUDFLARE_API_TOKEN || env.CF_API_TOKEN || find(/API[_-]?TOKEN|(?:^|_)TOKEN$/i);
37
+ const accountId = override?.accountId || env.CLOUDFLARE_ACCOUNT_ID || env.CF_ACCOUNT_ID || find(/ACCOUNT[_-]?ID/i);
38
+ if (!apiToken) {
39
+ const keys = Object.keys(env);
40
+ throw new Error(`No Cloudflare API token in the '${bundle}' bundle ` +
41
+ `(keys present: ${keys.length ? keys.join(', ') : 'none'}). ` +
42
+ `Pass it directly with --token <t> [--account <id>], or add it: ` +
43
+ `agents secrets add ${bundle} CLOUDFLARE_API_TOKEN`);
44
+ }
45
+ return { apiToken, accountId };
46
+ }
@@ -0,0 +1,35 @@
1
+ /** One compiled non-JS Worker module (e.g. WASM) uploaded alongside the script. */
2
+ export interface WorkerModule {
3
+ name: string;
4
+ contentType: string;
5
+ contents: Uint8Array<ArrayBuffer>;
6
+ }
7
+ /** A Worker script plus any extra modules to upload with it. */
8
+ export interface WorkerBundle {
9
+ script: string;
10
+ modules: WorkerModule[];
11
+ }
12
+ export interface CloudflareRequest {
13
+ apiToken: string;
14
+ method: string;
15
+ pathname: string;
16
+ body?: unknown;
17
+ form?: FormData;
18
+ }
19
+ export type CloudflareRequester = <T = unknown>(request: CloudflareRequest) => Promise<T>;
20
+ interface ProvisionOptions {
21
+ request?: CloudflareRequester;
22
+ }
23
+ /** Create the R2 bucket (idempotent). */
24
+ export declare function createBucket(apiToken: string, accountId: string, name: string, opts?: ProvisionOptions): Promise<void>;
25
+ /** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
26
+ export declare function deployWorker(apiToken: string, accountId: string, workerName: string, worker: string | WorkerBundle, bucketName: string, opts?: ProvisionOptions): Promise<void>;
27
+ /** Add/update a secret_text binding using Cloudflare's Workers Secrets API. */
28
+ export declare function putWorkerSecret(apiToken: string, accountId: string, workerName: string, name: string, text: string, opts?: ProvisionOptions): Promise<void>;
29
+ /** Enable the free `*.workers.dev` route for the script, and return the account subdomain. */
30
+ export declare function enableWorkersDev(apiToken: string, accountId: string, workerName: string, opts?: ProvisionOptions): Promise<string>;
31
+ /** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
32
+ export declare function findZoneId(apiToken: string, domain: string, opts?: ProvisionOptions): Promise<string | null>;
33
+ /** Map a custom hostname (e.g. `share.getrush.ai`) to the Worker via Workers Custom Domains. */
34
+ export declare function addCustomDomain(apiToken: string, accountId: string, workerName: string, zoneId: string, hostname: string, opts?: ProvisionOptions): Promise<void>;
35
+ export {};
@@ -0,0 +1,144 @@
1
+ // Generic Cloudflare orchestration — plain `fetch` against the CF REST API (the
2
+ // repo has no CF wrapper). Create an R2 bucket, upload a module Worker with an R2
3
+ // binding, set secrets via the Workers Secrets API, enable the free
4
+ // `*.workers.dev` subdomain, and map a custom domain when the token owns the zone.
5
+ //
6
+ // This was `lib/share/provision.ts`. The artifact share engine moved out to
7
+ // `@phnx-labs/artifacts-cli` (PHNX-3992), but `agents traces` provisions its own
8
+ // isolated Cloudflare-backed store with the SAME primitives — so the generic
9
+ // layer relocated here (share-specific lifecycle/collab/managed helpers left
10
+ // with the share engine). `traces/provision.ts` is now the only consumer.
11
+ const CF_API = 'https://api.cloudflare.com/client/v4';
12
+ async function cf(apiToken, method, pathname, body, form) {
13
+ const headers = { authorization: `Bearer ${apiToken}` };
14
+ let payload;
15
+ if (form) {
16
+ payload = form; // fetch sets multipart boundary
17
+ }
18
+ else if (body !== undefined) {
19
+ headers['content-type'] = 'application/json';
20
+ payload = JSON.stringify(body);
21
+ }
22
+ const res = await fetch(`${CF_API}${pathname}`, { method, headers, body: payload });
23
+ const json = (await res.json().catch(() => ({})));
24
+ if (!res.ok || json.success === false) {
25
+ const msg = (json.errors ?? []).map((e) => `${e.code ?? ''} ${e.message ?? ''}`.trim()).join('; ') ||
26
+ res.statusText;
27
+ throw new Error(`Cloudflare ${method} ${pathname} failed (${res.status}): ${msg}`);
28
+ }
29
+ return json.result;
30
+ }
31
+ const defaultCloudflareRequester = (request) => cf(request.apiToken, request.method, request.pathname, request.body, request.form);
32
+ /** True if the CF error looks like "the thing already exists" (idempotent create). */
33
+ function isAlreadyExists(e) {
34
+ return /already exists|duplicate|10004|10014/i.test(String(e));
35
+ }
36
+ /** Create the R2 bucket (idempotent). */
37
+ export async function createBucket(apiToken, accountId, name, opts = {}) {
38
+ const request = opts.request ?? defaultCloudflareRequester;
39
+ try {
40
+ await request({
41
+ apiToken,
42
+ method: 'POST',
43
+ pathname: `/accounts/${accountId}/r2/buckets`,
44
+ body: { name },
45
+ });
46
+ }
47
+ catch (e) {
48
+ if (!isAlreadyExists(e))
49
+ throw e;
50
+ }
51
+ }
52
+ /** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
53
+ export async function deployWorker(apiToken, accountId, workerName, worker, bucketName, opts = {}) {
54
+ const request = opts.request ?? defaultCloudflareRequester;
55
+ const metadata = {
56
+ main_module: 'worker.js',
57
+ compatibility_date: '2024-11-06',
58
+ bindings: [
59
+ { type: 'r2_bucket', name: 'BUCKET', bucket_name: bucketName },
60
+ ],
61
+ };
62
+ const form = new FormData();
63
+ form.set('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
64
+ const bundle = typeof worker === 'string' ? { script: worker, modules: [] } : worker;
65
+ form.set('worker.js', new Blob([bundle.script], { type: 'application/javascript+module' }), 'worker.js');
66
+ for (const module of bundle.modules) {
67
+ form.set(module.name, new Blob([module.contents], { type: module.contentType }), module.name);
68
+ }
69
+ await request({
70
+ apiToken,
71
+ method: 'PUT',
72
+ pathname: `/accounts/${accountId}/workers/scripts/${workerName}`,
73
+ form,
74
+ });
75
+ }
76
+ /** Add/update a secret_text binding using Cloudflare's Workers Secrets API. */
77
+ export async function putWorkerSecret(apiToken, accountId, workerName, name, text, opts = {}) {
78
+ const request = opts.request ?? defaultCloudflareRequester;
79
+ await request({
80
+ apiToken,
81
+ method: 'PUT',
82
+ pathname: `/accounts/${accountId}/workers/scripts/${workerName}/secrets`,
83
+ body: { name, text, type: 'secret_text' },
84
+ });
85
+ }
86
+ /** Enable the free `*.workers.dev` route for the script, and return the account subdomain. */
87
+ export async function enableWorkersDev(apiToken, accountId, workerName, opts = {}) {
88
+ const request = opts.request ?? defaultCloudflareRequester;
89
+ await request({
90
+ apiToken,
91
+ method: 'POST',
92
+ pathname: `/accounts/${accountId}/workers/scripts/${workerName}/subdomain`,
93
+ body: {
94
+ enabled: true,
95
+ previews_enabled: false,
96
+ },
97
+ });
98
+ const sub = await request({
99
+ apiToken,
100
+ method: 'GET',
101
+ pathname: `/accounts/${accountId}/workers/subdomain`,
102
+ });
103
+ if (!sub?.subdomain) {
104
+ throw new Error('No workers.dev subdomain on this account yet — register one at dash.cloudflare.com → Workers → Subdomain, then re-run.');
105
+ }
106
+ return sub.subdomain;
107
+ }
108
+ /** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
109
+ export async function findZoneId(apiToken, domain, opts = {}) {
110
+ const request = opts.request ?? defaultCloudflareRequester;
111
+ // Try the exact name, then the registrable parent (share.getrush.ai -> getrush.ai).
112
+ const candidates = [domain, domain.split('.').slice(-2).join('.')];
113
+ for (const name of candidates) {
114
+ const zones = await request({
115
+ apiToken,
116
+ method: 'GET',
117
+ pathname: `/zones?name=${encodeURIComponent(name)}`,
118
+ }).catch(() => []);
119
+ if (zones?.length)
120
+ return zones[0].id;
121
+ }
122
+ return null;
123
+ }
124
+ /** Map a custom hostname (e.g. `share.getrush.ai`) to the Worker via Workers Custom Domains. */
125
+ export async function addCustomDomain(apiToken, accountId, workerName, zoneId, hostname, opts = {}) {
126
+ const request = opts.request ?? defaultCloudflareRequester;
127
+ try {
128
+ await request({
129
+ apiToken,
130
+ method: 'PUT',
131
+ pathname: `/accounts/${accountId}/workers/domains`,
132
+ body: {
133
+ zone_id: zoneId,
134
+ hostname,
135
+ service: workerName,
136
+ environment: 'production',
137
+ },
138
+ });
139
+ }
140
+ catch (e) {
141
+ if (!isAlreadyExists(e))
142
+ throw e;
143
+ }
144
+ }
@@ -26,7 +26,61 @@ export interface ComputerAction {
26
26
  agent?: string;
27
27
  machineId?: string;
28
28
  hostname?: string;
29
+ /**
30
+ * The file a successful `screenshot` action actually wrote.
31
+ *
32
+ * Recorded by the standalone engine only AFTER the write succeeded, so its
33
+ * presence is proof the file existed — which is why nothing here ever derives
34
+ * a path from the verb. An action from before the producer carried this field
35
+ * has no capture, and that is reported honestly as none rather than guessed at
36
+ * from an output flag that may never have been written.
37
+ */
38
+ capture?: {
39
+ path: string;
40
+ kind: 'screenshot';
41
+ name: string;
42
+ bytes?: number;
43
+ };
29
44
  }
45
+ /**
46
+ * The standalone engine's OWN action ledger.
47
+ *
48
+ * `agents computer` is a thin consumer of the standalone `computer` engine
49
+ * (PHNX-4075), and that engine ALWAYS appends every action it performs to
50
+ * `<cache>/computer/actions/<day>.jsonl` — independently of whether agents-cli
51
+ * was in the call at all. So a `computer` command the operator ran directly
52
+ * appears ONLY here: it never reached `recordComputerAction`, so it is in
53
+ * neither the feed event ledger nor `computer_sessions`. Reading only those two
54
+ * meant the actions an operator actually performed were invisible to every
55
+ * agents-cli surface.
56
+ */
57
+ export declare function standaloneComputerActionsDir(): string;
58
+ /**
59
+ * Read the standalone ledger, newest day first, bounded by `limit` actions.
60
+ *
61
+ * Bounded by construction: days are read newest-first and reading stops as soon
62
+ * as the budget is met, so a box with months of history costs the same as one
63
+ * with a day of it.
64
+ */
65
+ export declare function listStandaloneComputerActions(opts?: {
66
+ limit?: number;
67
+ dir?: string;
68
+ observer?: string;
69
+ }): ComputerAction[];
70
+ /**
71
+ * Union the two ledgers, preferring the standalone record for any run present in
72
+ * both.
73
+ *
74
+ * Dedupe keys on `invocationId`, NOT on a timestamp or pid. When a command is
75
+ * forwarded through `agents computer`, BOTH stores receive it — and the
76
+ * forwarding rewrites `ts` and `pid` on the way through, so the two copies of one
77
+ * action do not agree on either. `invocationId` is minted by the engine and
78
+ * echoed unchanged, which makes it the only field that identifies the same run in
79
+ * both files. A legacy feed record with no invocationId cannot be matched to
80
+ * anything, so it is kept: dropping it would lose history the standalone ledger
81
+ * never had.
82
+ */
83
+ export declare function mergeComputerActionSources(standalone: ComputerAction[], legacy: ComputerAction[]): ComputerAction[];
30
84
  /** Read `computer.action` events straight from the durable event ledger,
31
85
  * newest first, bounded by `limit`. Malformed/legacy records (missing
32
86
  * `command` or `pid`, or an unparseable `ts`) are skipped, never thrown —
@@ -94,6 +148,7 @@ export declare function groupIntoComputerRuns(actions: ComputerAction[], resolve
94
148
  export declare function buildComputerSessionRows(opts?: {
95
149
  limit?: number;
96
150
  machine?: string;
151
+ observer?: string;
97
152
  }): ComputerRunRow[];
98
153
  /**
99
154
  * Search predicate for the interactive picker: task text, machine/remote
@@ -58,6 +58,10 @@
58
58
  * `sessionId` doesn't resolve (a rotated/unindexed session) but `launchId`
59
59
  * still does via the more authoritative pid registry.
60
60
  */
61
+ import * as fs from 'node:fs';
62
+ import * as path from 'node:path';
63
+ import { getCacheDir } from '../state.js';
64
+ import { machineId } from '../machine-id.js';
61
65
  import { query, truncate } from '../feed/events.js';
62
66
  import { formatRelativeTime } from '../session/relative-time.js';
63
67
  import { getSessionById, listComputerSessionRecords, pruneToolSessions } from '../session/db.js';
@@ -70,6 +74,21 @@ export const TASK_PREVIEW_MAX_CHARS = 200;
70
74
  * already reads newest-first and stops at this cutoff mid-scan, so raising
71
75
  * it only ever costs as much as the history actually holds. */
72
76
  const DEFAULT_ACTION_LIMIT = 5000;
77
+ /** A capture record, accepted only when the producer gave a real path + name. */
78
+ function parseActionCapture(value) {
79
+ if (!value || typeof value !== 'object')
80
+ return undefined;
81
+ const record = value;
82
+ if (typeof record.path !== 'string' || !record.path)
83
+ return undefined;
84
+ const name = typeof record.name === 'string' && record.name ? record.name : path.basename(record.path);
85
+ return {
86
+ path: record.path,
87
+ kind: 'screenshot',
88
+ name,
89
+ ...(typeof record.bytes === 'number' ? { bytes: record.bytes } : {}),
90
+ };
91
+ }
73
92
  function recordToAction(r) {
74
93
  if (typeof r.command !== 'string' || typeof r.pid !== 'number')
75
94
  return null;
@@ -91,8 +110,153 @@ function recordToAction(r) {
91
110
  agent: typeof r.agent === 'string' ? r.agent : undefined,
92
111
  machineId: typeof r.machineId === 'string' ? r.machineId : undefined,
93
112
  hostname: typeof r.hostname === 'string' ? r.hostname : undefined,
113
+ capture: parseActionCapture(r.capture),
94
114
  };
95
115
  }
116
+ /**
117
+ * The standalone engine's OWN action ledger.
118
+ *
119
+ * `agents computer` is a thin consumer of the standalone `computer` engine
120
+ * (PHNX-4075), and that engine ALWAYS appends every action it performs to
121
+ * `<cache>/computer/actions/<day>.jsonl` — independently of whether agents-cli
122
+ * was in the call at all. So a `computer` command the operator ran directly
123
+ * appears ONLY here: it never reached `recordComputerAction`, so it is in
124
+ * neither the feed event ledger nor `computer_sessions`. Reading only those two
125
+ * meant the actions an operator actually performed were invisible to every
126
+ * agents-cli surface.
127
+ */
128
+ export function standaloneComputerActionsDir() {
129
+ return path.join(getCacheDir(), 'computer', 'actions');
130
+ }
131
+ /**
132
+ * One line of the standalone ledger as a {@link ComputerAction}.
133
+ *
134
+ * The on-disk record is the same `computer.action` event the engine reports on
135
+ * fd 4 — the contract `computer-client.ts` `ComputerActionEvent` already
136
+ * describes and `recordComputerAction` already consumes — so this maps the same
137
+ * fields rather than inventing a second vocabulary. A record missing the two
138
+ * fields that make it an action at all (`command`, a parseable timestamp) is
139
+ * skipped, never defaulted: the file is plain JSONL another process may be
140
+ * mid-append to.
141
+ */
142
+ function standaloneLineToAction(line, observer) {
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(line);
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ if (!parsed || typeof parsed !== 'object')
151
+ return null;
152
+ const record = parsed;
153
+ const verb = typeof record.command === 'string' ? record.command : undefined;
154
+ const ts = typeof record.ts === 'string' ? record.ts : undefined;
155
+ if (!verb || !ts)
156
+ return null;
157
+ const tsMs = Date.parse(ts);
158
+ if (Number.isNaN(tsMs))
159
+ return null;
160
+ const text = (key) => (typeof record[key] === 'string' ? record[key] : undefined);
161
+ const num = (key) => (typeof record[key] === 'number' ? record[key] : undefined);
162
+ // The producer identifies the DRIVEN host only for a remote run, and emits no
163
+ // `hostname`/`machineId` of its own at all. Defaulting those to the observing
164
+ // machine HERE, at the source, is what keeps a direct local action attributable:
165
+ // without it `groupIntoComputerRuns` fell back to `machine: 'unknown'`, the row's
166
+ // device read `unknown`, and every locally-run `computer` action vanished the
167
+ // moment a consumer filtered by device. The observer IS the machine that ran it
168
+ // — this ledger is per-machine by construction, written by the engine on the box
169
+ // it ran on — so the fallback is a fact, not a guess. An explicit `host` still
170
+ // wins, because that names a genuinely different driven machine.
171
+ return {
172
+ verb, ts, tsMs,
173
+ // The engine's pid is its own; a record without one still groups by
174
+ // invocationId, which is the identity that actually matters here.
175
+ pid: num('pid') ?? 0,
176
+ invocationId: text('invocationId'),
177
+ targetPid: num('targetPid'),
178
+ bundle: text('bundle'),
179
+ host: text('host'),
180
+ task: text('task'),
181
+ sessionId: text('sessionId'),
182
+ launchId: text('launchId'),
183
+ agent: text('agent'),
184
+ machineId: text('machineId'),
185
+ hostname: text('hostname') ?? observer,
186
+ capture: parseActionCapture(record.capture),
187
+ };
188
+ }
189
+ /**
190
+ * Read the standalone ledger, newest day first, bounded by `limit` actions.
191
+ *
192
+ * Bounded by construction: days are read newest-first and reading stops as soon
193
+ * as the budget is met, so a box with months of history costs the same as one
194
+ * with a day of it.
195
+ */
196
+ export function listStandaloneComputerActions(opts = {}) {
197
+ const dir = opts.dir ?? standaloneComputerActionsDir();
198
+ const limit = opts.limit ?? DEFAULT_ACTION_LIMIT;
199
+ // This ledger is per-machine by construction, so "who ran it" is this machine
200
+ // unless the record names a driven remote host.
201
+ const observer = opts.observer ?? machineId();
202
+ let days;
203
+ try {
204
+ days = fs.readdirSync(dir).filter((name) => name.endsWith('.jsonl')).sort().reverse();
205
+ }
206
+ catch {
207
+ return []; // The engine has never run here, or is not installed.
208
+ }
209
+ const out = [];
210
+ for (const day of days) {
211
+ if (out.length >= limit)
212
+ break;
213
+ let lines;
214
+ try {
215
+ lines = fs.readFileSync(path.join(dir, day), 'utf8').split('\n');
216
+ }
217
+ catch {
218
+ continue; /* rotated or removed mid-read */
219
+ }
220
+ // Newest last within a day, and the budget favours the newest actions.
221
+ for (let index = lines.length - 1; index >= 0 && out.length < limit; index--) {
222
+ const line = lines[index];
223
+ if (!line)
224
+ continue;
225
+ const action = standaloneLineToAction(line, observer);
226
+ if (action)
227
+ out.push(action);
228
+ }
229
+ }
230
+ out.sort((a, b) => b.tsMs - a.tsMs);
231
+ return out;
232
+ }
233
+ /**
234
+ * Union the two ledgers, preferring the standalone record for any run present in
235
+ * both.
236
+ *
237
+ * Dedupe keys on `invocationId`, NOT on a timestamp or pid. When a command is
238
+ * forwarded through `agents computer`, BOTH stores receive it — and the
239
+ * forwarding rewrites `ts` and `pid` on the way through, so the two copies of one
240
+ * action do not agree on either. `invocationId` is minted by the engine and
241
+ * echoed unchanged, which makes it the only field that identifies the same run in
242
+ * both files. A legacy feed record with no invocationId cannot be matched to
243
+ * anything, so it is kept: dropping it would lose history the standalone ledger
244
+ * never had.
245
+ */
246
+ export function mergeComputerActionSources(standalone, legacy) {
247
+ const standaloneRuns = new Set();
248
+ for (const action of standalone)
249
+ if (action.invocationId)
250
+ standaloneRuns.add(action.invocationId);
251
+ const merged = [...standalone];
252
+ for (const action of legacy) {
253
+ if (action.invocationId && standaloneRuns.has(action.invocationId))
254
+ continue;
255
+ merged.push(action);
256
+ }
257
+ merged.sort((a, b) => b.tsMs - a.tsMs);
258
+ return merged;
259
+ }
96
260
  /** Read `computer.action` events straight from the durable event ledger,
97
261
  * newest first, bounded by `limit`. Malformed/legacy records (missing
98
262
  * `command` or `pid`, or an unparseable `ts`) are skipped, never thrown —
@@ -240,7 +404,10 @@ function appendPrunedRunsFromDb(rows, limit) {
240
404
  * flat/`--json` printer's) data source. `machine` narrows to rows whose
241
405
  * invoking hostname, machineId, or `--device` target contains the substring. */
242
406
  export function buildComputerSessionRows(opts = {}) {
243
- const actions = listComputerActions({ limit: opts.limit });
407
+ const actions = mergeComputerActionSources(
408
+ // `observer` is threaded from the caller's scope so a row's device and the
409
+ // scope that reported it can never name the same box differently.
410
+ listStandaloneComputerActions({ limit: opts.limit, ...(opts.observer ? { observer: opts.observer } : {}) }), listComputerActions({ limit: opts.limit }));
244
411
  const index = buildLaunchSessionIndex();
245
412
  const rows = groupIntoComputerRuns(actions, (sessionId) => getSessionById(sessionId), (launchId) => resolveLaunchSession(index, launchId));
246
413
  // Retention runs here, on the listing path, never on the action hot path:
@@ -16,7 +16,7 @@ export type ConfigScope = 'run' | 'interactive' | 'auto' | 'browser' | 'project'
16
16
  * CONFIG_KEYS (user scope); this list is only the parser's allow-set so an
17
17
  * unknown `menubar.menu.<x>` fails loud here instead of at the store.
18
18
  */
19
- export declare const MENUBAR_MENU_PROPERTIES: readonly ["defaultProject", "workingRowsShown", "showPreviews", "projectPriorityFilter", "hideCompletedMilestones", "bannerWhenNeedsYou", "includeOtherDeviceRequests", "groupBy", "thenBy", "projectScope", "projectSort", "ticketSort", "showPullRequests"];
19
+ export declare const MENUBAR_MENU_PROPERTIES: readonly ["defaultProject", "workingRowsShown", "showPreviews", "projectPriorityFilter", "hideCompletedMilestones", "bannerWhenNeedsYou", "includeOtherDeviceRequests", "groupBy", "thenBy", "projectScope", "projectSort", "ticketSort", "showPullRequests", "sessionUpdates", "deviceSort", "headlessAgent", "headlessFallbackAgent", "headlessPlacement"];
20
20
  /** A run-time default key: model, mode, effort, or tier override. */
21
21
  export interface ParsedRunConfigKey {
22
22
  scope: 'run';
@@ -29,6 +29,11 @@ export const MENUBAR_MENU_PROPERTIES = [
29
29
  'projectSort',
30
30
  'ticketSort',
31
31
  'showPullRequests',
32
+ 'sessionUpdates',
33
+ 'deviceSort',
34
+ 'headlessAgent',
35
+ 'headlessFallbackAgent',
36
+ 'headlessPlacement',
32
37
  ];
33
38
  const DEVICE_CONFIG_PROPERTIES = [
34
39
  'browser.viewer',
@@ -878,7 +878,7 @@ export async function runDaemon() {
878
878
  assertTestDaemonHome();
879
879
  // Lifecycle readers and launchers do not run services. Load their code only
880
880
  // in the daemon process, before it claims or publishes lifecycle state.
881
- const [{ BrowserService }, { SessionIndexService }, { SessionSummarizerService }, { SessionTitleService }, { MonitorEngineService }, { AccountUsageService, AccountAuthService }, { CatchupService }, { BrowserIPCService }, { WatchdogService }, { DeviceProbeService }, { SelfHealService }, { SelfUpdateService }, { HarnessUpdateService }, { AuthSyncService }, { UsageSyncService }, { StateDirCheckService }, { SessionStateService }, { AttentionNotifyService }, { WebhookReceiverService }, { HeartbeatService }, { TmuxReapService }, { BrowserTaskReapService },] = await Promise.all([
881
+ const [{ BrowserService }, { SessionIndexService }, { SessionSummarizerService }, { SessionTitleService }, { MonitorEngineService }, { AccountUsageService, AccountAuthService }, { CatchupService }, { BrowserIPCService }, { WatchdogService }, { DeviceProbeService }, { SelfHealService }, { SelfUpdateService }, { HarnessUpdateService }, { AuthSyncService }, { UsageSyncService }, { StateDirCheckService }, { SessionStateService }, { FeedStreamService }, { AttentionNotifyService }, { WebhookReceiverService }, { HeartbeatService }, { TmuxReapService }, { BrowserTaskReapService },] = await Promise.all([
882
882
  import('../browser/service.js'),
883
883
  import('./session-index-service.js'),
884
884
  import('./session-summarizer-service.js'),
@@ -896,6 +896,7 @@ export async function runDaemon() {
896
896
  import('./usage-sync-service.js'),
897
897
  import('./state-dir-check-service.js'),
898
898
  import('./session-state-service.js'),
899
+ import('./feed-stream-service.js'),
899
900
  import('./attention-notify-service.js'),
900
901
  import('./webhook-receiver-service.js'),
901
902
  import('./heartbeat-service.js'),
@@ -1021,6 +1022,12 @@ export async function runDaemon() {
1021
1022
  }
1022
1023
  else
1023
1024
  log('INFO', 'Live session-state service disabled');
1025
+ // The shared feed fan-out. Registered even when disabled at boot so a later
1026
+ // `agents daemon services enable feed-stream` brings it up over SIGHUP, the
1027
+ // same live-transition shape browser IPC uses.
1028
+ supervisor.register(new FeedStreamService(), { enabled: isEnabled('feed-stream') });
1029
+ if (!isEnabled('feed-stream'))
1030
+ log('INFO', 'Shared feed stream service disabled');
1024
1031
  const monitorEngineSvc = new MonitorEngineService();
1025
1032
  if (isEnabled('monitors'))
1026
1033
  supervisor.register(monitorEngineSvc);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The shared feed collector's lifecycle as a `DaemonService`.
3
+ *
4
+ * WHY THE DAEMON OWNS IT. Dialing every peer over ssh and holding those
5
+ * connections open is an ACTION on the fleet, so by the repo's one-scheduler /
6
+ * one-executor rule it belongs to the daemon and not to a UI surface. Before
7
+ * this service, every consumer of `agents feed watch --json` ran its own
8
+ * `watchFleetFeed`, which is N ssh children per peer for N readers. The daemon
9
+ * runs one, and the readers attach over a socket (`feed/hub-server.ts`).
10
+ *
11
+ * The server binds on start, but the fan-out itself is DEMAND-GATED by the hub:
12
+ * with no client connected there is no `watchFleetFeed`, so an idle box holds no
13
+ * peer connections — the same idle-cost rule `SessionStateService` follows for
14
+ * its gather.
15
+ */
16
+ import { BaseDaemonService, type DaemonContext } from './service.js';
17
+ import type { DaemonServiceId } from '../daemon-services.js';
18
+ export declare class FeedStreamService extends BaseDaemonService {
19
+ readonly id: DaemonServiceId;
20
+ private server;
21
+ protected onStart(ctx: DaemonContext): Promise<void>;
22
+ protected onStop(): Promise<void>;
23
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The shared feed collector's lifecycle as a `DaemonService`.
3
+ *
4
+ * WHY THE DAEMON OWNS IT. Dialing every peer over ssh and holding those
5
+ * connections open is an ACTION on the fleet, so by the repo's one-scheduler /
6
+ * one-executor rule it belongs to the daemon and not to a UI surface. Before
7
+ * this service, every consumer of `agents feed watch --json` ran its own
8
+ * `watchFleetFeed`, which is N ssh children per peer for N readers. The daemon
9
+ * runs one, and the readers attach over a socket (`feed/hub-server.ts`).
10
+ *
11
+ * The server binds on start, but the fan-out itself is DEMAND-GATED by the hub:
12
+ * with no client connected there is no `watchFleetFeed`, so an idle box holds no
13
+ * peer connections — the same idle-cost rule `SessionStateService` follows for
14
+ * its gather.
15
+ */
16
+ import { BaseDaemonService } from './service.js';
17
+ import { FeedHub } from '../feed/hub.js';
18
+ import { FeedHubServer } from '../feed/hub-server.js';
19
+ import { sharedLocalFeedHub, watchFleetFeed } from '../feed/watch.js';
20
+ export class FeedStreamService extends BaseDaemonService {
21
+ id = 'feed-stream';
22
+ server = null;
23
+ async onStart(ctx) {
24
+ // Two collectors, one socket. The fleet hub holds one ssh child per peer plus
25
+ // this box's rows; the local hub is this box ONLY and cannot fan out, so a
26
+ // reader that wants just this machine never causes a peer dial. They are
27
+ // separate hubs rather than one filtered stream because demand must be
28
+ // separate too: a local-only reader must not start the fleet fan-out.
29
+ const fleet = new FeedHub({ watch: watchFleetFeed });
30
+ this.server = new FeedHubServer(fleet, undefined, sharedLocalFeedHub());
31
+ await this.server.start();
32
+ ctx.log('INFO', 'Feed stream hub listening (fan-out starts on first reader)');
33
+ }
34
+ async onStop() {
35
+ const server = this.server;
36
+ this.server = null;
37
+ if (server)
38
+ await server.stop();
39
+ }
40
+ }
@@ -7,7 +7,7 @@
7
7
  * enabled without pulling in the whole daemon lifecycle.
8
8
  */
9
9
  /** Every service the daemon can host. IDs are kebab-case and stable. */
10
- export type DaemonServiceId = 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'session-title' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'session-summarizer' | 'attention-notify' | 'harness-update';
10
+ export type DaemonServiceId = 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'session-title' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'feed-stream' | 'session-summarizer' | 'attention-notify' | 'harness-update';
11
11
  /** Human-readable metadata for each service. */
12
12
  interface DaemonServiceDef {
13
13
  id: DaemonServiceId;
@@ -92,6 +92,11 @@ export const DAEMON_SERVICES = [
92
92
  title: 'Live session state',
93
93
  description: 'Publishes this host\'s active session metadata for sessions watch and fleet consumers.',
94
94
  },
95
+ {
96
+ id: 'feed-stream',
97
+ title: 'Shared feed stream',
98
+ description: 'Owns the one fleet feed fan-out and serves it to every reader over a socket, so N consumers cost one ssh per peer instead of N.',
99
+ },
95
100
  {
96
101
  id: 'session-index',
97
102
  title: 'Session-index warm',