@phnx-labs/agents-cli 1.20.29 → 1.20.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/commands/computer-actions.js +6 -2
  2. package/dist/commands/computer.d.ts +12 -0
  3. package/dist/commands/computer.js +88 -13
  4. package/dist/commands/inspect.js +1 -1
  5. package/dist/commands/models.js +8 -2
  6. package/dist/commands/sessions.js +156 -44
  7. package/dist/commands/sync.js +70 -14
  8. package/dist/lib/agents.d.ts +0 -4
  9. package/dist/lib/agents.js +54 -5
  10. package/dist/lib/browser/drivers/ssh.js +4 -35
  11. package/dist/lib/computer-rpc.d.ts +6 -1
  12. package/dist/lib/computer-rpc.js +86 -3
  13. package/dist/lib/exec.js +14 -0
  14. package/dist/lib/models.js +138 -5
  15. package/dist/lib/runner.js +7 -7
  16. package/dist/lib/session/active.d.ts +13 -0
  17. package/dist/lib/session/active.js +79 -18
  18. package/dist/lib/session/cloud.js +2 -0
  19. package/dist/lib/session/db.d.ts +11 -0
  20. package/dist/lib/session/db.js +62 -5
  21. package/dist/lib/session/discover.d.ts +5 -0
  22. package/dist/lib/session/discover.js +81 -0
  23. package/dist/lib/session/parse.d.ts +15 -0
  24. package/dist/lib/session/parse.js +22 -2
  25. package/dist/lib/session/remote.d.ts +1 -1
  26. package/dist/lib/session/remote.js +8 -3
  27. package/dist/lib/session/state.d.ts +82 -0
  28. package/dist/lib/session/state.js +221 -0
  29. package/dist/lib/session/tail.d.ts +18 -0
  30. package/dist/lib/session/tail.js +57 -0
  31. package/dist/lib/session/types.d.ts +9 -0
  32. package/dist/lib/session/width.d.ts +29 -0
  33. package/dist/lib/session/width.js +91 -0
  34. package/dist/lib/shims.d.ts +17 -1
  35. package/dist/lib/shims.js +130 -6
  36. package/dist/lib/ssh-tunnel.d.ts +127 -0
  37. package/dist/lib/ssh-tunnel.js +346 -0
  38. package/dist/lib/state.d.ts +2 -0
  39. package/dist/lib/state.js +17 -1
  40. package/dist/lib/teams/agents.d.ts +11 -1
  41. package/dist/lib/teams/agents.js +16 -2
  42. package/dist/lib/types.d.ts +1 -0
  43. package/dist/lib/versions.d.ts +19 -0
  44. package/dist/lib/versions.js +84 -24
  45. package/package.json +1 -1
