@phnx-labs/agents-cli 1.20.63 → 1.20.65
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 +56 -3
- package/README.md +46 -2
- 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.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- 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-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- 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 +36 -0
- package/dist/lib/agents.js +80 -20
- 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/browser/drivers/ssh.js +19 -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/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -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 +46 -18
- 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 +71 -0
- 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/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -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 +201 -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 +35 -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/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- 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 +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- 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 +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- 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/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- 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 +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- 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
|
@@ -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',
|
|
@@ -12,23 +12,54 @@
|
|
|
12
12
|
* also exists locally always wins — the mirror only ever fills in sessions
|
|
13
13
|
* originated on other machines.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
/** One constituent file of a session — a session has exactly one for file-shaped
|
|
16
|
+
* agents (Claude, Codex, …), and many for directory-shaped ones (Kimi). */
|
|
17
|
+
export interface SessionFile {
|
|
16
18
|
/** Absolute path on this machine. */
|
|
17
19
|
absPath: string;
|
|
18
|
-
/** Globally-unique session id (the grouping key across machines). */
|
|
19
|
-
sessionId: string;
|
|
20
20
|
/** Path relative to the agent's subdir root — preserved in the mirror layout. */
|
|
21
21
|
relKey: string;
|
|
22
22
|
}
|
|
23
|
+
export interface LocalTranscript {
|
|
24
|
+
/** Globally-unique session id (the grouping key across machines). */
|
|
25
|
+
sessionId: string;
|
|
26
|
+
/** Every file that makes up this session. Length 1 for file-shaped agents. */
|
|
27
|
+
files: SessionFile[];
|
|
28
|
+
}
|
|
23
29
|
export interface SyncAgentSpec {
|
|
24
30
|
id: string;
|
|
25
31
|
/** Config subdir under the agent home that holds transcripts. */
|
|
26
32
|
subdir: string;
|
|
27
33
|
/** File extension to walk for this agent (defaults to .jsonl). */
|
|
28
34
|
ext?: string;
|
|
35
|
+
/**
|
|
36
|
+
* A session is a DIRECTORY of files (e.g. Kimi: state.json + agents/…/wire.jsonl
|
|
37
|
+
* + per-tool task sidecars), not a single transcript. When set, every file under
|
|
38
|
+
* the session dir (matching `exts`, passing `fileFilter`) syncs, is stored under
|
|
39
|
+
* its own R2 sub-key, and is mirrored at its own relative path — instead of the
|
|
40
|
+
* file-shaped "one transcript per session" model.
|
|
41
|
+
*/
|
|
42
|
+
dirShaped?: boolean;
|
|
43
|
+
/** Extensions a dir-shaped agent walks (defaults to `[ext ?? '.jsonl']`). */
|
|
44
|
+
exts?: string[];
|
|
45
|
+
/** Optional per-file exclusion for dir-shaped agents (lock/scratch files, …). */
|
|
46
|
+
fileFilter?(relKey: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Extensions whose files are append-only event logs and therefore CRDT-mergeable
|
|
49
|
+
* (G-Set union across forked copies). A dir-shaped session usually mixes an
|
|
50
|
+
* append-only conversation log (`wire.jsonl`) with mutable metadata blobs
|
|
51
|
+
* (`state.json`) — line-unioning the latter would corrupt it, so any file NOT
|
|
52
|
+
* matching an entry here is reconciled last-writer-wins instead. Undefined (the
|
|
53
|
+
* file-shaped default) means every file is mergeable — the single `.jsonl`
|
|
54
|
+
* transcript keeps its existing union behaviour.
|
|
55
|
+
*/
|
|
56
|
+
mergeableExts?: string[];
|
|
29
57
|
/** Derive the session id from a storage-relative key. */
|
|
30
58
|
sessionIdFromRelKey(relKey: string): string;
|
|
31
59
|
}
|
|
60
|
+
/** True when a file at `relKey` is an append-only log that CRDT-unions across
|
|
61
|
+
* forks; false when it must be reconciled last-writer-wins (mutable blob). */
|
|
62
|
+
export declare function isMergeableFile(spec: SyncAgentSpec, relKey: string): boolean;
|
|
32
63
|
export declare const SYNC_AGENTS: SyncAgentSpec[];
|
|
33
64
|
/**
|
|
34
65
|
* List this machine's own transcript files for an agent, EXCLUDING the sync
|
|
@@ -38,10 +69,27 @@ export declare const SYNC_AGENTS: SyncAgentSpec[];
|
|
|
38
69
|
export declare function listLocalTranscripts(spec: SyncAgentSpec): LocalTranscript[];
|
|
39
70
|
/** Session ids this machine holds locally (live home), used to skip mirror writes. */
|
|
40
71
|
export declare function localSessionIds(spec: SyncAgentSpec): Set<string>;
|
|
41
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Absolute mirror path for a remote machine's transcript — lands in a scan root.
|
|
74
|
+
*
|
|
75
|
+
* `machine` and `relKey` come from a peer's manifest (untrusted: any peer with
|
|
76
|
+
* write access to the shared bucket controls them). Unlike the push side, which
|
|
77
|
+
* already drops `relKey` starting with `..` when building the manifest, the pull
|
|
78
|
+
* side would otherwise `fs.writeFileSync` at this path with peer-controlled
|
|
79
|
+
* content. `machine` is constrained to a single segment and `relKey` (which may
|
|
80
|
+
* legitimately nest, e.g. `projects/x/y.jsonl`) is contained beneath the
|
|
81
|
+
* per-machine mirror root, so a crafted `relKey` like `../../../.ssh/authorized_keys`
|
|
82
|
+
* cannot write outside `~/.agents/.history/backups/<agent>/<machine>/<subdir>`.
|
|
83
|
+
*/
|
|
42
84
|
export declare function mirrorPath(spec: SyncAgentSpec, machine: string, relKey: string): string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
85
|
+
/**
|
|
86
|
+
* R2 object key for a transcript.
|
|
87
|
+
* - file-shaped (relKey omitted): sessions/<machine>/<agent>/<sessionId>.jsonl —
|
|
88
|
+
* unchanged, so existing claude/codex/droid objects keep their keys.
|
|
89
|
+
* - dir-shaped (relKey given): sessions/<machine>/<agent>/<sessionId>/<relKey> —
|
|
90
|
+
* one object per constituent file of the session directory.
|
|
91
|
+
*/
|
|
92
|
+
export declare function objectKey(machine: string, agentId: string, sessionId: string, relKey?: string): string;
|
|
45
93
|
/** R2 object key for a machine's manifest. */
|
|
46
94
|
export declare function manifestKey(machine: string): string;
|
|
47
95
|
/** Prefix under which all machine manifests live (for discovery). */
|
|
Binary file
|
|
@@ -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();
|
|
@@ -18,12 +18,23 @@ export interface ManifestEntry {
|
|
|
18
18
|
/** Latest event timestamp in the transcript. */
|
|
19
19
|
lastTs: string;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* sessionId -> the session's file entries.
|
|
23
|
+
* - file-shaped agents (claude/codex/droid) store a single `ManifestEntry`,
|
|
24
|
+
* byte-identical to the pre-multi-file format, so a machine on an older CLI
|
|
25
|
+
* reads them unchanged.
|
|
26
|
+
* - dir-shaped agents (kimi) store a `ManifestEntry[]`, one per constituent file.
|
|
27
|
+
* A machine that predates multi-file support skips these (an unknown agent is
|
|
28
|
+
* skipped whole; a kimi-aware-but-file-shaped reader fails to find the flat
|
|
29
|
+
* object key and simply retries — it never crashes or corrupts).
|
|
30
|
+
*/
|
|
31
|
+
export type AgentManifest = Record<string, ManifestEntry | ManifestEntry[]>;
|
|
32
|
+
/** Normalize a manifest value to its file-entry list (single entry -> length-1). */
|
|
33
|
+
export declare function manifestEntries(value: ManifestEntry | ManifestEntry[]): ManifestEntry[];
|
|
23
34
|
export interface Manifest {
|
|
24
35
|
machine: string;
|
|
25
36
|
updatedAt: string;
|
|
26
|
-
/** agentId -> (sessionId -> entry) */
|
|
37
|
+
/** agentId -> (sessionId -> entry | entry[]) */
|
|
27
38
|
agents: Record<string, AgentManifest>;
|
|
28
39
|
}
|
|
29
40
|
export declare function emptyManifest(machine: string, updatedAt: string): Manifest;
|
|
@@ -13,6 +13,10 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
15
|
import { getCacheDir } from '../../state.js';
|
|
16
|
+
/** Normalize a manifest value to its file-entry list (single entry -> length-1). */
|
|
17
|
+
export function manifestEntries(value) {
|
|
18
|
+
return Array.isArray(value) ? value : [value];
|
|
19
|
+
}
|
|
16
20
|
export function emptyManifest(machine, updatedAt) {
|
|
17
21
|
return { machine, updatedAt, agents: {} };
|
|
18
22
|
}
|
|
@@ -55,10 +55,31 @@ export interface PendingSession {
|
|
|
55
55
|
*/
|
|
56
56
|
export declare function selectSessionsToFetch(copies: Map<string, Map<string, RemoteCopy[]>>, localIdsByAgent: Map<string, Set<string>>, pullState: PullState): PendingSession[];
|
|
57
57
|
/**
|
|
58
|
-
* Resolve the mirror destination +
|
|
58
|
+
* Resolve the mirror destination + reconciled content for ONE file across its
|
|
59
|
+
* copies (every copy here is the same file — same relKey — held by a different
|
|
60
|
+
* machine). Pure.
|
|
61
|
+
*
|
|
59
62
|
* The canonical path comes from the lexicographically-smallest machine so every
|
|
60
|
-
* puller derives an identical location
|
|
63
|
+
* puller derives an identical location. The content depends on the file's kind
|
|
64
|
+
* (see `isMergeableFile`):
|
|
65
|
+
* - append-only logs (a transcript `.jsonl`) take the CRDT G-Set union — every
|
|
66
|
+
* machine converges to byte-identical output regardless of order.
|
|
67
|
+
* - mutable blobs (Kimi `state.json`) can't be line-unioned without corruption,
|
|
68
|
+
* so they resolve **last-writer-wins**: the copy with the latest event
|
|
69
|
+
* timestamp, tie-broken by content hash so the pick is deterministic fleet-wide.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* The `lastTs` a manifest entry carries for one file. Append-only logs (a
|
|
73
|
+
* conversation `.jsonl`) embed per-line event timestamps, so their recency is
|
|
74
|
+
* the latest line timestamp (`transcriptStats`). Mutable blobs (Kimi
|
|
75
|
+
* `state.json`, the per-tool `tasks/*.json` sidecars) carry no event timestamp —
|
|
76
|
+
* their own `updatedAt`/`createdAt` fields are agent-specific and unreliable — so
|
|
77
|
+
* their "last written" signal is the file mtime. Without this, `transcriptStats`
|
|
78
|
+
* returns `''` for every blob and the last-writer-wins branch in
|
|
79
|
+
* `resolveMirrorWrite` silently degrades to "highest-hash-wins", which can pick a
|
|
80
|
+
* stale copy over the genuinely newer one.
|
|
61
81
|
*/
|
|
82
|
+
export declare function deriveLastTs(spec: SyncAgentSpec, relKey: string, content: string, mtimeMs: number): string;
|
|
62
83
|
export declare function resolveMirrorWrite(spec: SyncAgentSpec, copies: RemoteCopy[], contents: string[]): {
|
|
63
84
|
dest: string;
|
|
64
85
|
content: string;
|
|
@@ -18,10 +18,10 @@ import * as fs from 'fs';
|
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import { R2Client } from './r2.js';
|
|
20
20
|
import { loadR2Config, machineId } from './config.js';
|
|
21
|
-
import { SYNC_AGENTS, listLocalTranscripts, localSessionIds, mirrorPath, objectKey, manifestKey, SESSIONS_PREFIX, } from './agents.js';
|
|
21
|
+
import { SYNC_AGENTS, listLocalTranscripts, localSessionIds, mirrorPath, objectKey, manifestKey, isMergeableFile, SESSIONS_PREFIX, } from './agents.js';
|
|
22
22
|
import { mergeTranscripts, transcriptStats } from './crdt.js';
|
|
23
23
|
import { resolveSyncEncKey, encryptTranscript, decryptTranscriptBody } from './transcript-crypto.js';
|
|
24
|
-
import { emptyManifest, parseManifest, hashContent, loadLedger, saveLedger, ledgerUnchanged, ledgerRecord, loadLocalManifest, saveLocalManifest, loadPullState, savePullState, sourceSignature, } from './manifest.js';
|
|
24
|
+
import { emptyManifest, parseManifest, hashContent, loadLedger, saveLedger, ledgerUnchanged, ledgerRecord, loadLocalManifest, saveLocalManifest, loadPullState, savePullState, sourceSignature, manifestEntries, } from './manifest.js';
|
|
25
25
|
const nowIso = () => new Date().toISOString();
|
|
26
26
|
function specById(id) {
|
|
27
27
|
return SYNC_AGENTS.find(s => s.id === id);
|
|
@@ -42,45 +42,59 @@ async function pushOwn(r2, me, encKey, opts, result) {
|
|
|
42
42
|
for (const spec of SYNC_AGENTS) {
|
|
43
43
|
const agentManifest = {};
|
|
44
44
|
for (const t of listLocalTranscripts(spec)) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
45
|
+
// A session is one file (file-shaped) or many (dir-shaped). Push each file
|
|
46
|
+
// that changed, reusing the prior manifest entry (keyed by relKey) for the
|
|
47
|
+
// ones the ledger shows unchanged. Object keys nest under the session id for
|
|
48
|
+
// dir-shaped agents and stay flat for file-shaped ones (unchanged keys).
|
|
49
|
+
const prevVal = prev?.agents?.[spec.id]?.[t.sessionId];
|
|
50
|
+
const prevByRel = new Map((prevVal ? manifestEntries(prevVal) : []).map(e => [e.relKey, e]));
|
|
51
|
+
const entries = [];
|
|
52
|
+
for (const f of t.files) {
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.statSync(f.absPath);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const prevEntry = prevByRel.get(f.relKey);
|
|
61
|
+
if (prevEntry && ledgerUnchanged(ledger, f.absPath, stat.size, stat.mtimeMs)) {
|
|
62
|
+
entries.push(prevEntry); // unchanged: reuse, no read, no upload
|
|
63
|
+
result.pushSkipped++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
let content;
|
|
67
|
+
try {
|
|
68
|
+
content = fs.readFileSync(f.absPath, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Identity for CRDT merge is the PLAINTEXT hash (ciphertext is
|
|
74
|
+
// non-deterministic), so hash + manifest are computed on cleartext; only
|
|
75
|
+
// the stored object body is sealed.
|
|
76
|
+
const hash = hashContent(content);
|
|
77
|
+
const lastTs = deriveLastTs(spec, f.relKey, content, stat.mtimeMs);
|
|
78
|
+
const entry = { relKey: f.relKey, size: stat.size, hash, lastTs };
|
|
79
|
+
const body = encKey ? encryptTranscript(content, encKey) : content;
|
|
80
|
+
const contentType = encKey ? 'application/json' : 'application/x-ndjson';
|
|
81
|
+
try {
|
|
82
|
+
await r2.put(objectKey(me, spec.id, t.sessionId, spec.dirShaped ? f.relKey : undefined), body, contentType);
|
|
83
|
+
ledgerRecord(ledger, f.absPath, stat.size, stat.mtimeMs, hash);
|
|
84
|
+
entries.push(entry);
|
|
85
|
+
result.pushed++;
|
|
86
|
+
const label = spec.dirShaped ? `${t.sessionId.slice(0, 8)}/${f.relKey}` : t.sessionId.slice(0, 8);
|
|
87
|
+
if (opts.verbose)
|
|
88
|
+
opts.log?.(` push ${spec.id}/${label} (${stat.size}B)`);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
result.errors.push(`push ${spec.id}/${t.sessionId}/${f.relKey}: ${err.message}`);
|
|
92
|
+
}
|
|
83
93
|
}
|
|
94
|
+
// File-shaped: store the single entry (byte-identical to the old format so
|
|
95
|
+
// older CLIs read it unchanged). Dir-shaped: store the per-file array.
|
|
96
|
+
if (entries.length > 0)
|
|
97
|
+
agentManifest[t.sessionId] = spec.dirShaped ? entries : entries[0];
|
|
84
98
|
}
|
|
85
99
|
if (Object.keys(agentManifest).length > 0)
|
|
86
100
|
manifest.agents[spec.id] = agentManifest;
|
|
@@ -115,17 +129,60 @@ export function selectSessionsToFetch(copies, localIdsByAgent, pullState) {
|
|
|
115
129
|
return pending;
|
|
116
130
|
}
|
|
117
131
|
/**
|
|
118
|
-
* Resolve the mirror destination +
|
|
132
|
+
* Resolve the mirror destination + reconciled content for ONE file across its
|
|
133
|
+
* copies (every copy here is the same file — same relKey — held by a different
|
|
134
|
+
* machine). Pure.
|
|
135
|
+
*
|
|
119
136
|
* The canonical path comes from the lexicographically-smallest machine so every
|
|
120
|
-
* puller derives an identical location
|
|
137
|
+
* puller derives an identical location. The content depends on the file's kind
|
|
138
|
+
* (see `isMergeableFile`):
|
|
139
|
+
* - append-only logs (a transcript `.jsonl`) take the CRDT G-Set union — every
|
|
140
|
+
* machine converges to byte-identical output regardless of order.
|
|
141
|
+
* - mutable blobs (Kimi `state.json`) can't be line-unioned without corruption,
|
|
142
|
+
* so they resolve **last-writer-wins**: the copy with the latest event
|
|
143
|
+
* timestamp, tie-broken by content hash so the pick is deterministic fleet-wide.
|
|
121
144
|
*/
|
|
145
|
+
/**
|
|
146
|
+
* The `lastTs` a manifest entry carries for one file. Append-only logs (a
|
|
147
|
+
* conversation `.jsonl`) embed per-line event timestamps, so their recency is
|
|
148
|
+
* the latest line timestamp (`transcriptStats`). Mutable blobs (Kimi
|
|
149
|
+
* `state.json`, the per-tool `tasks/*.json` sidecars) carry no event timestamp —
|
|
150
|
+
* their own `updatedAt`/`createdAt` fields are agent-specific and unreliable — so
|
|
151
|
+
* their "last written" signal is the file mtime. Without this, `transcriptStats`
|
|
152
|
+
* returns `''` for every blob and the last-writer-wins branch in
|
|
153
|
+
* `resolveMirrorWrite` silently degrades to "highest-hash-wins", which can pick a
|
|
154
|
+
* stale copy over the genuinely newer one.
|
|
155
|
+
*/
|
|
156
|
+
export function deriveLastTs(spec, relKey, content, mtimeMs) {
|
|
157
|
+
if (isMergeableFile(spec, relKey))
|
|
158
|
+
return transcriptStats(content).lastTs;
|
|
159
|
+
return new Date(mtimeMs).toISOString();
|
|
160
|
+
}
|
|
122
161
|
export function resolveMirrorWrite(spec, copies, contents) {
|
|
123
162
|
const canonical = [...copies].sort((a, b) => (a.machine < b.machine ? -1 : a.machine > b.machine ? 1 : 0))[0];
|
|
124
|
-
|
|
163
|
+
let content;
|
|
164
|
+
if (contents.length === 1) {
|
|
165
|
+
content = contents[0];
|
|
166
|
+
}
|
|
167
|
+
else if (isMergeableFile(spec, canonical.entry.relKey)) {
|
|
168
|
+
content = mergeTranscripts(contents);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Last-writer-wins: highest (lastTs, hash) among the copies.
|
|
172
|
+
let win = 0;
|
|
173
|
+
for (let i = 1; i < copies.length; i++) {
|
|
174
|
+
const a = copies[i].entry, b = copies[win].entry;
|
|
175
|
+
if (a.lastTs > b.lastTs || (a.lastTs === b.lastTs && a.hash > b.hash))
|
|
176
|
+
win = i;
|
|
177
|
+
}
|
|
178
|
+
content = contents[win];
|
|
179
|
+
}
|
|
125
180
|
return {
|
|
126
181
|
dest: mirrorPath(spec, canonical.machine, canonical.entry.relKey),
|
|
127
182
|
content,
|
|
128
|
-
merged
|
|
183
|
+
// "merged" means an actual CRDT line-union happened — not a last-writer-wins
|
|
184
|
+
// pick of one mutable blob over another (that discards a copy, it doesn't merge).
|
|
185
|
+
merged: contents.length > 1 && isMergeableFile(spec, canonical.entry.relKey),
|
|
129
186
|
};
|
|
130
187
|
}
|
|
131
188
|
/**
|
|
@@ -176,9 +233,13 @@ async function pullAndReconcile(r2, me, encKey, opts, result) {
|
|
|
176
233
|
let byAgent = copies.get(agentId);
|
|
177
234
|
if (!byAgent)
|
|
178
235
|
copies.set(agentId, (byAgent = new Map()));
|
|
179
|
-
for (const [sessionId,
|
|
236
|
+
for (const [sessionId, value] of Object.entries(sessions)) {
|
|
180
237
|
const list = byAgent.get(sessionId) ?? [];
|
|
181
|
-
|
|
238
|
+
// One RemoteCopy per (machine, file): file-shaped sessions contribute one,
|
|
239
|
+
// dir-shaped ones contribute an entry per constituent file. `manifestEntries`
|
|
240
|
+
// also normalizes a single-object entry written by an older CLI.
|
|
241
|
+
for (const entry of manifestEntries(value))
|
|
242
|
+
list.push({ machine: m, entry });
|
|
182
243
|
byAgent.set(sessionId, list);
|
|
183
244
|
}
|
|
184
245
|
}
|
|
@@ -197,43 +258,85 @@ async function pullAndReconcile(r2, me, encKey, opts, result) {
|
|
|
197
258
|
result.pullSkipped += candidates - pending.length; // local-owned or unchanged
|
|
198
259
|
for (const { agentId, sessionId, copies: list, sig } of pending) {
|
|
199
260
|
const spec = specById(agentId);
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
const
|
|
261
|
+
// A dir-shaped session spans several files; reconcile each file independently
|
|
262
|
+
// (its copies across machines share one relKey). File-shaped sessions have a
|
|
263
|
+
// single group, so this collapses to the original one-file path.
|
|
264
|
+
const byRel = new Map();
|
|
204
265
|
for (const c of list) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
266
|
+
const g = byRel.get(c.entry.relKey);
|
|
267
|
+
if (g)
|
|
268
|
+
g.push(c);
|
|
269
|
+
else
|
|
270
|
+
byRel.set(c.entry.relKey, [c]);
|
|
271
|
+
}
|
|
272
|
+
// Resolve every file's write before touching disk. If ANY file's fetch is
|
|
273
|
+
// incomplete (a copy 404s / consistency lag), abandon the WHOLE session this
|
|
274
|
+
// tick — no writes, no pull-state stamp — so it retries intact next time
|
|
275
|
+
// rather than materializing half a session and stamping it done.
|
|
276
|
+
const writes = [];
|
|
277
|
+
let incomplete = false;
|
|
278
|
+
try {
|
|
279
|
+
for (const group of byRel.values()) {
|
|
280
|
+
const fetched = [];
|
|
281
|
+
for (const c of group) {
|
|
282
|
+
try {
|
|
283
|
+
const body = await r2.get(objectKey(c.machine, agentId, sessionId, spec.dirShaped ? c.entry.relKey : undefined));
|
|
284
|
+
// Decrypt before the body reaches the CRDT union — merge + mirror always
|
|
285
|
+
// operate on plaintext. A legacy plaintext object passes through
|
|
286
|
+
// untouched; an envelope without a key throws (surfaced below).
|
|
287
|
+
fetched.push(body === null ? null : decryptTranscriptBody(body, encKey));
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
result.errors.push(`get ${c.machine}/${sessionId}/${c.entry.relKey}: ${err.message}`);
|
|
291
|
+
fetched.push(null);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const resolved = reconcileCopies(spec, group, fetched);
|
|
295
|
+
if (!resolved) {
|
|
296
|
+
incomplete = true;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
writes.push(resolved);
|
|
215
300
|
}
|
|
216
301
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
302
|
+
catch (err) {
|
|
303
|
+
// `reconcileCopies` -> `mirrorPath` now rejects unsafe peer-controlled
|
|
304
|
+
// machine/relKey (C1 containment). That rejection must stay scoped to this
|
|
305
|
+
// one session: without this catch a single malicious/malformed manifest
|
|
306
|
+
// entry would throw out of the whole `pending` loop, skip the
|
|
307
|
+
// `savePullState` below, and re-throw every tick — a peer-triggered DoS on
|
|
308
|
+
// everyone else's session-sync. Record it and skip; never stamp pull-state
|
|
309
|
+
// for a rejected entry (so nothing marks the bad session "done").
|
|
310
|
+
result.errors.push(`resolve mirror ${agentId}/${sessionId}: ${err.message}`);
|
|
221
311
|
continue;
|
|
222
|
-
|
|
312
|
+
}
|
|
313
|
+
if (incomplete)
|
|
314
|
+
continue; // retry next tick, session intact
|
|
223
315
|
try {
|
|
224
|
-
let
|
|
225
|
-
|
|
226
|
-
|
|
316
|
+
let changed = false;
|
|
317
|
+
let mergedAny = false;
|
|
318
|
+
for (const { dest, content, merged } of writes) {
|
|
319
|
+
let existing = null;
|
|
320
|
+
try {
|
|
321
|
+
existing = fs.readFileSync(dest, 'utf-8');
|
|
322
|
+
}
|
|
323
|
+
catch { /* not present yet */ }
|
|
324
|
+
if (existing !== content) {
|
|
325
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
326
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
327
|
+
changed = true;
|
|
328
|
+
if (merged)
|
|
329
|
+
mergedAny = true;
|
|
330
|
+
}
|
|
227
331
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
231
|
-
fs.writeFileSync(dest, content, 'utf-8');
|
|
232
|
-
if (merged)
|
|
332
|
+
if (changed) {
|
|
333
|
+
if (mergedAny)
|
|
233
334
|
result.merged++;
|
|
234
335
|
result.pulled++;
|
|
235
336
|
if (opts.verbose) {
|
|
236
|
-
|
|
337
|
+
const machines = [...new Set(list.map(c => c.machine))].join('+');
|
|
338
|
+
const files = byRel.size > 1 ? ` (${byRel.size} files)` : '';
|
|
339
|
+
opts.log?.(` pull ${agentId}/${sessionId.slice(0, 8)}${files} <- ${machines}`);
|
|
237
340
|
}
|
|
238
341
|
}
|
|
239
342
|
else {
|
|
@@ -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
|
package/dist/lib/ssh-tunnel.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export declare const REMOTE_HELPER_PORT = 8765;
|
|
|
49
49
|
export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
|
|
50
50
|
/** Basename of the cross-published exe under native/computer-win/dist. */
|
|
51
51
|
export declare const WIN_HELPER_EXE = "computer-helper-win.exe";
|
|
52
|
+
/** Basename of the daemon's shared-secret token file under %LOCALAPPDATA%\agents. */
|
|
53
|
+
export declare const WIN_HELPER_TOKEN_FILE = "helper-token";
|
|
52
54
|
/**
|
|
53
55
|
* Locate a locally built Windows daemon exe — a repo-checkout build output from
|
|
54
56
|
* `scripts/build-win.sh`, or one bundled next to the package. Local paths are
|
|
@@ -101,6 +103,16 @@ export declare function remoteStatePath(device: string): string;
|
|
|
101
103
|
export declare function readRemoteState(device: string): RemoteTunnelState | null;
|
|
102
104
|
export declare function writeRemoteState(state: RemoteTunnelState): void;
|
|
103
105
|
export declare function clearRemoteState(device: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Local file holding the shared-secret token for a device's helper daemon.
|
|
108
|
+
* Written at `setup --host` (0600), read at `start --host` so the RPC client
|
|
109
|
+
* authenticates. The remote daemon reads the same value from its own
|
|
110
|
+
* `--token-file`; the CLI is the source of truth and never round-trips it back.
|
|
111
|
+
*/
|
|
112
|
+
export declare function helperTokenPath(device: string): string;
|
|
113
|
+
export declare function readHelperToken(device: string): string | null;
|
|
114
|
+
export declare function writeHelperToken(device: string, token: string): void;
|
|
115
|
+
export declare function clearHelperToken(device: string): void;
|
|
104
116
|
/** Resolve a registered device to its ssh pieces, or throw a clear error. */
|
|
105
117
|
export declare function resolveRemoteDevice(name: string): Promise<{
|
|
106
118
|
device: DeviceProfile;
|
|
@@ -123,7 +135,13 @@ export declare function buildVerifyPushScript(remotePath: string, expectedBytes:
|
|
|
123
135
|
* survives ssh disconnect — the same rationale as the browser WMI launch. The
|
|
124
136
|
* task is started immediately so the caller need not log out/in.
|
|
125
137
|
*/
|
|
126
|
-
|
|
138
|
+
/**
|
|
139
|
+
* PowerShell that writes the shared-secret token under %LOCALAPPDATA%\agents
|
|
140
|
+
* with an owner-only ACL (inheritance removed, read granted only to the current
|
|
141
|
+
* user) and echoes the resolved path so the caller can pass it to `--token-file`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function buildWriteTokenScript(token: string): string;
|
|
144
|
+
export declare function buildRegisterTaskScript(port: number, taskName: string, tokenPath: string): string;
|
|
127
145
|
/** PowerShell that unregisters the task and stops any running daemon process. */
|
|
128
146
|
export declare function buildUnregisterTaskScript(taskName: string): string;
|
|
129
147
|
/** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
|