@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.
- package/CHANGELOG.md +245 -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 +210 -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 +214 -148
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/feed.js +65 -14
- 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-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- 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.d.ts +21 -2
- package/dist/commands/sessions.js +158 -11
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-term.d.ts +24 -0
- package/dist/commands/setup-term.js +70 -0
- 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 +29 -11
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +93 -302
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +3 -9
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +6 -5
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +229 -37
- package/dist/lib/browser/types.d.ts +18 -0
- 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/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- 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/device-config.js +61 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- 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/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +141 -0
- package/dist/lib/feed/hub-server.js +586 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- 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/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- 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/harness-auth-capabilities.js +7 -2
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +72 -1
- package/dist/lib/hosts/remote-cmd.js +151 -10
- 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/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- 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/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- 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/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- 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/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- 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 +27 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +220 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- 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/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- 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/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
- 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,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 =
|
|
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';
|
package/dist/lib/config-keys.js
CHANGED
|
@@ -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',
|