@@ -63,6 +63,15 @@ export interface SessionMeta {
63
63
  label?: string;
64
64
  /** Set when this session was spawned by `agents teams`. */
65
65
  teamOrigin?: TeamOrigin;
66
+ /** Durable state signals extracted at scan time by the session-state engine. */
67
+ /** PR URL, if the session opened one (`gh pr create`). */
68
+ prUrl?: string;
69
+ /** PR number parsed from prUrl, for compact display. */
70
+ prNumber?: number;
71
+ /** Worktree slug when cwd is under `.agents/worktrees/<slug>/`. */
72
+ worktreeSlug?: string;
73
+ /** Tracker ticket ref (e.g. RUSH-1234) from the prompt or branch. */
74
+ ticketId?: string;
66
75
  /**
67
76
  * True when the session was spawned programmatically (SDK entrypoint) rather
68
77
  * than by a human at the Claude CLI. Captured at scan time from the JSONL
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Terminal display-width helpers.
3
+ *
4
+ * `String.length` is the wrong ruler for a terminal: it over-counts ANSI colour
5
+ * escapes (chalk output) and under-counts wide glyphs (CJK, emoji) which occupy
6
+ * two cells. The result is the drifting, wrapping session-table line users see
7
+ * under tmux and over `--host` SSH. Every renderer that sizes a session-table
8
+ * cell measures and truncates through this module so alignment is computed once,
9
+ * correctly, from the same source of truth.
10
+ */
11
+ /** Strip SGR colour escapes so width is measured on visible characters only. */
12
+ export declare function stripAnsi(s: string): string;
13
+ /** Visible display width of a string, ANSI-aware and wide-char-aware. */
14
+ export declare function stringWidth(s: string): number;
15
+ /**
16
+ * Truncate to a target display width, appending '…' when shortened. Operates on
17
+ * the visible (ANSI-stripped) string; callers colour the result afterwards so
18
+ * the ellipsis is never inserted mid-escape.
19
+ */
20
+ export declare function truncateToWidth(s: string, max: number): string;
21
+ /** Right-pad with spaces to a target display width. Never truncates. */
22
+ export declare function padToWidth(s: string, width: number): string;
23
+ /**
24
+ * Effective terminal width. Reads `$COLUMNS` first so it survives tmux and
25
+ * `--host` SSH (where `process.stdout.columns` is unset or wrong), falls back to
26
+ * the TTY's reported width, then to `fallback`. Clamped to a sane band so a
27
+ * bogus value can't produce a 0-wide or absurdly long table.
28
+ */
29
+ export declare function terminalWidth(fallback?: number): number;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Terminal display-width helpers.
3
+ *
4
+ * `String.length` is the wrong ruler for a terminal: it over-counts ANSI colour
5
+ * escapes (chalk output) and under-counts wide glyphs (CJK, emoji) which occupy
6
+ * two cells. The result is the drifting, wrapping session-table line users see
7
+ * under tmux and over `--host` SSH. Every renderer that sizes a session-table
8
+ * cell measures and truncates through this module so alignment is computed once,
9
+ * correctly, from the same source of truth.
10
+ */
11
+ /** SGR colour sequences emitted by chalk (e.g. `\x1b[32m`). */
12
+ const SGR_REGEX = /\x1b\[[0-9;]*m/g;
13
+ /** Strip SGR colour escapes so width is measured on visible characters only. */
14
+ export function stripAnsi(s) {
15
+ return s.replace(SGR_REGEX, '');
16
+ }
17
+ /**
18
+ * Display cells for one code point: 0 for zero-width combining/ZWJ/variation
19
+ * selectors, 2 for East-Asian-wide and emoji ranges, 1 otherwise. Compact and
20
+ * dependency-free — covers the glyphs that actually show up in prompts/titles.
21
+ */
22
+ function charWidth(cp) {
23
+ if (cp === 0)
24
+ return 0;
25
+ // Zero-width: combining marks, zero-width joiner, variation selectors.
26
+ if ((cp >= 0x0300 && cp <= 0x036f) ||
27
+ cp === 0x200b || cp === 0x200d ||
28
+ (cp >= 0xfe00 && cp <= 0xfe0f))
29
+ return 0;
30
+ // Wide (2 cells): CJK, Hangul, fullwidth forms, emoji & pictographs.
31
+ if ((cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
32
+ (cp >= 0x2e80 && cp <= 0xa4cf) || // CJK radicals … Yi
33
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
34
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK compatibility ideographs
35
+ (cp >= 0xfe30 && cp <= 0xfe4f) || // CJK compatibility forms
36
+ (cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
37
+ (cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth signs
38
+ (cp >= 0x1f300 && cp <= 0x1faff) || // emoji & symbols
39
+ (cp >= 0x20000 && cp <= 0x3fffd) // CJK Ext-B and beyond
40
+ )
41
+ return 2;
42
+ return 1;
43
+ }
44
+ /** Visible display width of a string, ANSI-aware and wide-char-aware. */
45
+ export function stringWidth(s) {
46
+ const plain = stripAnsi(s);
47
+ let w = 0;
48
+ for (const ch of plain)
49
+ w += charWidth(ch.codePointAt(0));
50
+ return w;
51
+ }
52
+ /**
53
+ * Truncate to a target display width, appending '…' when shortened. Operates on
54
+ * the visible (ANSI-stripped) string; callers colour the result afterwards so
55
+ * the ellipsis is never inserted mid-escape.
56
+ */
57
+ export function truncateToWidth(s, max) {
58
+ if (max <= 0)
59
+ return '';
60
+ const plain = stripAnsi(s);
61
+ if (stringWidth(plain) <= max)
62
+ return plain;
63
+ let w = 0;
64
+ let out = '';
65
+ for (const ch of plain) {
66
+ const cw = charWidth(ch.codePointAt(0));
67
+ if (w + cw > max - 1)
68
+ break; // reserve one cell for the ellipsis
69
+ out += ch;
70
+ w += cw;
71
+ }
72
+ return out + '…';
73
+ }
74
+ /** Right-pad with spaces to a target display width. Never truncates. */
75
+ export function padToWidth(s, width) {
76
+ const pad = width - stringWidth(s);
77
+ return pad > 0 ? s + ' '.repeat(pad) : s;
78
+ }
79
+ /**
80
+ * Effective terminal width. Reads `$COLUMNS` first so it survives tmux and
81
+ * `--host` SSH (where `process.stdout.columns` is unset or wrong), falls back to
82
+ * the TTY's reported width, then to `fallback`. Clamped to a sane band so a
83
+ * bogus value can't produce a 0-wide or absurdly long table.
84
+ */
85
+ export function terminalWidth(fallback = 100) {
86
+ const env = Number.parseInt(process.env.COLUMNS ?? '', 10);
87
+ const raw = Number.isFinite(env) && env > 0
88
+ ? env
89
+ : (process.stdout.columns || fallback);
90
+ return Math.max(60, Math.min(200, raw));
91
+ }
@@ -108,8 +108,14 @@ export declare function removeShim(agent: AgentId): boolean;
108
108
  * v5 — hard-disable Codex startup update checks in versioned aliases.
109
109
  * v6 — versions moved from ~/.agents-system/versions to ~/.agents/versions
110
110
  * (two-repo split: system = shipped defaults, user = operational state).
111
+ * v7 — runtime state split into ~/.agents/.history and ~/.agents/.cache.
112
+ * v8 — resolve grok/kimi/droid binaries from their real install locations
113
+ * (~/.grok/downloads, ~/.kimi-code/bin, ~/.local/bin) instead of the
114
+ * hardcoded node_modules/.bin, which never exists for these three and
115
+ * made every versioned alias (the path `agents teams` pins to) fail
116
+ * with "<agent>@<version> not installed". Also emit GROK_HOME.
111
117
  */
112
- export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 7;
118
+ export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 8;
113
119
  /**
114
120
  * Generate a versioned alias script that directly execs a specific version.
115
121
  * e.g., claude@2.0.65 -> directly runs that version's binary
@@ -159,6 +165,16 @@ export declare function versionedAliasExists(agent: AgentId, version: string): b
159
165
  *
160
166
  * Returns: { success: boolean, backupPath?: string, error?: string }
161
167
  */
168
+ /**
169
+ * Seed a version's config home with the account credential so switching versions
170
+ * doesn't log the CLI out. Droid/antigravity/kimi (registry `authFiles`) store
171
+ * login as files inside the per-version config dir; sign-in is account-global,
172
+ * so we copy the FRESHEST existing copy (by mtime, across all installed version
173
+ * homes) into `toConfigDir` when its copy is missing or older. mtime is
174
+ * preserved so the "freshest" comparison stays stable and switches don't
175
+ * ping-pong. Best-effort: a failed copy just means the user re-logs in.
176
+ */
177
+ export declare function carryForwardAuthFiles(agent: AgentId, toConfigDir: string): void;
162
178
  export declare function switchConfigSymlink(agent: AgentId, version: string): Promise<{
163
179
  success: boolean;
164
180
  backupPath?: string;
package/dist/lib/shims.js CHANGED
@@ -16,7 +16,7 @@ import { confirm, select } from '@inquirer/prompts';
16
16
  import { IS_WINDOWS, prependToWindowsUserPath } from './platform/index.js';
17
17
  import { getShimsDir, getVersionsDir, getBackupsDir, ensureAgentsDir } from './state.js';
18
18
  export { getShimsDir };
19
- import { AGENTS } from './agents.js';
19
+ import { AGENTS, agentConfigDirName } from './agents.js';
20
20
  /**
21
21
  * Files and directories to always skip during conflict detection and migration.
22
22
  * These are never user config that should be migrated.
@@ -596,8 +596,14 @@ export function removeShim(agent) {
596
596
  * v5 — hard-disable Codex startup update checks in versioned aliases.
597
597
  * v6 — versions moved from ~/.agents-system/versions to ~/.agents/versions
598
598
  * (two-repo split: system = shipped defaults, user = operational state).
599
+ * v7 — runtime state split into ~/.agents/.history and ~/.agents/.cache.
600
+ * v8 — resolve grok/kimi/droid binaries from their real install locations
601
+ * (~/.grok/downloads, ~/.kimi-code/bin, ~/.local/bin) instead of the
602
+ * hardcoded node_modules/.bin, which never exists for these three and
603
+ * made every versioned alias (the path `agents teams` pins to) fail
604
+ * with "<agent>@<version> not installed". Also emit GROK_HOME.
599
605
  */
600
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 7;
606
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 8;
601
607
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
602
608
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
603
609
  // The version string is interpolated into a generated bash script and into
@@ -640,22 +646,70 @@ export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${co
640
646
  # version MCP and session state are isolated.
641
647
  export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
642
648
  `
643
- : agent === 'kimi'
649
+ : agent === 'grok'
644
650
  ? `
651
+ # Grok Build uses GROK_HOME to isolate its entire configuration tree (skills,
652
+ # hooks, plugins, agents, memory, sessions, config.toml, MCP). Point direct
653
+ # aliases at the versioned home for isolation parity with the main shim.
654
+ export GROK_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
655
+ `
656
+ : agent === 'kimi'
657
+ ? `
645
658
  # Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
646
659
  # mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
647
660
  export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
648
661
  `
649
- : '';
662
+ : '';
650
663
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
664
+ // Resolve the binary the same way the main shim does (see generateShimScript).
665
+ // Grok, Kimi, and Droid do NOT ship into node_modules/.bin — Grok downloads a
666
+ // native binary to ~/.grok/downloads, Kimi to ~/.kimi-code/bin, and Droid
667
+ // (Factory AI) installs a standalone binary to ~/.local/bin. Hardcoding the
668
+ // node_modules path made every versioned alias for these three fail with
669
+ // "<agent>@<version> not installed", which is exactly the path `agents teams`
670
+ // takes once it pins a teammate's version.
671
+ // This template is unix-only — on Windows the .cmd companion delegates to
672
+ // "agents __shim" which resolves via getBinaryPath() instead.
673
+ const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
674
+ const binaryResolution = agent === 'grok'
675
+ ? `# Grok ships its native binary in ~/.grok/downloads, not node_modules.
676
+ GROK_DOWNLOADS="$HOME/.grok/downloads"
677
+ BINARY=""
678
+ if [ -d "$GROK_DOWNLOADS" ]; then
679
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
680
+ [ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
681
+ fi
682
+ [ -n "$BINARY" ] && [ -x "$BINARY" ] || BINARY=$(command -v grok 2>/dev/null || echo "")`
683
+ : agent === 'kimi'
684
+ ? `# Kimi ships its binary in ~/.kimi-code/bin, not node_modules.
685
+ KIMI_BINARY="$HOME/.kimi-code/bin/kimi"
686
+ if [ -x "$KIMI_BINARY" ]; then
687
+ BINARY="$KIMI_BINARY"
688
+ else
689
+ BINARY=$(command -v kimi 2>/dev/null || echo "")
690
+ fi`
691
+ : agent === 'droid'
692
+ ? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
693
+ # there is no npm package and nothing lands in node_modules/.bin. The PATH
694
+ # fallback refuses anything under our shims dir to avoid an infinite re-exec.
695
+ DROID_BINARY="$HOME/.local/bin/droid"
696
+ if [ -x "$DROID_BINARY" ]; then
697
+ BINARY="$DROID_BINARY"
698
+ else
699
+ BINARY=$(command -v droid 2>/dev/null || echo "")
700
+ case "$BINARY" in
701
+ "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
702
+ esac
703
+ fi`
704
+ : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
651
705
  return `#!/bin/bash
652
706
  # Auto-generated by agents-cli - do not edit
653
707
  # ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
654
708
  # Direct alias for ${agentConfig.name}@${version}
655
709
 
656
- BINARY="$HOME/.agents/.history/versions/${agent}/${version}/node_modules/.bin/${agentConfig.cliCommand}"
710
+ ${binaryResolution}
657
711
 
658
- if [ ! -x "$BINARY" ]; then
712
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
659
713
  echo "agents: ${agent}@${version} not installed" >&2
660
714
  exit 1
661
715
  fi
@@ -831,6 +885,70 @@ function detectMigrationConflicts(agent, version) {
831
885
  *
832
886
  * Returns: { success: boolean, backupPath?: string, error?: string }
833
887
  */
888
+ /**
889
+ * Seed a version's config home with the account credential so switching versions
890
+ * doesn't log the CLI out. Droid/antigravity/kimi (registry `authFiles`) store
891
+ * login as files inside the per-version config dir; sign-in is account-global,
892
+ * so we copy the FRESHEST existing copy (by mtime, across all installed version
893
+ * homes) into `toConfigDir` when its copy is missing or older. mtime is
894
+ * preserved so the "freshest" comparison stays stable and switches don't
895
+ * ping-pong. Best-effort: a failed copy just means the user re-logs in.
896
+ */
897
+ export function carryForwardAuthFiles(agent, toConfigDir) {
898
+ const authFiles = AGENTS[agent].authFiles;
899
+ if (!authFiles || authFiles.length === 0)
900
+ return;
901
+ const configDirName = agentConfigDirName(agent);
902
+ const versionsBase = path.join(getVersionsDir(), agent);
903
+ let sourceDirs = [];
904
+ try {
905
+ sourceDirs = fs
906
+ .readdirSync(versionsBase)
907
+ .map(v => path.join(versionsBase, v, 'home', configDirName));
908
+ }
909
+ catch {
910
+ return; // no installed versions to source from
911
+ }
912
+ for (const rel of authFiles) {
913
+ const dest = path.join(toConfigDir, rel);
914
+ const destResolved = path.resolve(dest);
915
+ // Newest existing source copy across all version homes (excluding dest).
916
+ let newest = null;
917
+ for (const dir of sourceDirs) {
918
+ const src = path.join(dir, rel);
919
+ if (path.resolve(src) === destResolved)
920
+ continue;
921
+ let st;
922
+ try {
923
+ st = fs.statSync(src);
924
+ }
925
+ catch {
926
+ continue;
927
+ }
928
+ if (!st.isFile())
929
+ continue;
930
+ if (!newest || st.mtimeMs > newest.mtimeMs)
931
+ newest = { path: src, mtimeMs: st.mtimeMs };
932
+ }
933
+ if (!newest)
934
+ continue;
935
+ // Skip when the target already has an at-least-as-fresh copy.
936
+ try {
937
+ const dstat = fs.statSync(dest);
938
+ if (dstat.mtimeMs >= newest.mtimeMs)
939
+ continue;
940
+ }
941
+ catch { /* dest missing — copy below */ }
942
+ try {
943
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
944
+ const srcStat = fs.statSync(newest.path);
945
+ fs.copyFileSync(newest.path, dest);
946
+ fs.chmodSync(dest, (srcStat.mode & 0o777) || 0o600);
947
+ fs.utimesSync(dest, srcStat.atime, srcStat.mtime);
948
+ }
949
+ catch { /* best-effort; a failed carry just means a re-login */ }
950
+ }
951
+ }
834
952
  export async function switchConfigSymlink(agent, version) {
835
953
  const configPath = getAgentConfigPath(agent);
836
954
  const versionConfigPath = getVersionConfigPath(agent, version);
@@ -838,6 +956,12 @@ export async function switchConfigSymlink(agent, version) {
838
956
  if (!fs.existsSync(versionConfigPath)) {
839
957
  fs.mkdirSync(versionConfigPath, { recursive: true });
840
958
  }
959
+ // Carry the account credential into the version we're switching to. Droid /
960
+ // antigravity / kimi store login as files INSIDE the per-version config home;
961
+ // switching versions repoints the symlink to a home that was never logged in,
962
+ // silently logging the CLI out. Sign-in is account-global, so seed the target
963
+ // home with the freshest existing credential before we flip the symlink.
964
+ carryForwardAuthFiles(agent, versionConfigPath);
841
965
  try {
842
966
  const stat = fs.lstatSync(configPath);
843
967
  if (stat.isSymbolicLink()) {
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Shared SSH port-forward tunnel + remote computer-helper provisioning.
3
+ *
4
+ * Two layers live here:
5
+ *
6
+ * 1. `startSSHTunnel` — the generic `ssh -L localPort:127.0.0.1:remotePort -N`
7
+ * spawn, extracted verbatim from the browser CDP driver so both the browser
8
+ * and `agents computer --host` reach a remote loopback service through one
9
+ * hardened tunnel. Behavior for the browser caller is unchanged (default,
10
+ * foreground, stderr-captured).
11
+ *
12
+ * 2. Remote computer-helper orchestration — resolve a registered device to an
13
+ * ssh target, push the cross-published Windows daemon exe, register it as a
14
+ * LOGON scheduled task (interactive session so real-desktop UIA/screenshot
15
+ * works and it survives the ssh disconnect), and open a tunnel the TS RPC
16
+ * client drives via TCP. Everything rides the existing `ssh-exec` /
17
+ * `devices/connect` primitives — no parallel SSH implementation.
18
+ */
19
+ import { type ChildProcess } from 'child_process';
20
+ import { type DeviceProfile } from './devices/registry.js';
21
+ export interface StartTunnelOptions {
22
+ /**
23
+ * Detach the tunnel so it OUTLIVES this CLI process. Used by
24
+ * `agents computer start --host` — the tunnel must persist across separate
25
+ * verb invocations (`apps`, `click`, …) until `stop --host` tears it down.
26
+ * The browser driver leaves this false: it holds the tunnel for the lifetime
27
+ * of one CDP session and kills it on cleanup.
28
+ */
29
+ detached?: boolean;
30
+ }
31
+ /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure. */
32
+ export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number): string[];
33
+ /**
34
+ * Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
35
+ *
36
+ * Foreground (default): stderr is captured so a tunnel that dies inside 500ms
37
+ * rejects with the ssh error — the browser driver's original contract. Detached
38
+ * mode ignores stdio and `unref`s the child so the parent can exit while the
39
+ * tunnel lives; liveness is then confirmed by the caller probing the service.
40
+ */
41
+ export declare function startSSHTunnel(user: string, host: string, localPort: number, remotePort: number, opts?: StartTunnelOptions): Promise<ChildProcess>;
42
+ /** Loopback TCP port the Windows daemon binds on the remote (Program.cs default). */
43
+ export declare const REMOTE_HELPER_PORT = 8765;
44
+ /** Task Scheduler task name for the daemon. Stable so setup/stop pair up. */
45
+ export declare const REMOTE_TASK_NAME = "AgentsComputerHelper";
46
+ /** Basename of the cross-published exe under packages/computer-helper-win/dist. */
47
+ export declare const WIN_HELPER_EXE = "computer-helper-win.exe";
48
+ /**
49
+ * Locate the cross-published Windows daemon exe. Only the local build output is
50
+ * a candidate — `scripts/build-win.sh` writes it to packages/.../dist/.
51
+ */
52
+ export declare function resolveWinHelperExe(): string | null;
53
+ /** Persisted per-device tunnel state so verbs can reconnect after `start --host`. */
54
+ export interface RemoteTunnelState {
55
+ device: string;
56
+ target: string;
57
+ localPort: number;
58
+ remotePort: number;
59
+ tunnelPid: number;
60
+ token: string | null;
61
+ taskName: string;
62
+ startedAt: number;
63
+ }
64
+ /** State file path for a device. Device names are ssh-alias safe (validated). */
65
+ export declare function remoteStatePath(device: string): string;
66
+ export declare function readRemoteState(device: string): RemoteTunnelState | null;
67
+ export declare function writeRemoteState(state: RemoteTunnelState): void;
68
+ export declare function clearRemoteState(device: string): void;
69
+ /** Resolve a registered device to its ssh pieces, or throw a clear error. */
70
+ export declare function resolveRemoteDevice(name: string): Promise<{
71
+ device: DeviceProfile;
72
+ target: string;
73
+ user: string;
74
+ host: string;
75
+ }>;
76
+ /**
77
+ * PowerShell that streams base64 from stdin, decodes it incrementally to
78
+ * %LOCALAPPDATA%\agents\computer-helper-win.exe, and stops any running instance
79
+ * first so the file isn't locked. The CryptoStream/FromBase64Transform decode
80
+ * is streaming — the ~156MB exe never lands in memory whole on the remote.
81
+ */
82
+ export declare function buildPushScript(): string;
83
+ /**
84
+ * PowerShell that registers the daemon as a LOGON scheduled task. Interactive
85
+ * logon type + Highest run level so the daemon runs in the real desktop session
86
+ * (UIAutomation and ScreenCapture need a live session, not Session 0) and
87
+ * survives ssh disconnect — the same rationale as the browser WMI launch. The
88
+ * task is started immediately so the caller need not log out/in.
89
+ */
90
+ export declare function buildRegisterTaskScript(port: number, taskName: string): string;
91
+ /** PowerShell that unregisters the task and stops any running daemon process. */
92
+ export declare function buildUnregisterTaskScript(taskName: string): string;
93
+ /**
94
+ * `setup --host`: push the exe, then register + start the LOGON task. Both hops
95
+ * go through `sshExec` (BatchMode key auth — the same hardening the browser
96
+ * driver and `agents ssh` use). Throws with the remote stderr on any failure.
97
+ */
98
+ export declare function setupRemoteHelper(name: string): Promise<{
99
+ target: string;
100
+ taskName: string;
101
+ }>;
102
+ /** Reserve a free local TCP port by binding :0 and reading the assigned port. */
103
+ export declare function pickFreePort(): Promise<number>;
104
+ /**
105
+ * `start --host`: open a detached ssh -L tunnel to the remote daemon, verify it
106
+ * answers over TCP, and persist the tunnel state so verbs can reconnect. Returns
107
+ * the state (and leaves the tunnel running in the background).
108
+ */
109
+ export declare function startRemoteTunnel(name: string): Promise<RemoteTunnelState>;
110
+ /**
111
+ * `stop --host`: kill the local tunnel, unregister the remote task (best-effort
112
+ * — the box may be offline), and clear the persisted state.
113
+ */
114
+ export declare function stopRemoteHelper(name: string): Promise<{
115
+ tunnelKilled: boolean;
116
+ taskRemoved: boolean;
117
+ }>;
118
+ /**
119
+ * Point this process's RPC client at a device's live tunnel by setting
120
+ * COMPUTER_HELPER_TCP / COMPUTER_HELPER_TOKEN from persisted state. Called for
121
+ * remote verbs (`apps --host`, `click --host`, …) so the shared
122
+ * openComputerClient() transparently selects the TcpClient transport — no
123
+ * per-verb wiring. Exits with guidance when there is no active tunnel.
124
+ */
125
+ export declare function hydrateRemoteEnvFromState(name: string): void;
126
+ /** Generate a shared-secret token (reserved for token-file provisioning). */
127
+ export declare function generateToken(): string;