@phnx-labs/agents-cli 1.20.35 → 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 (225) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  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.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -0,0 +1,204 @@
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 { type HostResolver } from './transport.js';
45
+ import { type LaunchSpec, type EngineContext } from './types.js';
46
+ /**
47
+ * An already-running surface to type into. A superset of the engine's launch
48
+ * `Backend` — it adds `pty` (the sidecar), which the engine can't launch but can
49
+ * be injected into.
50
+ */
51
+ export type InjectTarget = {
52
+ backend: 'tmux';
53
+ pane: string;
54
+ socket?: string;
55
+ } | {
56
+ backend: 'iterm';
57
+ session?: string;
58
+ }
59
+ /**
60
+ * A VSCodium / Cursor / VS Code integrated terminal, addressed by the id the
61
+ * swarm-ext extension keys `live-terminals.json` on (the session UUID). `cli`
62
+ * is the editor CLI on PATH (`codium` / `cursor` / `code`); `scheme` is its
63
+ * URL scheme (`vscodium` / `cursor` / `vscode`) — the resolver fills both.
64
+ */
65
+ | {
66
+ backend: 'vscodium';
67
+ terminalId: string;
68
+ cli: string;
69
+ scheme: string;
70
+ } | {
71
+ backend: 'ghostty';
72
+ window?: string;
73
+ } | {
74
+ backend: 'pty';
75
+ id: string;
76
+ };
77
+ export type InjectBackend = InjectTarget['backend'];
78
+ export interface InjectOptions {
79
+ /** Append Enter after the text. Default true. */
80
+ enter?: boolean;
81
+ /**
82
+ * Fuse the text and its Enter into a SINGLE write. Default false — the safe
83
+ * default sends the text, then the Enter, as two separate writes (Ink-TUI
84
+ * safe). Set true for plain shells / REPLs that want one atomic line.
85
+ */
86
+ combined?: boolean;
87
+ /** tmux socket override (defaults to `target.socket`, then tmux's default socket). */
88
+ socket?: string;
89
+ /** Remote host for the tmux / AppleScript backends (runs the spec over SSH). Local by default. */
90
+ host?: string;
91
+ /** Resolve a host alias to an ssh target (see the engine's transport). */
92
+ resolveHost?: HostResolver;
93
+ /** Ambient context for the availability check (defaults to the live process context). */
94
+ ctx?: EngineContext;
95
+ /** Don't execute — return the spec(s) that WOULD run. Lets the macOS paths be asserted on Linux. */
96
+ dryRun?: boolean;
97
+ }
98
+ export interface InjectResult {
99
+ ok: boolean;
100
+ backend: InjectBackend;
101
+ /** Discrete writes delivered: 2 for the Ink-safe text+Enter split, 1 when combined or enter=false. */
102
+ writes: number;
103
+ /** For the tmux / AppleScript backends (or any dryRun), the spec(s) that ran / would run. */
104
+ specs?: LaunchSpec[];
105
+ error?: string;
106
+ }
107
+ /**
108
+ * argv for `tmux send-keys` targeting a pane by id. Starts with `tmux` (like the
109
+ * engine's `tmuxTabArgv`) so the same transport runs it locally or over SSH. The
110
+ * socket, when set, is positioned before the subcommand (`-S` must lead). `-l`
111
+ * sends the keys literally; without it tmux interprets a key name like `Enter`.
112
+ *
113
+ * NOTE: distinct from `sendKeys()` in src/lib/tmux/session.ts — that one
114
+ * addresses by session *name* on the default socket (the `agents tmux` surface)
115
+ * and shells out directly; this one addresses by *pane id* + arbitrary socket and
116
+ * returns an SSH-capable `LaunchSpec` for the engine transport. Don't collapse them.
117
+ */
118
+ export declare function tmuxSendKeysArgv(pane: string, keys: string, opts?: {
119
+ literal?: boolean;
120
+ socket?: string;
121
+ }): string[];
122
+ /** The one-or-two send-keys specs for a tmux injection (Ink-safe split by default). */
123
+ export declare function tmuxInjectSpecs(target: Extract<InjectTarget, {
124
+ backend: 'tmux';
125
+ }>, text: string, o: {
126
+ enter: boolean;
127
+ combined: boolean;
128
+ socket?: string;
129
+ }): LaunchSpec[];
130
+ /**
131
+ * AppleScript that types into a SPECIFIC iTerm2 split via `tell session id
132
+ * "<uuid>" to write text` — the exact split provenance's iterm rail identifies,
133
+ * addressed by its session UUID. Crucially there is NO `activate`: `write text`
134
+ * delivers to that session directly, so injection never brings iTerm forward or
135
+ * types into whatever split happens to be focused. Omitting `session` targets
136
+ * the current session (a direct-call convenience; the resolver always supplies
137
+ * one).
138
+ *
139
+ * Enter semantics — iTerm's `write text` appends a trailing newline, which fuses
140
+ * text+Enter into ONE write and Claude's Ink TUI swallows it. So the Ink-safe
141
+ * default (enter, not combined) suppresses that newline (`write text … newline
142
+ * no`) and sends the Return as a SEPARATE `write text` of a lone CR (`character
143
+ * id 13`) — two distinct pty writes, Enter seen on its own. `combined` opts into
144
+ * the single fused `write text` (auto-newline) for plain shells / REPLs.
145
+ *
146
+ * NOTE (macOS-only verification pending): the two-write CR path is the safe
147
+ * choice because it mirrors the tmux/pty Ink-safe split that IS verified here on
148
+ * Linux; whether a single `write text T` would also submit under Ink can only be
149
+ * confirmed on a Mac running iTerm (see the PR body).
150
+ */
151
+ export declare function itermInjectScript(text: string, opts: {
152
+ session?: string;
153
+ enter: boolean;
154
+ combined?: boolean;
155
+ }): string;
156
+ /**
157
+ * AppleScript for Ghostty (no scripting dictionary): raise the target window via
158
+ * System Events, then keystroke the text and a separate Return. `window` matches
159
+ * a window whose title contains the string; omitted → the frontmost Ghostty window.
160
+ */
161
+ export declare function ghosttyInjectScript(text: string, opts: {
162
+ window?: string;
163
+ enter: boolean;
164
+ }): string;
165
+ /** The osascript spec for a macOS injection. One invocation delivers the text + Return. */
166
+ export declare function appleScriptInjectSpec(target: Extract<InjectTarget, {
167
+ backend: 'iterm' | 'ghostty';
168
+ }>, text: string, enter: boolean, combined?: boolean): LaunchSpec;
169
+ /**
170
+ * The `<scheme>://swarmify.swarm-ext/inject?p=<payload>` URL the extension
171
+ * handles to type into an ALREADY-open integrated terminal (the inject sibling
172
+ * of the launch backend's `/spawn`, src/lib/terminal/backends/vscodium-agent.ts).
173
+ *
174
+ * Payload is base64url-encoded JSON in a single `p` param — identical encoding
175
+ * to `spawnUri`, and for the same reason: VS Code percent-decodes `uri.query`
176
+ * once before the extension parses it, so a naive multi-param query would
177
+ * mis-split a text containing `&`/`=`; base64url (`[A-Za-z0-9_-]`) survives that
178
+ * decode untouched. `terminalId` is the id the extension keys live-terminals.json
179
+ * on (the session UUID); `enter` tells the extension to submit; `combined` asks
180
+ * it to fuse text+Enter into one write (default is the Ink-safe two-write split,
181
+ * mirroring the tmux/iterm paths — the extension owns that split on its side).
182
+ *
183
+ * DEPENDENCY: the `/inject` verb ships in the swarm-ext extension via PR #608's
184
+ * successor (PR #608 introduces `/spawn`; the inject verb + `write`/`enter`
185
+ * handling is its follow-up). Until that lands, this routes correctly on the CLI
186
+ * side but the extension will no-op the unknown verb. See the PR body.
187
+ */
188
+ export declare function vscodiumInjectUri(scheme: string, terminalId: string, text: string, opts: {
189
+ enter: boolean;
190
+ combined: boolean;
191
+ }): string;
192
+ /** The editor-CLI spec for a VSCodium/Cursor/VS Code injection (one `--open-url` invocation). */
193
+ export declare function vscodiumInjectSpec(target: Extract<InjectTarget, {
194
+ backend: 'vscodium';
195
+ }>, text: string, opts: {
196
+ enter: boolean;
197
+ combined: boolean;
198
+ }): LaunchSpec;
199
+ /**
200
+ * Deliver `text` (+ Enter) into an existing terminal surface. Resolves the
201
+ * target's backend and routes to the matching primitive. Never throws — launch
202
+ * failures come back in the result, like the engine's `openSurface`.
203
+ */
204
+ export declare function injectIntoTerminal(target: InjectTarget, text: string, opts?: InjectOptions): Promise<InjectResult>;
@@ -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,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
+ }
@@ -8,7 +8,7 @@
8
8
  * is attended and live. See docs/terminal-engine.md.
9
9
  */
10
10
  /** An interactive terminal backend the engine can drive. */
11
- export type Backend = 'iterm' | 'ghostty' | 'tmux';
11
+ export type Backend = 'iterm' | 'ghostty' | 'tmux' | 'vscodium-agent';
12
12
  /** Which way a split pane grows. `right` = side-by-side; `down` = stacked. */
13
13
  export type SplitDirection = 'right' | 'down';
14
14
  /** Where a surface lands: a new tab, or a split of the current pane. */