@phnx-labs/agents-cli 1.20.64 → 1.20.66
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 +51 -3
- package/README.md +156 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.d.ts +48 -0
- package/dist/commands/exec.js +159 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +66 -20
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +79 -4
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +205 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +45 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +36 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** One issued token, stored by hash only. */
|
|
2
|
+
export interface ControlTokenRecord {
|
|
3
|
+
/** Short public id for display / revocation (not secret). */
|
|
4
|
+
id: string;
|
|
5
|
+
/** SHA-256 hex of the raw token. */
|
|
6
|
+
hash: string;
|
|
7
|
+
/** Human label, e.g. "muqsit-iphone". */
|
|
8
|
+
label: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Mint a new control token, persist its hash, and return the raw token ONCE.
|
|
13
|
+
* The caller must surface it to the operator immediately — it cannot be
|
|
14
|
+
* recovered later.
|
|
15
|
+
*/
|
|
16
|
+
export declare function addControlToken(label: string): {
|
|
17
|
+
id: string;
|
|
18
|
+
token: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Ensure at least one token exists. Returns the raw token only when it had to
|
|
22
|
+
* mint one (first `--control` boot); otherwise `{ created: false }` and the
|
|
23
|
+
* existing tokens stand.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ensureControlToken(label?: string): {
|
|
26
|
+
created: true;
|
|
27
|
+
id: string;
|
|
28
|
+
token: string;
|
|
29
|
+
} | {
|
|
30
|
+
created: false;
|
|
31
|
+
};
|
|
32
|
+
/** True when `presented` matches a stored token hash (constant-time compare). */
|
|
33
|
+
export declare function verifyControlToken(presented: string | undefined): boolean;
|
|
34
|
+
/** List issued tokens (hashes only — safe to display). */
|
|
35
|
+
export declare function listControlTokens(): ControlTokenRecord[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer-token store for the authenticated `agents serve --control` server.
|
|
3
|
+
*
|
|
4
|
+
* Only the SHA-256 *hash* of each token is written to disk — the raw token is
|
|
5
|
+
* shown once at creation (like an API key) and never persisted, so a leaked
|
|
6
|
+
* store file cannot be replayed. This is the anchor-side credential the iOS
|
|
7
|
+
* cockpit presents on every request; `agents devices pair-ios` (Phase 3) mints
|
|
8
|
+
* additional per-device tokens through {@link addControlToken}.
|
|
9
|
+
*
|
|
10
|
+
* Store path: `<cache>/serve/control-tokens.json` — under `.cache/`, which is
|
|
11
|
+
* gitignored, so no credential material lands in a version-controlled repo.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { createHash, randomBytes, timingSafeEqual } from 'crypto';
|
|
16
|
+
import { getCacheDir } from '../state.js';
|
|
17
|
+
function storePath() {
|
|
18
|
+
return path.join(getCacheDir(), 'serve', 'control-tokens.json');
|
|
19
|
+
}
|
|
20
|
+
function readStore() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(storePath(), 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (parsed && Array.isArray(parsed.tokens))
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Missing/corrupt store → start empty; a fresh token gets minted below.
|
|
29
|
+
}
|
|
30
|
+
return { tokens: [] };
|
|
31
|
+
}
|
|
32
|
+
function writeStore(store) {
|
|
33
|
+
const p = storePath();
|
|
34
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
35
|
+
// 0600: readable only by the owner — it holds token hashes. `mode` on
|
|
36
|
+
// writeFileSync is honored only at *creation*, so chmod every write to
|
|
37
|
+
// self-heal if the perms were ever widened externally (this is a credential
|
|
38
|
+
// file, even if only hashes).
|
|
39
|
+
fs.writeFileSync(p, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
40
|
+
fs.chmodSync(p, 0o600);
|
|
41
|
+
}
|
|
42
|
+
function sha256(s) {
|
|
43
|
+
return createHash('sha256').update(s).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mint a new control token, persist its hash, and return the raw token ONCE.
|
|
47
|
+
* The caller must surface it to the operator immediately — it cannot be
|
|
48
|
+
* recovered later.
|
|
49
|
+
*/
|
|
50
|
+
export function addControlToken(label) {
|
|
51
|
+
const store = readStore();
|
|
52
|
+
const id = randomBytes(4).toString('hex');
|
|
53
|
+
const token = randomBytes(32).toString('hex');
|
|
54
|
+
store.tokens.push({ id, hash: sha256(token), label, createdAt: new Date().toISOString() });
|
|
55
|
+
writeStore(store);
|
|
56
|
+
return { id, token };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Ensure at least one token exists. Returns the raw token only when it had to
|
|
60
|
+
* mint one (first `--control` boot); otherwise `{ created: false }` and the
|
|
61
|
+
* existing tokens stand.
|
|
62
|
+
*/
|
|
63
|
+
export function ensureControlToken(label = 'default') {
|
|
64
|
+
const store = readStore();
|
|
65
|
+
if (store.tokens.length > 0)
|
|
66
|
+
return { created: false };
|
|
67
|
+
const { id, token } = addControlToken(label);
|
|
68
|
+
return { created: true, id, token };
|
|
69
|
+
}
|
|
70
|
+
/** True when `presented` matches a stored token hash (constant-time compare). */
|
|
71
|
+
export function verifyControlToken(presented) {
|
|
72
|
+
if (!presented)
|
|
73
|
+
return false;
|
|
74
|
+
const want = Buffer.from(sha256(presented), 'hex');
|
|
75
|
+
for (const rec of readStore().tokens) {
|
|
76
|
+
const have = Buffer.from(rec.hash, 'hex');
|
|
77
|
+
if (have.length === want.length && timingSafeEqual(have, want))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/** List issued tokens (hashes only — safe to display). */
|
|
83
|
+
export function listControlTokens() {
|
|
84
|
+
return readStore().tokens;
|
|
85
|
+
}
|
|
@@ -76,6 +76,12 @@ export interface BuildRecordOpts {
|
|
|
76
76
|
redact: boolean;
|
|
77
77
|
/** Non-null → seal each body with this key; null → plaintext bodies. */
|
|
78
78
|
encryptKey: Buffer | null;
|
|
79
|
+
/**
|
|
80
|
+
* Literal secret values to mask verbatim during redaction (value-aware pass):
|
|
81
|
+
* e.g. live credentials from an injected secrets bundle, so they never leak in
|
|
82
|
+
* an exported transcript regardless of format. Only used when `redact` is set.
|
|
83
|
+
*/
|
|
84
|
+
knownSecrets?: readonly string[];
|
|
79
85
|
}
|
|
80
86
|
/** Look up the sync spec for an agent id (undefined → agent not sync-representable). */
|
|
81
87
|
export declare function specForAgent(agentId: string): SyncAgentSpec | undefined;
|
|
@@ -104,6 +110,14 @@ export declare function makeHeader(args: {
|
|
|
104
110
|
export declare function mergeRecords(sets: BundleRecord[][]): BundleRecord[];
|
|
105
111
|
/** Serialize a bundle to its NDJSON wire form (header line + one line per record). */
|
|
106
112
|
export declare function serializeBundle(header: BundleHeader, records: BundleRecord[]): string;
|
|
113
|
+
/**
|
|
114
|
+
* Write a serialized bundle to disk owner-only (0600). A bundle carries raw
|
|
115
|
+
* transcript bodies — even redacted, never world/group-readable — and encryption
|
|
116
|
+
* is opt-in, so the file mode is the baseline confidentiality guard. `mode` on
|
|
117
|
+
* `writeFileSync` only applies when the file is created, so we `chmod` too to
|
|
118
|
+
* clamp an existing (possibly looser) file on overwrite.
|
|
119
|
+
*/
|
|
120
|
+
export declare function writeBundleFile(outPath: string, wire: string): void;
|
|
107
121
|
/** Parse an NDJSON bundle, validating the header kind + version. Throws on malformed input. */
|
|
108
122
|
export declare function parseBundle(text: string): ParsedBundle;
|
|
109
123
|
/** Placement outcome for one record, computed against what is already on disk. */
|
|
@@ -42,7 +42,7 @@ export function isExportableAgent(agentId) {
|
|
|
42
42
|
export function buildRecord(file, opts) {
|
|
43
43
|
let body = fs.readFileSync(file.absPath, 'utf-8');
|
|
44
44
|
if (opts.redact)
|
|
45
|
-
body = redactSecrets(body);
|
|
45
|
+
body = redactSecrets(body, opts.knownSecrets);
|
|
46
46
|
const hash = hashContent(body);
|
|
47
47
|
const size = Buffer.byteLength(body, 'utf-8');
|
|
48
48
|
let stored = body;
|
|
@@ -105,6 +105,17 @@ export function serializeBundle(header, records) {
|
|
|
105
105
|
lines.push(JSON.stringify(r));
|
|
106
106
|
return lines.join('\n') + '\n';
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Write a serialized bundle to disk owner-only (0600). A bundle carries raw
|
|
110
|
+
* transcript bodies — even redacted, never world/group-readable — and encryption
|
|
111
|
+
* is opt-in, so the file mode is the baseline confidentiality guard. `mode` on
|
|
112
|
+
* `writeFileSync` only applies when the file is created, so we `chmod` too to
|
|
113
|
+
* clamp an existing (possibly looser) file on overwrite.
|
|
114
|
+
*/
|
|
115
|
+
export function writeBundleFile(outPath, wire) {
|
|
116
|
+
fs.writeFileSync(outPath, wire, { encoding: 'utf-8', mode: 0o600 });
|
|
117
|
+
fs.chmodSync(outPath, 0o600);
|
|
118
|
+
}
|
|
108
119
|
/** Parse an NDJSON bundle, validating the header kind + version. Throws on malformed input. */
|
|
109
120
|
export function parseBundle(text) {
|
|
110
121
|
const lines = text.split('\n').filter(l => l.trim().length > 0);
|
|
@@ -18,7 +18,7 @@ import chalk from 'chalk';
|
|
|
18
18
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
19
|
import { sshTargetFor } from '../devices/connect.js';
|
|
20
20
|
import { resolveExplicitTargets } from '../devices/resolve-target.js';
|
|
21
|
-
import { loadDevices } from '../devices/registry.js';
|
|
21
|
+
import { loadDevices, isControlDevice } from '../devices/registry.js';
|
|
22
22
|
import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
|
|
23
23
|
import { machineId, normalizeHost } from './sync/config.js';
|
|
24
24
|
import { NO_FANOUT_ENV } from './remote-active.js';
|
|
@@ -129,6 +129,10 @@ export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
|
129
129
|
continue;
|
|
130
130
|
if (normalizeHost(d.name) === self)
|
|
131
131
|
continue;
|
|
132
|
+
// Control-only devices (a phone/tablet running the cockpit) drive the fleet
|
|
133
|
+
// but never run agents — never dial them, whatever their platform reads as.
|
|
134
|
+
if (isControlDevice(d))
|
|
135
|
+
continue;
|
|
132
136
|
// Only machines that can actually run the CLI. iOS/tablet nodes register as
|
|
133
137
|
// `unknown` platform and can never answer, so skip them rather than burn a
|
|
134
138
|
// full ConnectTimeout on each.
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* markers. Codex has no such tools, so it falls back to last-role + question
|
|
15
15
|
* shape + mtime — same function, driven off the normalized events.
|
|
16
16
|
*/
|
|
17
|
-
import type { SessionAttachment, SessionEvent } from './types.js';
|
|
17
|
+
import type { SessionAttachment, SessionEvent, TodoItem, TodoProgress } from './types.js';
|
|
18
|
+
export type { TodoItem, TodoProgress };
|
|
18
19
|
export type SessionActivity = 'working' | 'waiting_input' | 'idle';
|
|
19
20
|
export type AwaitingReason = 'question' | 'plan_review' | 'permission';
|
|
20
21
|
/** One discrete choice the agent offered the user. */
|
|
@@ -41,28 +42,6 @@ export interface StructuredQuestion {
|
|
|
41
42
|
reason: AwaitingReason;
|
|
42
43
|
options?: QuestionOption[];
|
|
43
44
|
}
|
|
44
|
-
/** One `TodoWrite` checklist item, as Claude's plan tool emits it. */
|
|
45
|
-
export interface TodoItem {
|
|
46
|
-
content: string;
|
|
47
|
-
status: 'pending' | 'in_progress' | 'completed';
|
|
48
|
-
/** Present-continuous label shown while this item is the active step. */
|
|
49
|
-
activeForm?: string;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Live plan progress derived from the most recent `TodoWrite` in the transcript
|
|
53
|
-
* (RUSH-1380). Lets the Factory Floor show "N/M done" + the current step for any
|
|
54
|
-
* session — including remote / device-dispatched agents that carry no local
|
|
55
|
-
* tool-call stream — instead of only a coarse working/idle verb.
|
|
56
|
-
*/
|
|
57
|
-
export interface TodoProgress {
|
|
58
|
-
items: TodoItem[];
|
|
59
|
-
/** Count of completed items. */
|
|
60
|
-
done: number;
|
|
61
|
-
/** Total items. */
|
|
62
|
-
total: number;
|
|
63
|
-
/** The in-progress item's activeForm (falls back to its content). The live step. */
|
|
64
|
-
activeForm?: string;
|
|
65
|
-
}
|
|
66
45
|
export interface DetectedPr {
|
|
67
46
|
url: string;
|
|
68
47
|
number?: number;
|
|
@@ -140,8 +119,11 @@ export interface StateContext {
|
|
|
140
119
|
activeWindowMs?: number;
|
|
141
120
|
}
|
|
142
121
|
/**
|
|
143
|
-
* Derive live plan progress from a
|
|
144
|
-
*
|
|
122
|
+
* Derive live plan progress from a checklist tool call's args. Accepts both
|
|
123
|
+
* Claude's `TodoWrite` (`todos: [{content,status,activeForm}]`) and Codex's
|
|
124
|
+
* `update_plan` (`plan: [{step,status}]`) shapes, so the CLI is the single source
|
|
125
|
+
* of checklist state for every agent. Returns undefined when there is no usable
|
|
126
|
+
* list, so a session with no plan carries no `todos` field.
|
|
145
127
|
*/
|
|
146
128
|
export declare function extractTodoProgress(args?: Record<string, any>): TodoProgress | undefined;
|
|
147
129
|
/** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
|
|
@@ -47,14 +47,22 @@ const PROSE_QUESTION_FRESH_MS = 30 * 60_000;
|
|
|
47
47
|
/** Claude tool names that structurally mean "the agent handed control back to you". */
|
|
48
48
|
const PLAN_TOOL = 'ExitPlanMode';
|
|
49
49
|
const ASK_TOOL = 'AskUserQuestion';
|
|
50
|
-
/**
|
|
50
|
+
/** The live plan/checklist tools: Claude's `TodoWrite` and Codex's `update_plan`. */
|
|
51
51
|
const TODO_TOOL = 'TodoWrite';
|
|
52
|
+
const CODEX_PLAN_TOOL = 'update_plan';
|
|
52
53
|
/**
|
|
53
|
-
* Derive live plan progress from a
|
|
54
|
-
*
|
|
54
|
+
* Derive live plan progress from a checklist tool call's args. Accepts both
|
|
55
|
+
* Claude's `TodoWrite` (`todos: [{content,status,activeForm}]`) and Codex's
|
|
56
|
+
* `update_plan` (`plan: [{step,status}]`) shapes, so the CLI is the single source
|
|
57
|
+
* of checklist state for every agent. Returns undefined when there is no usable
|
|
58
|
+
* list, so a session with no plan carries no `todos` field.
|
|
55
59
|
*/
|
|
56
60
|
export function extractTodoProgress(args) {
|
|
57
|
-
const raw = args?.todos
|
|
61
|
+
const raw = Array.isArray(args?.todos)
|
|
62
|
+
? args.todos
|
|
63
|
+
: Array.isArray(args?.plan)
|
|
64
|
+
? args.plan
|
|
65
|
+
: undefined;
|
|
58
66
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
59
67
|
return undefined;
|
|
60
68
|
const items = [];
|
|
@@ -64,7 +72,9 @@ export function extractTodoProgress(args) {
|
|
|
64
72
|
? t.content
|
|
65
73
|
: typeof t?.text === 'string' && t.text
|
|
66
74
|
? t.text
|
|
67
|
-
:
|
|
75
|
+
: typeof t?.step === 'string' && t.step
|
|
76
|
+
? t.step
|
|
77
|
+
: activeForm ?? '';
|
|
68
78
|
if (!content)
|
|
69
79
|
continue;
|
|
70
80
|
const status = t?.status === 'completed' || t?.status === 'in_progress' ? t.status : 'pending';
|
|
@@ -318,7 +328,7 @@ export function inferActivity(events, ctx = {}) {
|
|
|
318
328
|
// Live plan progress: the most recent TodoWrite's checklist (RUSH-1380). Attached
|
|
319
329
|
// to `base` so every return path below carries it — a working, waiting, or idle
|
|
320
330
|
// session all keep showing how far the plan got.
|
|
321
|
-
const lastTodo = lastOf(meaningful, e => e.type === 'tool_use' && e.tool === TODO_TOOL);
|
|
331
|
+
const lastTodo = lastOf(meaningful, e => e.type === 'tool_use' && (e.tool === TODO_TOOL || e.tool === CODEX_PLAN_TOOL));
|
|
322
332
|
const todos = lastTodo ? extractTodoProgress(lastTodo.args) : undefined;
|
|
323
333
|
const base = {
|
|
324
334
|
activity: 'idle',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* machine's stable identity. Credentials come from the `r2.backups` secrets
|
|
4
4
|
* bundle (OS keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
5
5
|
*/
|
|
6
|
-
import { readAndResolveBundleEnv } from '../../secrets/bundles.js';
|
|
6
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../../secrets/bundles.js';
|
|
7
7
|
/** Secrets bundle holding the R2 credentials. */
|
|
8
8
|
export const SYNC_BUNDLE = 'r2.backups';
|
|
9
9
|
/**
|
|
@@ -12,7 +12,13 @@ export const SYNC_BUNDLE = 'r2.backups';
|
|
|
12
12
|
* without real credentials (no silent fallback).
|
|
13
13
|
*/
|
|
14
14
|
function resolveR2Config() {
|
|
15
|
-
|
|
15
|
+
// The daemon monitor loop is headless by construction (no TTY, ever), so
|
|
16
|
+
// isHeadlessSecretsContext() is true here and this resolves broker-only — a
|
|
17
|
+
// broker miss can never pop an unattended Touch ID sheet on the user's screen
|
|
18
|
+
// (same rationale as daemon.ts readDaemonClaudeOAuthToken). Using the shared
|
|
19
|
+
// predicate rather than a literal keeps it consistent with the other callers
|
|
20
|
+
// and lets any interactive caller of loadR2Config still prompt.
|
|
21
|
+
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'sessions-sync', agentOnly: isHeadlessSecretsContext() });
|
|
16
22
|
const accountId = env.R2_ACCOUNT_ID?.trim();
|
|
17
23
|
const bucket = env.R2_BUCKET_NAME?.trim();
|
|
18
24
|
const accessKeyId = env.R2_ACCESS_KEY_ID?.trim();
|
|
@@ -41,6 +41,29 @@ export interface SessionAttachment {
|
|
|
41
41
|
mediaType: string;
|
|
42
42
|
sizeBytes?: number;
|
|
43
43
|
}
|
|
44
|
+
/** One checklist item, as Claude's `TodoWrite` (`content`) or Codex's `update_plan` (`step`) emits it. */
|
|
45
|
+
export interface TodoItem {
|
|
46
|
+
content: string;
|
|
47
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
48
|
+
/** Present-continuous label shown while this item is the active step. */
|
|
49
|
+
activeForm?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Live plan progress derived from the most recent checklist write in the transcript
|
|
53
|
+
* (Claude `TodoWrite` / Codex `update_plan`, RUSH-1380). Lets consumers show "N/M
|
|
54
|
+
* done" + the current step for any session — including remote / device-dispatched
|
|
55
|
+
* agents that carry no local tool-call stream — instead of only a coarse
|
|
56
|
+
* working/idle verb.
|
|
57
|
+
*/
|
|
58
|
+
export interface TodoProgress {
|
|
59
|
+
items: TodoItem[];
|
|
60
|
+
/** Count of completed items. */
|
|
61
|
+
done: number;
|
|
62
|
+
/** Total items. */
|
|
63
|
+
total: number;
|
|
64
|
+
/** The in-progress item's activeForm (falls back to its content). The live step. */
|
|
65
|
+
activeForm?: string;
|
|
66
|
+
}
|
|
44
67
|
/** Metadata attached when a session was spawned by `agents teams`. */
|
|
45
68
|
export interface TeamOrigin {
|
|
46
69
|
/** Teammate name if set, otherwise first 8 chars of the agent UUID. */
|
|
@@ -116,6 +139,13 @@ export interface SessionMeta {
|
|
|
116
139
|
* recover the plan text — the CLI now carries it on the metadata row.
|
|
117
140
|
*/
|
|
118
141
|
plan?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Live plan progress from the most recent checklist write (Claude `TodoWrite`
|
|
144
|
+
* / Codex `update_plan`, RUSH-1503). Populated on `agents sessions <id> --json`
|
|
145
|
+
* from the state engine so the Factory panel reads the CLI's computed checklist
|
|
146
|
+
* instead of re-parsing the transcript. Absent when the session wrote no list.
|
|
147
|
+
*/
|
|
148
|
+
todos?: TodoProgress;
|
|
119
149
|
/**
|
|
120
150
|
* True when the session was spawned programmatically (SDK entrypoint) rather
|
|
121
151
|
* than by a human at the Claude CLI. Captured at scan time from the JSONL
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render an HTML file to a 1200×630 PNG — the Open Graph cover for a shared plan.
|
|
3
|
+
*
|
|
4
|
+
* When `agents share plan.html` runs, we screenshot the plan's own hero and use it
|
|
5
|
+
* as the `og:image`, so the link unfurls into a card in Slack / iMessage / Twitter /
|
|
6
|
+
* Discord. No AI, no central render service: it's a headless screenshot on the
|
|
7
|
+
* publisher's machine, so it works identically for us and for any user, and costs
|
|
8
|
+
* nothing.
|
|
9
|
+
*
|
|
10
|
+
* Browser resolution reuses the repo's own detector (`findFirstInstalledBrowser`,
|
|
11
|
+
* the same auto-pick behind `agents browser`), then falls back to a managed
|
|
12
|
+
* Chromium in the Playwright/Puppeteer caches (present on machines that have done
|
|
13
|
+
* browser automation), then `PUPPETEER_EXECUTABLE_PATH`. If nothing headless-capable
|
|
14
|
+
* is found, `captureCover` returns null and publishing proceeds without a cover —
|
|
15
|
+
* the link still works, it just won't have a preview image.
|
|
16
|
+
*/
|
|
17
|
+
/** OG standard card size; captured at OG_SCALE× for retina crispness. */
|
|
18
|
+
export declare const OG_WIDTH = 1200;
|
|
19
|
+
export declare const OG_HEIGHT = 630;
|
|
20
|
+
/** Device scale factor for the capture — the served PNG is OG_WIDTH*OG_SCALE × OG_HEIGHT*OG_SCALE. */
|
|
21
|
+
export declare const OG_SCALE = 2;
|
|
22
|
+
/** Ordered list of candidate Chromium-family binaries to try for headless capture. */
|
|
23
|
+
export declare function candidateBrowsers(): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Screenshot `htmlPath`'s top 1200×630 (its hero) to a PNG buffer, or null if no
|
|
26
|
+
* headless-capable browser is available or every candidate fails. Never throws —
|
|
27
|
+
* a cover is a nice-to-have, never a reason to fail a publish.
|
|
28
|
+
*/
|
|
29
|
+
export declare function captureCover(htmlPath: string, timeoutMs?: number): Promise<Buffer | null>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render an HTML file to a 1200×630 PNG — the Open Graph cover for a shared plan.
|
|
3
|
+
*
|
|
4
|
+
* When `agents share plan.html` runs, we screenshot the plan's own hero and use it
|
|
5
|
+
* as the `og:image`, so the link unfurls into a card in Slack / iMessage / Twitter /
|
|
6
|
+
* Discord. No AI, no central render service: it's a headless screenshot on the
|
|
7
|
+
* publisher's machine, so it works identically for us and for any user, and costs
|
|
8
|
+
* nothing.
|
|
9
|
+
*
|
|
10
|
+
* Browser resolution reuses the repo's own detector (`findFirstInstalledBrowser`,
|
|
11
|
+
* the same auto-pick behind `agents browser`), then falls back to a managed
|
|
12
|
+
* Chromium in the Playwright/Puppeteer caches (present on machines that have done
|
|
13
|
+
* browser automation), then `PUPPETEER_EXECUTABLE_PATH`. If nothing headless-capable
|
|
14
|
+
* is found, `captureCover` returns null and publishing proceeds without a cover —
|
|
15
|
+
* the link still works, it just won't have a preview image.
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { findFirstInstalledBrowser } from '../browser/chrome.js';
|
|
22
|
+
/** OG standard card size; captured at OG_SCALE× for retina crispness. */
|
|
23
|
+
export const OG_WIDTH = 1200;
|
|
24
|
+
export const OG_HEIGHT = 630;
|
|
25
|
+
/** Device scale factor for the capture — the served PNG is OG_WIDTH*OG_SCALE × OG_HEIGHT*OG_SCALE. */
|
|
26
|
+
export const OG_SCALE = 2;
|
|
27
|
+
// Installed browsers that are poor `--headless` hosts (they hang instead of
|
|
28
|
+
// capturing). We still let a managed Chromium or an explicit override handle those
|
|
29
|
+
// machines; we just don't burn a timeout probing these. (Only members of the
|
|
30
|
+
// browser detector's `BrowserType` are meaningful here; Comet is the real case.)
|
|
31
|
+
const BAD_HEADLESS_TYPES = new Set(['comet']);
|
|
32
|
+
/** Ordered list of candidate Chromium-family binaries to try for headless capture. */
|
|
33
|
+
export function candidateBrowsers() {
|
|
34
|
+
const out = [];
|
|
35
|
+
const push = (p) => {
|
|
36
|
+
if (p && fs.existsSync(p) && !out.includes(p))
|
|
37
|
+
out.push(p);
|
|
38
|
+
};
|
|
39
|
+
// 1) An explicit override always wins.
|
|
40
|
+
push(process.env.PUPPETEER_EXECUTABLE_PATH || process.env.AGENTS_SHARE_BROWSER);
|
|
41
|
+
// 2) Managed Chromium in the Playwright / Puppeteer caches — purpose-built for
|
|
42
|
+
// headless, so it's the most reliable capture host when present.
|
|
43
|
+
for (const bin of scanCaches())
|
|
44
|
+
push(bin);
|
|
45
|
+
// 3) The repo's own auto-pick (Chrome → Brave → Edge → Chromium by platform),
|
|
46
|
+
// skipping browsers known to be poor headless hosts so we don't eat a timeout.
|
|
47
|
+
try {
|
|
48
|
+
const pick = findFirstInstalledBrowser();
|
|
49
|
+
if (pick && !BAD_HEADLESS_TYPES.has(pick.browserType))
|
|
50
|
+
push(pick.binary);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// detector is best-effort
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** Newest-first Chromium binaries under the Playwright / Puppeteer download caches. */
|
|
58
|
+
function scanCaches() {
|
|
59
|
+
const home = os.homedir();
|
|
60
|
+
const roots = os.platform() === 'darwin'
|
|
61
|
+
? [
|
|
62
|
+
path.join(home, 'Library/Caches/ms-playwright'),
|
|
63
|
+
path.join(home, '.cache/ms-playwright'),
|
|
64
|
+
path.join(home, '.cache/puppeteer/chrome'),
|
|
65
|
+
]
|
|
66
|
+
: [path.join(home, '.cache/ms-playwright'), path.join(home, '.cache/puppeteer/chrome')];
|
|
67
|
+
const rel = os.platform() === 'darwin'
|
|
68
|
+
? [
|
|
69
|
+
'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
|
|
70
|
+
'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
71
|
+
'chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
72
|
+
]
|
|
73
|
+
: ['chrome-linux/chrome', 'chrome-linux64/chrome'];
|
|
74
|
+
const found = [];
|
|
75
|
+
for (const root of roots) {
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = fs.readdirSync(root).sort().reverse(); // newest version dir first
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const e of entries) {
|
|
84
|
+
for (const r of rel) {
|
|
85
|
+
const p = path.join(root, e, r);
|
|
86
|
+
if (fs.existsSync(p))
|
|
87
|
+
found.push(p);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Screenshot `htmlPath`'s top 1200×630 (its hero) to a PNG buffer, or null if no
|
|
95
|
+
* headless-capable browser is available or every candidate fails. Never throws —
|
|
96
|
+
* a cover is a nice-to-have, never a reason to fail a publish.
|
|
97
|
+
*/
|
|
98
|
+
export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
99
|
+
const abs = path.resolve(htmlPath);
|
|
100
|
+
if (!fs.existsSync(abs))
|
|
101
|
+
return null;
|
|
102
|
+
const fileUrl = `file://${abs.split('/').map(encodeURIComponent).join('/')}`;
|
|
103
|
+
for (const bin of candidateBrowsers()) {
|
|
104
|
+
const outPng = path.join(os.tmpdir(), `agents-share-cover-${process.pid}-${Date.now()}.png`);
|
|
105
|
+
const userDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-share-chrome-'));
|
|
106
|
+
try {
|
|
107
|
+
await new Promise((resolve, reject) => {
|
|
108
|
+
execFile(bin, [
|
|
109
|
+
'--headless=new',
|
|
110
|
+
'--disable-gpu',
|
|
111
|
+
'--hide-scrollbars',
|
|
112
|
+
'--no-first-run',
|
|
113
|
+
'--no-default-browser-check',
|
|
114
|
+
`--force-device-scale-factor=${OG_SCALE}`,
|
|
115
|
+
// Bound page JS (count-up animations etc.) so the shot fires promptly
|
|
116
|
+
// instead of waiting on long-running timers.
|
|
117
|
+
'--virtual-time-budget=8000',
|
|
118
|
+
`--window-size=${OG_WIDTH},${OG_HEIGHT}`,
|
|
119
|
+
`--user-data-dir=${userDir}`,
|
|
120
|
+
`--screenshot=${outPng}`,
|
|
121
|
+
fileUrl,
|
|
122
|
+
], { timeout: timeoutMs }, (err) => (err ? reject(err) : resolve()));
|
|
123
|
+
});
|
|
124
|
+
if (fs.existsSync(outPng)) {
|
|
125
|
+
const buf = fs.readFileSync(outPng);
|
|
126
|
+
// A valid PNG starts with the 8-byte signature; guard against 0-byte writes.
|
|
127
|
+
if (buf.length > 8 && buf[0] === 0x89 && buf[1] === 0x50)
|
|
128
|
+
return buf;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// try the next candidate
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
fs.rmSync(outPng, { force: true });
|
|
136
|
+
fs.rmSync(userDir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ShareConfig {
|
|
2
|
+
/** Public base, e.g. `https://share.agents-cli.sh` or `https://agent-share.<acct>.workers.dev`. */
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
accountId: string;
|
|
5
|
+
workerName: string;
|
|
6
|
+
bucketName: string;
|
|
7
|
+
/** Custom domain when mapped (e.g. `share.agents-cli.sh`). */
|
|
8
|
+
domain?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const SHARE_BUNDLE = "share";
|
|
11
|
+
export declare const SHARE_TOKEN_KEY = "SHARE_WRITE_TOKEN";
|
|
12
|
+
export declare const DEFAULT_CF_BUNDLE = "cloudflare.com";
|
|
13
|
+
export declare const DEFAULT_WORKER_NAME = "agents-share";
|
|
14
|
+
export declare const DEFAULT_BUCKET_NAME = "agents-share";
|
|
15
|
+
/** Read the persisted endpoint config, or null if `agents share setup`/`join` never ran. */
|
|
16
|
+
export declare function readShareConfig(): ShareConfig | null;
|
|
17
|
+
/** Persist the endpoint config to `agents.yaml` (syncs across the fleet). */
|
|
18
|
+
export declare function writeShareConfig(cfg: ShareConfig): void;
|
|
19
|
+
/** A fresh 32-byte hex write token. */
|
|
20
|
+
export declare function generateWriteToken(): string;
|
|
21
|
+
/** Persist the raw write token into the `share` secrets bundle (keychain-backed,
|
|
22
|
+
* fleet-injectable). Mirrors the add-key sequence in `commands/secrets.ts`. */
|
|
23
|
+
export declare function storeWriteToken(token: string): void;
|
|
24
|
+
/** Read the raw write token from the `share` secrets bundle. Throws with an
|
|
25
|
+
* actionable message if absent (run setup/join first). */
|
|
26
|
+
export declare function readWriteToken(): string;
|
|
27
|
+
/** Cloudflare API credentials for provisioning, read from `cloudflare.com` (or a
|
|
28
|
+
* user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
|
|
29
|
+
export declare function readCloudflareCreds(bundle?: string, override?: {
|
|
30
|
+
apiToken?: string;
|
|
31
|
+
accountId?: string;
|
|
32
|
+
}): {
|
|
33
|
+
apiToken: string;
|
|
34
|
+
accountId: string;
|
|
35
|
+
};
|