@phnx-labs/agents-cli 1.20.34 → 1.20.36

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 (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Terminal injection — Gap 2 of the Terminal Engine.
3
+ *
4
+ * Where the rest of the engine OPENS a surface (a tab / split running a
5
+ * command), injection types into an ALREADY-running surface. It's the primitive
6
+ * a native watchdog (RUSH-1415) needs to nudge a stalled agent with "continue"
7
+ * delivered into the exact terminal that agent lives in — the gap flagged in
8
+ * src/lib/session/provenance.ts:20:
9
+ *
10
+ * > Actually delivering the keystrokes is Gap 2 (pty/tmux send-keys).
11
+ *
12
+ * It mirrors the engine's shape exactly: pure per-backend spec builders (like
13
+ * `tmuxTabArgv` / `itermTabScript`) produce a `LaunchSpec` (argv), and the same
14
+ * `runSpec` transport runs it — so injection inherits the engine's local/remote
15
+ * (`--host` over SSH) execution for free. A `LaunchSpec` never opens anything;
16
+ * building one is side-effect-free and unit-testable without a display.
17
+ *
18
+ * Backends (each addresses the EXACT split, not the frontmost surface):
19
+ * - tmux → `tmux send-keys -t <pane>` — the send-keys primitive addressed by
20
+ * pane id + socket (the exact rail provenance.ts identifies).
21
+ * - iterm (macOS) → AppleScript `tell session id "<uuid>" to write text` —
22
+ * addresses the precise iTerm2 split by its session UUID WITHOUT
23
+ * `activate`, so it never steals focus or types into the wrong split.
24
+ * Uses the engine's `appleScriptStr` escaper; guarded by the iterm
25
+ * backend's `isAvailable` (platform + app installed).
26
+ * - vscodium (VSCodium / Cursor / VS Code) → the editor CLI's `--open-url`
27
+ * into the swarmify `swarm-ext` extension's `/inject` verb, targeting
28
+ * a live-terminals.json terminal by id. Focus-independent, exact
29
+ * terminal, and (like the launch backend, src/lib/terminal/backends/
30
+ * vscodium-agent.ts) works over `--host` SSH and on Linux.
31
+ * - ghostty (macOS) → COARSE only: Ghostty has no scripting dictionary, so
32
+ * there is no per-split addressing — this raises a window and types
33
+ * via System Events keystrokes, stealing focus. The resolver refuses
34
+ * to route here by default (see resolve.ts); it stays behind an
35
+ * explicit opt-in.
36
+ * - pty → the `agents pty write` sidecar path (ptyRequest). Local-only —
37
+ * the sidecar is not an engine transport surface.
38
+ *
39
+ * Ink-TUI Enter semantics: Claude's Ink TUI swallows an Enter fused to the text,
40
+ * so the default path delivers the text and the Enter as TWO SEPARATE writes
41
+ * (swarmify's `sendText(text,false)` then `sendText('\r',false)`). `combined`
42
+ * opts into one fused write for plain shells / REPLs.
43
+ */
44
+ import { ptyRequest } from '../pty-client.js';
45
+ import { appleScriptStr } from './quote.js';
46
+ import { runSpec } from './transport.js';
47
+ import { itermBackend, ghosttyBackend } from './backends/index.js';
48
+ import { currentContext } from './types.js';
49
+ /** Carriage return — what Enter delivers into a raw PTY/tmux byte stream. */
50
+ const CR = '\r';
51
+ // --- tmux -------------------------------------------------------------------
52
+ /**
53
+ * argv for `tmux send-keys` targeting a pane by id. Starts with `tmux` (like the
54
+ * engine's `tmuxTabArgv`) so the same transport runs it locally or over SSH. The
55
+ * socket, when set, is positioned before the subcommand (`-S` must lead). `-l`
56
+ * sends the keys literally; without it tmux interprets a key name like `Enter`.
57
+ *
58
+ * NOTE: distinct from `sendKeys()` in src/lib/tmux/session.ts — that one
59
+ * addresses by session *name* on the default socket (the `agents tmux` surface)
60
+ * and shells out directly; this one addresses by *pane id* + arbitrary socket and
61
+ * returns an SSH-capable `LaunchSpec` for the engine transport. Don't collapse them.
62
+ */
63
+ export function tmuxSendKeysArgv(pane, keys, opts = {}) {
64
+ const argv = ['tmux'];
65
+ if (opts.socket)
66
+ argv.push('-S', opts.socket);
67
+ argv.push('send-keys', '-t', pane);
68
+ if (opts.literal)
69
+ argv.push('-l');
70
+ argv.push(keys);
71
+ return argv;
72
+ }
73
+ /** The one-or-two send-keys specs for a tmux injection (Ink-safe split by default). */
74
+ export function tmuxInjectSpecs(target, text, o) {
75
+ const socket = o.socket ?? target.socket;
76
+ if (o.enter && o.combined) {
77
+ return [{ argv: tmuxSendKeysArgv(target.pane, text + CR, { literal: true, socket }) }];
78
+ }
79
+ const specs = [{ argv: tmuxSendKeysArgv(target.pane, text, { literal: true, socket }) }];
80
+ // A separate Enter keypress — its own write, so the Ink TUI sees Enter alone.
81
+ if (o.enter)
82
+ specs.push({ argv: tmuxSendKeysArgv(target.pane, 'Enter', { socket }) });
83
+ return specs;
84
+ }
85
+ // --- macOS (iterm / ghostty) ------------------------------------------------
86
+ /**
87
+ * AppleScript that types into a SPECIFIC iTerm2 split via `tell session id
88
+ * "<uuid>" to write text` — the exact split provenance's iterm rail identifies,
89
+ * addressed by its session UUID. Crucially there is NO `activate`: `write text`
90
+ * delivers to that session directly, so injection never brings iTerm forward or
91
+ * types into whatever split happens to be focused. Omitting `session` targets
92
+ * the current session (a direct-call convenience; the resolver always supplies
93
+ * one).
94
+ *
95
+ * Enter semantics — iTerm's `write text` appends a trailing newline, which fuses
96
+ * text+Enter into ONE write and Claude's Ink TUI swallows it. So the Ink-safe
97
+ * default (enter, not combined) suppresses that newline (`write text … newline
98
+ * no`) and sends the Return as a SEPARATE `write text` of a lone CR (`character
99
+ * id 13`) — two distinct pty writes, Enter seen on its own. `combined` opts into
100
+ * the single fused `write text` (auto-newline) for plain shells / REPLs.
101
+ *
102
+ * NOTE (macOS-only verification pending): the two-write CR path is the safe
103
+ * choice because it mirrors the tmux/pty Ink-safe split that IS verified here on
104
+ * Linux; whether a single `write text T` would also submit under Ink can only be
105
+ * confirmed on a Mac running iTerm (see the PR body).
106
+ */
107
+ export function itermInjectScript(text, opts) {
108
+ // Two writes for the Ink-safe default; one fused write for combined / enter=false.
109
+ const body = opts.enter && opts.combined
110
+ ? [`write text ${appleScriptStr(text)}`]
111
+ : opts.enter
112
+ ? [`write text ${appleScriptStr(text)} newline no`, 'write text (character id 13) newline no']
113
+ : [`write text ${appleScriptStr(text)} newline no`];
114
+ const target = opts.session
115
+ ? `session id ${appleScriptStr(opts.session)}`
116
+ : 'current session of current window';
117
+ return [
118
+ 'tell application "iTerm2"',
119
+ ` tell ${target}`,
120
+ ...body.map((l) => ` ${l}`),
121
+ ' end tell',
122
+ 'end tell',
123
+ ].join('\n');
124
+ }
125
+ /**
126
+ * AppleScript for Ghostty (no scripting dictionary): raise the target window via
127
+ * System Events, then keystroke the text and a separate Return. `window` matches
128
+ * a window whose title contains the string; omitted → the frontmost Ghostty window.
129
+ */
130
+ export function ghosttyInjectScript(text, opts) {
131
+ const lines = ['tell application "System Events"', ' tell process "ghostty"', ' set frontmost to true'];
132
+ if (opts.window) {
133
+ lines.push(` perform action "AXRaise" of (first window whose title contains ${appleScriptStr(opts.window)})`);
134
+ }
135
+ lines.push(' end tell');
136
+ lines.push(` keystroke ${appleScriptStr(text)}`);
137
+ if (opts.enter)
138
+ lines.push(' key code 36');
139
+ lines.push('end tell');
140
+ return lines.join('\n');
141
+ }
142
+ /** The osascript spec for a macOS injection. One invocation delivers the text + Return. */
143
+ export function appleScriptInjectSpec(target, text, enter, combined = false) {
144
+ const script = target.backend === 'iterm'
145
+ ? itermInjectScript(text, { session: target.session, enter, combined })
146
+ : ghosttyInjectScript(text, { window: target.window, enter });
147
+ return { argv: ['osascript', '-e', script] };
148
+ }
149
+ // --- vscodium (VSCodium / Cursor / VS Code integrated terminal) -------------
150
+ /** The swarmify extension identifier that owns the swarm-ext URI verbs (matches the launch backend). */
151
+ const EXTENSION_AUTHORITY = 'swarmify.swarm-ext';
152
+ /**
153
+ * The `<scheme>://swarmify.swarm-ext/inject?p=<payload>` URL the extension
154
+ * handles to type into an ALREADY-open integrated terminal (the inject sibling
155
+ * of the launch backend's `/spawn`, src/lib/terminal/backends/vscodium-agent.ts).
156
+ *
157
+ * Payload is base64url-encoded JSON in a single `p` param — identical encoding
158
+ * to `spawnUri`, and for the same reason: VS Code percent-decodes `uri.query`
159
+ * once before the extension parses it, so a naive multi-param query would
160
+ * mis-split a text containing `&`/`=`; base64url (`[A-Za-z0-9_-]`) survives that
161
+ * decode untouched. `terminalId` is the id the extension keys live-terminals.json
162
+ * on (the session UUID); `enter` tells the extension to submit; `combined` asks
163
+ * it to fuse text+Enter into one write (default is the Ink-safe two-write split,
164
+ * mirroring the tmux/iterm paths — the extension owns that split on its side).
165
+ *
166
+ * DEPENDENCY: the `/inject` verb ships in the swarm-ext extension via PR #608's
167
+ * successor (PR #608 introduces `/spawn`; the inject verb + `write`/`enter`
168
+ * handling is its follow-up). Until that lands, this routes correctly on the CLI
169
+ * side but the extension will no-op the unknown verb. See the PR body.
170
+ */
171
+ export function vscodiumInjectUri(scheme, terminalId, text, opts) {
172
+ const payload = { terminalId, text, enter: opts.enter, combined: opts.combined };
173
+ const p = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
174
+ return `${scheme}://${EXTENSION_AUTHORITY}/inject?p=${p}`;
175
+ }
176
+ /** The editor-CLI spec for a VSCodium/Cursor/VS Code injection (one `--open-url` invocation). */
177
+ export function vscodiumInjectSpec(target, text, opts) {
178
+ return { argv: [target.cli, '--open-url', vscodiumInjectUri(target.scheme, target.terminalId, text, opts)] };
179
+ }
180
+ // --- pty --------------------------------------------------------------------
181
+ async function injectPty(target, text, o) {
182
+ if (o.host && o.host !== 'local') {
183
+ return { ok: false, backend: 'pty', writes: 0, error: 'pty injection is local-only (the sidecar is not a remote surface)' };
184
+ }
185
+ // Same Ink-safe split as tmux: text write, then a separate CR write.
186
+ const writes = o.enter && o.combined ? [text + CR] : o.enter ? [text, CR] : [text];
187
+ if (o.dryRun)
188
+ return { ok: true, backend: 'pty', writes: writes.length };
189
+ try {
190
+ for (const input of writes) {
191
+ const res = await ptyRequest('write', target.id, { input });
192
+ if (!res.ok)
193
+ return { ok: false, backend: 'pty', writes: 0, error: res.error ?? 'pty write failed' };
194
+ }
195
+ return { ok: true, backend: 'pty', writes: writes.length };
196
+ }
197
+ catch (err) {
198
+ return { ok: false, backend: 'pty', writes: 0, error: err instanceof Error ? err.message : String(err) };
199
+ }
200
+ }
201
+ // --- public entry -----------------------------------------------------------
202
+ /**
203
+ * Deliver `text` (+ Enter) into an existing terminal surface. Resolves the
204
+ * target's backend and routes to the matching primitive. Never throws — launch
205
+ * failures come back in the result, like the engine's `openSurface`.
206
+ */
207
+ export async function injectIntoTerminal(target, text, opts = {}) {
208
+ const enter = opts.enter !== false;
209
+ const combined = opts.combined === true;
210
+ if (target.backend === 'pty') {
211
+ return injectPty(target, text, { enter, combined, host: opts.host, dryRun: opts.dryRun });
212
+ }
213
+ // tmux + AppleScript + editor-CLI backends all run through the engine transport.
214
+ const specs = target.backend === 'tmux'
215
+ ? tmuxInjectSpecs(target, text, { enter, combined, socket: opts.socket })
216
+ : target.backend === 'vscodium'
217
+ ? [vscodiumInjectSpec(target, text, { enter, combined })]
218
+ : [appleScriptInjectSpec(target, text, enter, combined)];
219
+ // Discrete writes delivered on the far side. tmux counts its send-keys calls.
220
+ // iterm/vscodium honor `combined` (text+Enter fused into one write). ghostty's
221
+ // coarse keystroke path ignores `combined` (it always emits keystroke + a
222
+ // separate Return), so its count tracks `enter` alone.
223
+ const writes = target.backend === 'tmux'
224
+ ? specs.length
225
+ : target.backend === 'ghostty'
226
+ ? enter ? 2 : 1
227
+ : enter && !combined ? 2 : 1;
228
+ if (opts.dryRun)
229
+ return { ok: true, backend: target.backend, writes, specs };
230
+ // AppleScript backends: guard on the backend's own availability, but only for a
231
+ // LOCAL run (a remote host is assumed to have the app — its ssh leg reports failure).
232
+ if (target.backend === 'iterm' || target.backend === 'ghostty') {
233
+ if (!opts.host || opts.host === 'local') {
234
+ const backend = target.backend === 'iterm' ? itermBackend : ghosttyBackend;
235
+ const ctx = opts.ctx ?? currentContext();
236
+ if (!backend.isAvailable(ctx)) {
237
+ return { ok: false, backend: target.backend, writes: 0, specs, error: `${backend.label} is not available here (platform ${ctx.platform})` };
238
+ }
239
+ }
240
+ }
241
+ for (const spec of specs) {
242
+ const res = await runSpec(spec, opts.host, opts.resolveHost);
243
+ if (!res.ok)
244
+ return { ok: false, backend: target.backend, writes: 0, specs, error: res.error };
245
+ }
246
+ return { ok: true, backend: target.backend, writes, specs };
247
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Layout policy — how a batch of surfaces is arranged.
3
+ *
4
+ * `two-per-tab` (default) packs sessions two-up: session 1 opens a new tab,
5
+ * session 2 splits it (right), session 3 opens a new tab, and so on — so each
6
+ * tab holds a left+right pair. `tabs` gives every session its own tab.
7
+ */
8
+ import type { Layout } from './types.js';
9
+ export type Packing = 'two-per-tab' | 'tabs';
10
+ /** Assign a layout to each index in a batch of `count` surfaces. */
11
+ export declare function planLayouts(count: number, packing?: Packing): Layout[];
@@ -0,0 +1,11 @@
1
+ /** Assign a layout to each index in a batch of `count` surfaces. */
2
+ export function planLayouts(count, packing = 'two-per-tab') {
3
+ const out = [];
4
+ for (let i = 0; i < count; i++) {
5
+ if (packing === 'tabs')
6
+ out.push('tab');
7
+ else
8
+ out.push(i % 2 === 0 ? 'tab' : 'split-right');
9
+ }
10
+ return out;
11
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Quoting helpers for building launch commands.
3
+ *
4
+ * `shellQuote` is the single canonical POSIX single-quoter, re-exported from the
5
+ * SSH transport so the local and remote legs quote identically. `appleScriptStr`
6
+ * is the AppleScript string-literal escaper used by the iTerm/Ghostty backends.
7
+ */
8
+ import { shellQuote } from '../ssh-exec.js';
9
+ export { shellQuote };
10
+ /** AppleScript double-quoted string literal (escape backslash, then quote). */
11
+ export declare function appleScriptStr(s: string): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Quoting helpers for building launch commands.
3
+ *
4
+ * `shellQuote` is the single canonical POSIX single-quoter, re-exported from the
5
+ * SSH transport so the local and remote legs quote identically. `appleScriptStr`
6
+ * is the AppleScript string-literal escaper used by the iTerm/Ghostty backends.
7
+ */
8
+ import { shellQuote } from '../ssh-exec.js';
9
+ export { shellQuote };
10
+ /** AppleScript double-quoted string literal (escape backslash, then quote). */
11
+ export function appleScriptStr(s) {
12
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
13
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Inject-target resolution — the safety choke point (RUSH-1415).
3
+ *
4
+ * The watchdog knows a session has stalled and wants to nudge it with "continue".
5
+ * `injectIntoTerminal` (inject.ts) can DELIVER into a target; this module decides
6
+ * the RIGHT target for a given sessionId — the exact split the agent lives in —
7
+ * and NEVER guesses. If it can't name a precise rail it returns `addressable:
8
+ * false` with a reason, and the watchdog skips rather than typing into the
9
+ * frontmost / wrong split.
10
+ *
11
+ * Resolution reads the agent's INHERITED ENV (via session provenance) plus the
12
+ * IDE's live-terminals registry (off disk) — no cooperation from the agent:
13
+ *
14
+ * tmux > iterm > vscodium > pty
15
+ *
16
+ * tmux wins whenever present: a `tmux send-keys -t <pane>` reaches the pane no
17
+ * matter which host app (iTerm / Ghostty / VS Code) is above it, so it's correct
18
+ * inside ANY of them. Absent tmux, the host-app rail: iTerm2's exact split by
19
+ * session UUID (env), or a VSCodium/Cursor/VS Code integrated terminal addressed
20
+ * by its live-terminals id. Ghostty has no per-split addressing (no scripting
21
+ * dictionary), so a Ghostty-hosted session with no tmux is honestly reported
22
+ * un-addressable — the coarse focus-stealing window path stays behind an explicit
23
+ * opt-in and is never chosen by default.
24
+ */
25
+ import type { ActiveSession } from '../session/active.js';
26
+ import type { InjectTarget } from './inject.js';
27
+ /** A resolved rail, or an honest refusal. The watchdog acts only on `addressable: true`. */
28
+ export type InjectRail = 'tmux' | 'iterm' | 'vscodium' | 'ghostty' | 'pty';
29
+ export type InjectResolution = {
30
+ addressable: true;
31
+ rail: InjectRail;
32
+ target: InjectTarget;
33
+ note?: string;
34
+ } | {
35
+ addressable: false;
36
+ reason: string;
37
+ };
38
+ export interface ResolveOptions {
39
+ /**
40
+ * Allow the COARSE Ghostty path (raise the frontmost/opt-in window and type via
41
+ * System Events keystrokes — steals focus, not split-precise). Off by default:
42
+ * a Ghostty session with no tmux resolves to un-addressable instead.
43
+ */
44
+ allowGhosttyFocus?: boolean;
45
+ /**
46
+ * A known `agents pty` sidecar id for this session, if the caller has one. No
47
+ * automatic sessionId -> pty mapping exists yet, so pty is only emitted when
48
+ * supplied here (the lowest-precedence rail).
49
+ */
50
+ ptyId?: string;
51
+ }
52
+ /**
53
+ * Resolve a target from an already-fetched ActiveSession. Pure — no I/O — so the
54
+ * precedence logic is unit-testable without the process table. This is where the
55
+ * tmux > iterm > vscodium > pty precedence and the Ghostty refusal live.
56
+ */
57
+ export declare function resolveInjectTargetForSession(session: ActiveSession, opts?: ResolveOptions): InjectResolution;
58
+ /**
59
+ * The single resolver the watchdog calls: sessionId -> a precise InjectTarget or
60
+ * an honest refusal. Fetches the active session (which carries provenance, host,
61
+ * and pid), then applies the pure precedence above. Returns `addressable: false`
62
+ * when the session isn't live / can't be found — never a guess.
63
+ */
64
+ export declare function resolveInjectTarget(sessionId: string, opts?: ResolveOptions): Promise<InjectResolution>;
@@ -0,0 +1,90 @@
1
+ import { getActiveSessions } from '../session/active.js';
2
+ /** The editor CLIs that speak the swarm-ext URI protocol, keyed by the host detectHost() reports. */
3
+ const IDE_INJECT_VARIANTS = {
4
+ codium: { cli: 'codium', scheme: 'vscodium' },
5
+ cursor: { cli: 'cursor', scheme: 'cursor' },
6
+ code: { cli: 'code', scheme: 'vscode' },
7
+ };
8
+ /**
9
+ * Resolve a target from an already-fetched ActiveSession. Pure — no I/O — so the
10
+ * precedence logic is unit-testable without the process table. This is where the
11
+ * tmux > iterm > vscodium > pty precedence and the Ghostty refusal live.
12
+ */
13
+ export function resolveInjectTargetForSession(session, opts = {}) {
14
+ const prov = session.provenance;
15
+ // 1. tmux — correct inside any host app, so it takes precedence whenever the
16
+ // env carries a pane. (provenance.reply already encodes tmux-over-iterm.)
17
+ if (prov?.mux?.kind === 'tmux' && prov.mux.pane) {
18
+ return {
19
+ addressable: true,
20
+ rail: 'tmux',
21
+ target: { backend: 'tmux', pane: prov.mux.pane, socket: prov.mux.socket },
22
+ };
23
+ }
24
+ // 2. iterm — the exact split by session UUID (env-derived, focus-safe).
25
+ if (prov?.reply?.rail === 'iterm') {
26
+ return {
27
+ addressable: true,
28
+ rail: 'iterm',
29
+ target: { backend: 'iterm', session: prov.reply.session },
30
+ };
31
+ }
32
+ // 3. vscodium — a VSCodium/Cursor/VS Code integrated terminal, addressed by the
33
+ // id the extension keys live-terminals.json on (the session UUID). Only when
34
+ // the session is IDE-hosted AND we know its id.
35
+ const variant = session.host ? IDE_INJECT_VARIANTS[session.host] : undefined;
36
+ if (variant) {
37
+ if (!session.sessionId) {
38
+ return { addressable: false, reason: `IDE terminal (${session.host}) has no session id to address` };
39
+ }
40
+ return {
41
+ addressable: true,
42
+ rail: 'vscodium',
43
+ target: { backend: 'vscodium', terminalId: session.sessionId, cli: variant.cli, scheme: variant.scheme },
44
+ };
45
+ }
46
+ // 4. pty — lowest precedence, only when the caller supplied a sidecar id.
47
+ if (opts.ptyId) {
48
+ return { addressable: true, rail: 'pty', target: { backend: 'pty', id: opts.ptyId } };
49
+ }
50
+ // 5. Ghostty — no per-split addressing exists. Refuse by default; the coarse,
51
+ // focus-stealing window path is opt-in only.
52
+ if (session.host === 'ghostty') {
53
+ if (opts.allowGhosttyFocus) {
54
+ return {
55
+ addressable: true,
56
+ rail: 'ghostty',
57
+ target: { backend: 'ghostty' },
58
+ note: 'coarse Ghostty window path (opt-in): raises a window and types into the FOCUSED split — not split-precise',
59
+ };
60
+ }
61
+ return { addressable: false, reason: 'un-addressable (ghostty, no tmux): no per-split addressing; watchdog skips' };
62
+ }
63
+ return {
64
+ addressable: false,
65
+ reason: session.host
66
+ ? `no precise inject rail for host '${session.host}' (no tmux/iterm/IDE terminal detected)`
67
+ : 'no inject rail: session is not inside tmux, iTerm, or an IDE terminal',
68
+ };
69
+ }
70
+ /**
71
+ * The single resolver the watchdog calls: sessionId -> a precise InjectTarget or
72
+ * an honest refusal. Fetches the active session (which carries provenance, host,
73
+ * and pid), then applies the pure precedence above. Returns `addressable: false`
74
+ * when the session isn't live / can't be found — never a guess.
75
+ */
76
+ export async function resolveInjectTarget(sessionId, opts = {}) {
77
+ if (!sessionId)
78
+ return { addressable: false, reason: 'no sessionId given' };
79
+ let sessions;
80
+ try {
81
+ sessions = await getActiveSessions();
82
+ }
83
+ catch (err) {
84
+ return { addressable: false, reason: `could not list active sessions: ${err instanceof Error ? err.message : String(err)}` };
85
+ }
86
+ const session = sessions.find((s) => s.sessionId === sessionId);
87
+ if (!session)
88
+ return { addressable: false, reason: `no live session found for id ${sessionId}` };
89
+ return resolveInjectTargetForSession(session, opts);
90
+ }
@@ -0,0 +1,6 @@
1
+ /** `cd <cwd> && exec <command>` — for backends that don't set cwd natively. */
2
+ export declare function loginExec(cwd: string, command: string[]): string;
3
+ /** `exec <command>` — for backends that set the working directory natively. */
4
+ export declare function execOnly(command: string[]): string;
5
+ /** Wrap an inner shell script in an interactive login zsh: `zsh -ilc '<inner>'`. */
6
+ export declare function iLoginShell(inner: string): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Interactive-login-shell wrappers shared by every backend.
3
+ *
4
+ * `-i` is load-bearing, not cosmetic: the version-pinned shims (e.g.
5
+ * `claude@2.1.187`) live in `~/.agents/.cache/shims`, which `.zshrc` puts on
6
+ * PATH for *interactive* shells only. A plain `zsh -lc` (login, non-interactive)
7
+ * skips `.zshrc`, so the shim isn't found and the surface dies with "command not
8
+ * found". Every backend wraps its command in `zsh -ilc` for this reason — do not
9
+ * drop the `-i`.
10
+ */
11
+ import { shellQuote } from './quote.js';
12
+ /** `cd <cwd> && exec <command>` — for backends that don't set cwd natively. */
13
+ export function loginExec(cwd, command) {
14
+ return `cd ${shellQuote(cwd)} && exec ${command.join(' ')}`;
15
+ }
16
+ /** `exec <command>` — for backends that set the working directory natively. */
17
+ export function execOnly(command) {
18
+ return `exec ${command.join(' ')}`;
19
+ }
20
+ /** Wrap an inner shell script in an interactive login zsh: `zsh -ilc '<inner>'`. */
21
+ export function iLoginShell(inner) {
22
+ return `zsh -ilc ${shellQuote(inner)}`;
23
+ }
@@ -0,0 +1,15 @@
1
+ import type { LaunchSpec } from './types.js';
2
+ /** Resolve a host alias to an ssh target. Default: identity (ssh_config resolves it). */
3
+ export type HostResolver = (alias: string) => string;
4
+ export interface RunResult {
5
+ ok: boolean;
6
+ error?: string;
7
+ }
8
+ /** Run the spec on this machine: spawn the launcher, resolve when it exits. */
9
+ export declare function runLocal(spec: LaunchSpec): Promise<RunResult>;
10
+ /** Serialize a launch argv into a single POSIX-quoted shell command string. */
11
+ export declare function remoteCommand(spec: LaunchSpec): string;
12
+ /** Run the spec on a remote host over SSH. */
13
+ export declare function runRemote(spec: LaunchSpec, target: string): RunResult;
14
+ /** Run a spec locally (no host / 'local') or on a resolved remote host. */
15
+ export declare function runSpec(spec: LaunchSpec, host?: string, resolveHost?: HostResolver): Promise<RunResult>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Transport — runs a LaunchSpec locally or on a remote host.
3
+ *
4
+ * Local: spawn the launcher (osascript / tmux) and wait for it to exit — these
5
+ * are short-lived commands that create the surface and return, so waiting gives
6
+ * a real success/failure. Remote: serialize the argv into one shell string and
7
+ * hand it to `sshExec` — the same hardened SSH primitive `agents sessions
8
+ * --host` and the browser driver use (target-injection guard, POSIX quoting,
9
+ * connection multiplexing).
10
+ */
11
+ import { spawn } from 'child_process';
12
+ import { sshExec } from '../ssh-exec.js';
13
+ import { shellQuote } from './quote.js';
14
+ /** Run the spec on this machine: spawn the launcher, resolve when it exits. */
15
+ export function runLocal(spec) {
16
+ return new Promise((resolve) => {
17
+ const child = spawn(spec.argv[0], spec.argv.slice(1), { stdio: 'ignore' });
18
+ child.on('error', (err) => resolve({ ok: false, error: err.message }));
19
+ child.on('close', (code) => resolve(code === 0 ? { ok: true } : { ok: false, error: `${spec.argv[0]} exited with code ${code}` }));
20
+ });
21
+ }
22
+ /** Serialize a launch argv into a single POSIX-quoted shell command string. */
23
+ export function remoteCommand(spec) {
24
+ return spec.argv.map(shellQuote).join(' ');
25
+ }
26
+ /** Run the spec on a remote host over SSH. */
27
+ export function runRemote(spec, target) {
28
+ const res = sshExec(target, remoteCommand(spec), { multiplex: true });
29
+ if (res.code === 0)
30
+ return { ok: true };
31
+ const err = (res.stderr || '').trim();
32
+ return { ok: false, error: err || `ssh exited with code ${res.code}` };
33
+ }
34
+ /** Run a spec locally (no host / 'local') or on a resolved remote host. */
35
+ export async function runSpec(spec, host, resolveHost) {
36
+ if (!host || host === 'local')
37
+ return runLocal(spec);
38
+ const target = resolveHost ? resolveHost(host) : host;
39
+ return runRemote(spec, target);
40
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Core types for the terminal launch engine.
3
+ *
4
+ * The engine opens an *interactive* surface — a tab or a split pane — running a
5
+ * command, in a chosen terminal backend (iTerm / Ghostty / tmux), on this
6
+ * machine or a remote host. This is distinct from cloud providers
7
+ * (src/lib/cloud), which dispatch autonomous headless tasks; a terminal surface
8
+ * is attended and live. See docs/terminal-engine.md.
9
+ */
10
+ /** An interactive terminal backend the engine can drive. */
11
+ export type Backend = 'iterm' | 'ghostty' | 'tmux' | 'vscodium-agent';
12
+ /** Which way a split pane grows. `right` = side-by-side; `down` = stacked. */
13
+ export type SplitDirection = 'right' | 'down';
14
+ /** Where a surface lands: a new tab, or a split of the current pane. */
15
+ export type Layout = 'tab' | 'split-right' | 'split-down';
16
+ /** Ambient facts a backend needs to decide availability and detection. */
17
+ export interface EngineContext {
18
+ platform: NodeJS.Platform;
19
+ env: NodeJS.ProcessEnv;
20
+ }
21
+ /** Snapshot the live process context. */
22
+ export declare function currentContext(): EngineContext;
23
+ /** The concrete OS command that opens a surface — the pure output of a backend. */
24
+ export interface LaunchSpec {
25
+ /** argv to run (e.g. `['osascript','-e',script]` or `['tmux','split-window',…]`). */
26
+ argv: string[];
27
+ }
28
+ /** A single "open this command as this surface" instruction. */
29
+ export interface LaunchRequest {
30
+ backend: Backend;
31
+ layout: Layout;
32
+ /** Working directory the command runs in. */
33
+ cwd: string;
34
+ /** argv to exec in the surface (e.g. a resume command). */
35
+ command: string[];
36
+ /** undefined / 'local' = this machine; otherwise a resolvable host alias. */
37
+ host?: string;
38
+ }
39
+ /** Outcome of opening one surface. Never throws for launch failures — reports them. */
40
+ export interface LaunchResult {
41
+ ok: boolean;
42
+ request: LaunchRequest;
43
+ error?: string;
44
+ }
45
+ /**
46
+ * A terminal backend: pure builders + an availability check. No side effects —
47
+ * building a spec never opens anything, so every backend is unit-testable
48
+ * without a display. The engine's transport is what actually runs the spec.
49
+ */
50
+ export interface TerminalBackend {
51
+ readonly id: Backend;
52
+ readonly label: string;
53
+ /** Can this backend be driven here? (platform + app installed / inside tmux). */
54
+ isAvailable(ctx: EngineContext): boolean;
55
+ /** Command that opens a new tab running `command` in `cwd`. */
56
+ buildTab(cwd: string, command: string[]): LaunchSpec;
57
+ /** Command that splits the current surface, running `command` in `cwd`. */
58
+ buildSplit(cwd: string, command: string[], direction: SplitDirection): LaunchSpec;
59
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Core types for the terminal launch engine.
3
+ *
4
+ * The engine opens an *interactive* surface — a tab or a split pane — running a
5
+ * command, in a chosen terminal backend (iTerm / Ghostty / tmux), on this
6
+ * machine or a remote host. This is distinct from cloud providers
7
+ * (src/lib/cloud), which dispatch autonomous headless tasks; a terminal surface
8
+ * is attended and live. See docs/terminal-engine.md.
9
+ */
10
+ /** Snapshot the live process context. */
11
+ export function currentContext() {
12
+ return { platform: process.platform, env: process.env };
13
+ }