@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.
- package/CHANGELOG.md +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- 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. */
|