@phnx-labs/agents-cli 1.22.114 → 1.22.116

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 (243) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +210 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +214 -148
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/feed.js +65 -14
  19. package/dist/commands/focus.js +1 -1
  20. package/dist/commands/go.d.ts +0 -17
  21. package/dist/commands/go.js +2 -19
  22. package/dist/commands/logs.js +1 -1
  23. package/dist/commands/mcp.js +8 -83
  24. package/dist/commands/memory.js +4 -47
  25. package/dist/commands/message.js +4 -4
  26. package/dist/commands/plugins.js +1 -93
  27. package/dist/commands/repo.js +0 -44
  28. package/dist/commands/resume.js +4 -1
  29. package/dist/commands/secrets-passthrough.js +2 -2
  30. package/dist/commands/send.d.ts +4 -4
  31. package/dist/commands/send.js +6 -51
  32. package/dist/commands/sessions-backup-setup.js +1 -1
  33. package/dist/commands/sessions-picker.d.ts +11 -0
  34. package/dist/commands/sessions-picker.js +88 -7
  35. package/dist/commands/sessions-resume.js +0 -1
  36. package/dist/commands/sessions-share.d.ts +5 -7
  37. package/dist/commands/sessions-share.js +98 -49
  38. package/dist/commands/sessions.d.ts +21 -2
  39. package/dist/commands/sessions.js +158 -11
  40. package/dist/commands/setup-browser.js +18 -2
  41. package/dist/commands/setup-computer.js +20 -6
  42. package/dist/commands/setup-secrets.d.ts +2 -2
  43. package/dist/commands/setup-secrets.js +24 -4
  44. package/dist/commands/setup-term.d.ts +24 -0
  45. package/dist/commands/setup-term.js +70 -0
  46. package/dist/commands/setup-terminal.d.ts +3 -0
  47. package/dist/commands/setup-terminal.js +22 -0
  48. package/dist/commands/setup.d.ts +1 -1
  49. package/dist/commands/setup.js +29 -11
  50. package/dist/commands/skills.js +0 -8
  51. package/dist/commands/ssh.d.ts +6 -0
  52. package/dist/commands/ssh.js +93 -302
  53. package/dist/commands/sync.js +14 -5
  54. package/dist/commands/teams.js +1 -1
  55. package/dist/commands/traces.js +1 -1
  56. package/dist/lib/accounting/rotate.d.ts +63 -1
  57. package/dist/lib/accounting/rotate.js +56 -0
  58. package/dist/lib/accounts/add.d.ts +0 -5
  59. package/dist/lib/accounts/add.js +3 -9
  60. package/dist/lib/accounts/slots.js +32 -2
  61. package/dist/lib/answer-router.d.ts +11 -2
  62. package/dist/lib/answer-router.js +26 -2
  63. package/dist/lib/artifacts-client.d.ts +20 -0
  64. package/dist/lib/artifacts-client.js +46 -0
  65. package/dist/lib/auth-mint.d.ts +5 -4
  66. package/dist/lib/auth-mint.js +6 -5
  67. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  68. package/dist/lib/browser/runtime-state.d.ts +55 -0
  69. package/dist/lib/browser/runtime-state.js +99 -18
  70. package/dist/lib/browser/service.d.ts +10 -0
  71. package/dist/lib/browser/service.js +229 -37
  72. package/dist/lib/browser/types.d.ts +18 -0
  73. package/dist/lib/cli-resources.js +3 -1
  74. package/dist/lib/cloud/dispatch.js +1 -1
  75. package/dist/lib/cloudflare/creds.d.ts +10 -0
  76. package/dist/lib/cloudflare/creds.js +46 -0
  77. package/dist/lib/cloudflare/provision.d.ts +35 -0
  78. package/dist/lib/cloudflare/provision.js +144 -0
  79. package/dist/lib/computer/sessions-list.d.ts +55 -0
  80. package/dist/lib/computer/sessions-list.js +168 -1
  81. package/dist/lib/config-keys.d.ts +1 -1
  82. package/dist/lib/config-keys.js +5 -0
  83. package/dist/lib/daemon/daemon.js +8 -1
  84. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  85. package/dist/lib/daemon/feed-stream-service.js +40 -0
  86. package/dist/lib/daemon-services.d.ts +1 -1
  87. package/dist/lib/daemon-services.js +5 -0
  88. package/dist/lib/device-config.js +61 -0
  89. package/dist/lib/devices/connect.d.ts +49 -5
  90. package/dist/lib/devices/connect.js +169 -21
  91. package/dist/lib/devices/doctor-findings.js +2 -6
  92. package/dist/lib/feed/answer.d.ts +153 -4
  93. package/dist/lib/feed/answer.js +716 -105
  94. package/dist/lib/feed/envelope.d.ts +79 -0
  95. package/dist/lib/feed/envelope.js +23 -0
  96. package/dist/lib/feed/events.d.ts +6 -0
  97. package/dist/lib/feed/events.js +8 -0
  98. package/dist/lib/feed/feed.d.ts +61 -1
  99. package/dist/lib/feed/feed.js +226 -14
  100. package/dist/lib/feed/hub-server.d.ts +141 -0
  101. package/dist/lib/feed/hub-server.js +586 -0
  102. package/dist/lib/feed/hub.d.ts +95 -0
  103. package/dist/lib/feed/hub.js +255 -0
  104. package/dist/lib/feed/pr-status.d.ts +8 -0
  105. package/dist/lib/feed/pr-status.js +9 -1
  106. package/dist/lib/feed/tool-activity.d.ts +108 -0
  107. package/dist/lib/feed/tool-activity.js +313 -0
  108. package/dist/lib/feed/tools.d.ts +198 -0
  109. package/dist/lib/feed/tools.js +265 -0
  110. package/dist/lib/feed/watch.d.ts +50 -50
  111. package/dist/lib/feed/watch.js +147 -16
  112. package/dist/lib/feed-outcome.d.ts +1 -1
  113. package/dist/lib/feed-outcome.js +9 -2
  114. package/dist/lib/feed-policy.js +9 -3
  115. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  116. package/dist/lib/fleet/auth-sync.js +2 -89
  117. package/dist/lib/format.d.ts +1 -1
  118. package/dist/lib/format.js +1 -1
  119. package/dist/lib/git.d.ts +0 -16
  120. package/dist/lib/git.js +0 -58
  121. package/dist/lib/harness-auth-capabilities.js +7 -2
  122. package/dist/lib/helper-versions.js +1 -1
  123. package/dist/lib/hosts/dispatch.d.ts +20 -1
  124. package/dist/lib/hosts/dispatch.js +52 -30
  125. package/dist/lib/hosts/remote-cmd.d.ts +72 -1
  126. package/dist/lib/hosts/remote-cmd.js +151 -10
  127. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  128. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  129. package/dist/lib/mailbox.d.ts +12 -0
  130. package/dist/lib/mailbox.js +16 -2
  131. package/dist/lib/mcp.js +17 -11
  132. package/dist/lib/menubar/snapshot.d.ts +51 -0
  133. package/dist/lib/menubar/snapshot.js +42 -3
  134. package/dist/lib/open-url.js +2 -2
  135. package/dist/lib/probe.d.ts +4 -1
  136. package/dist/lib/probe.js +5 -2
  137. package/dist/lib/projects.d.ts +23 -0
  138. package/dist/lib/projects.js +78 -0
  139. package/dist/lib/pwsh.d.ts +33 -0
  140. package/dist/lib/pwsh.js +56 -0
  141. package/dist/lib/redact.d.ts +8 -0
  142. package/dist/lib/redact.js +11 -0
  143. package/dist/lib/refresh.d.ts +6 -2
  144. package/dist/lib/refresh.js +92 -72
  145. package/dist/lib/secrets-cli.d.ts +11 -0
  146. package/dist/lib/secrets-cli.js +30 -0
  147. package/dist/lib/secrets-client.js +3 -2
  148. package/dist/lib/session/active.d.ts +1 -0
  149. package/dist/lib/session/active.js +8 -0
  150. package/dist/lib/session/db.d.ts +67 -3
  151. package/dist/lib/session/db.js +381 -126
  152. package/dist/lib/session/detached.d.ts +7 -0
  153. package/dist/lib/session/detached.js +29 -0
  154. package/dist/lib/session/prompt.d.ts +23 -7
  155. package/dist/lib/session/prompt.js +46 -8
  156. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  157. package/dist/lib/session/remote/peer-stream.js +33 -6
  158. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  159. package/dist/lib/session/remote/remote-list.js +22 -6
  160. package/dist/lib/session/remote/watch.d.ts +12 -0
  161. package/dist/lib/session/remote/watch.js +9 -0
  162. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  163. package/dist/lib/session/remote-preview-cache.js +373 -0
  164. package/dist/lib/session/sync/backend.d.ts +3 -3
  165. package/dist/lib/session/sync/backend.js +3 -3
  166. package/dist/lib/session/sync/provision.d.ts +1 -1
  167. package/dist/lib/session/sync/provision.js +2 -2
  168. package/dist/lib/session/tail.d.ts +50 -0
  169. package/dist/lib/session/tail.js +219 -0
  170. package/dist/lib/sessions-client.js +0 -3
  171. package/dist/lib/setup-tool-install.d.ts +3 -0
  172. package/dist/lib/setup-tool-install.js +27 -0
  173. package/dist/lib/setup-tool-status.d.ts +22 -0
  174. package/dist/lib/setup-tool-status.js +220 -0
  175. package/dist/lib/share-runtime.d.ts +11 -0
  176. package/dist/lib/share-runtime.js +63 -0
  177. package/dist/lib/signin-badge.d.ts +19 -4
  178. package/dist/lib/signin-badge.js +29 -11
  179. package/dist/lib/smart-launch.d.ts +1 -5
  180. package/dist/lib/smart-launch.js +3 -11
  181. package/dist/lib/ssh-exec.d.ts +44 -0
  182. package/dist/lib/ssh-exec.js +119 -0
  183. package/dist/lib/startup/command-registry.d.ts +6 -4
  184. package/dist/lib/startup/command-registry.js +10 -7
  185. package/dist/lib/state.js +2 -2
  186. package/dist/lib/storage/selection.d.ts +2 -2
  187. package/dist/lib/storage/selection.js +2 -2
  188. package/dist/lib/sync-umbrella.d.ts +5 -0
  189. package/dist/lib/sync-umbrella.js +18 -10
  190. package/dist/lib/term-driver.d.ts +24 -0
  191. package/dist/lib/term-driver.js +36 -0
  192. package/dist/lib/terminal/index.d.ts +1 -1
  193. package/dist/lib/terminal/index.js +1 -1
  194. package/dist/lib/terminal/inject.d.ts +38 -0
  195. package/dist/lib/terminal/inject.js +55 -9
  196. package/dist/lib/terminal/transport.d.ts +15 -5
  197. package/dist/lib/terminal/transport.js +61 -11
  198. package/dist/lib/traces/backend.d.ts +1 -2
  199. package/dist/lib/traces/backend.js +1 -2
  200. package/dist/lib/traces/provision.d.ts +1 -1
  201. package/dist/lib/traces/provision.js +2 -2
  202. package/dist/lib/types.d.ts +8 -6
  203. package/package.json +2 -3
  204. package/dist/commands/artifacts-setup.d.ts +0 -53
  205. package/dist/commands/artifacts-setup.js +0 -161
  206. package/dist/commands/artifacts.d.ts +0 -18
  207. package/dist/commands/artifacts.js +0 -58
  208. package/dist/commands/attach.d.ts +0 -12
  209. package/dist/commands/attach.js +0 -86
  210. package/dist/commands/auth-mint.d.ts +0 -12
  211. package/dist/commands/auth-mint.js +0 -108
  212. package/dist/commands/reconnect.d.ts +0 -46
  213. package/dist/commands/reconnect.js +0 -115
  214. package/dist/commands/share.d.ts +0 -293
  215. package/dist/commands/share.js +0 -1424
  216. package/dist/lib/fleet/remote-login.d.ts +0 -170
  217. package/dist/lib/fleet/remote-login.js +0 -568
  218. package/dist/lib/share/analytics.d.ts +0 -13
  219. package/dist/lib/share/analytics.js +0 -45
  220. package/dist/lib/share/backend.d.ts +0 -120
  221. package/dist/lib/share/backend.js +0 -176
  222. package/dist/lib/share/capture.d.ts +0 -31
  223. package/dist/lib/share/capture.js +0 -174
  224. package/dist/lib/share/config.d.ts +0 -72
  225. package/dist/lib/share/config.js +0 -211
  226. package/dist/lib/share/delete.d.ts +0 -123
  227. package/dist/lib/share/delete.js +0 -173
  228. package/dist/lib/share/html.d.ts +0 -20
  229. package/dist/lib/share/html.js +0 -88
  230. package/dist/lib/share/http-error.d.ts +0 -53
  231. package/dist/lib/share/http-error.js +0 -65
  232. package/dist/lib/share/og.d.ts +0 -26
  233. package/dist/lib/share/og.js +0 -84
  234. package/dist/lib/share/provision.d.ts +0 -127
  235. package/dist/lib/share/provision.js +0 -285
  236. package/dist/lib/share/publish.d.ts +0 -379
  237. package/dist/lib/share/publish.js +0 -818
  238. package/dist/lib/share/worker-template.d.ts +0 -27
  239. package/dist/lib/share/worker-template.js +0 -2424
  240. package/dist/lib/storage/index.d.ts +0 -14
  241. package/dist/lib/storage/index.js +0 -14
  242. package/dist/lib/storage/visibility.d.ts +0 -82
  243. package/dist/lib/storage/visibility.js +0 -99
@@ -11,13 +11,15 @@
11
11
  import chalk from 'chalk';
12
12
  import { addWorkerRefusal } from './accounts/add.js';
13
13
  import { AGENTS } from './agents.js';
14
+ import { HARNESS_AUTH } from './harness-auth-capabilities.js';
14
15
  import { CONFIG_ENV_ISOLATED_AGENTS } from './installations/shims.js';
15
16
  /**
16
17
  * The exact command that logs a given agent in — for warn banners and nudges.
17
18
  * Driven off the registry `cliCommand` with the per-agent subcommand overrides
18
- * (verified against the real CLIs): codex/grok use `<cli> login`, opencode uses
19
- * `<cli> auth login`, claude logs in from inside its TUI via `/login`, and the
20
- * remaining agents (kimi, gemini, …) start their device/oauth flow on launch.
19
+ * (verified against the real CLIs): codex/grok/opencode run the finite login
20
+ * subcommand `HARNESS_AUTH` wires (`loginSubcommand`), claude logs in from
21
+ * inside its TUI via `/login`, and the remaining agents (kimi, gemini, …) start
22
+ * their device/oauth flow on launch.
21
23
  */
22
24
  export function loginHint(agentId) {
23
25
  const cli = AGENTS[agentId]?.cliCommand ?? agentId;
@@ -26,9 +28,8 @@ export function loginHint(agentId) {
26
28
  return `${cli}, then /login`;
27
29
  case 'codex':
28
30
  case 'grok':
29
- return `${cli} login`;
30
31
  case 'opencode':
31
- return `${cli} auth login`;
32
+ return `${cli} ${loginSubcommand(agentId)}`;
32
33
  // Warp Agent CLI has no `login` subcommand: running `warp` opens a browser
33
34
  // sign-in on launch (or set WARP_API_KEY / pass --api-key), so the default
34
35
  // bare-`warp` hint is correct.
@@ -36,10 +37,28 @@ export function loginHint(agentId) {
36
37
  return cli;
37
38
  }
38
39
  }
40
+ /**
41
+ * Harnesses that log back in through a finite native subcommand runnable via
42
+ * `agents run <agent>@<version> -- <args>`. Claude logs in from inside its TUI
43
+ * (`/login`); cursor and the rest start their device/oauth flow on launch.
44
+ */
45
+ export const SUBCOMMAND_LOGIN_AGENTS = ['codex', 'grok', 'opencode'];
46
+ /**
47
+ * The finite native login subcommand (`login`, `login --device-auth`,
48
+ * `auth login`) for a {@link SUBCOMMAND_LOGIN_AGENTS} harness, read from the one
49
+ * `HARNESS_AUTH` row so every surface that spells it — the hint, the per-version
50
+ * fix, `agents doctor` — agrees. Null for every other harness.
51
+ */
52
+ export function loginSubcommand(agent) {
53
+ if (!SUBCOMMAND_LOGIN_AGENTS.includes(agent))
54
+ return null;
55
+ return HARNESS_AUTH[agent].login.join(' ');
56
+ }
39
57
  /**
40
58
  * Exact action shown beside a non-live account. Every emitted command exists
41
59
  * today — never a planned surface and never a hidden verb:
42
- * - Per-device harnesses repair per box via `agents devices login`.
60
+ * - Per-device harnesses (kimi/antigravity) repair on the box itself: run the
61
+ * harness there (`loginHint`) and complete its native login.
43
62
  * - Named accounts re-auth through `agents accounts login <harness>#<name>`.
44
63
  * A known account with no slot on a headed device is onboarded with
45
64
  * `agents accounts add <harness> <name>`. A worker never runs an
@@ -55,7 +74,7 @@ export function fixFor(input) {
55
74
  if (verdict === 'live' || verdict === 'rate_limited' || verdict === 'unverified' || verdict === 'ready')
56
75
  return null;
57
76
  if (input.provisioning === 'per-device' || verdict === 'per-device') {
58
- return `agents devices login --agents ${agent}`;
77
+ return loginHint(agent);
59
78
  }
60
79
  if (input.name) {
61
80
  const worker = addWorkerRefusal(agent, input.name);
@@ -70,10 +89,9 @@ export function fixFor(input) {
70
89
  return loginHint(agent);
71
90
  if (agent === 'claude')
72
91
  return `agents run ${agent}@${version}, then /login`;
73
- if (agent === 'codex' || agent === 'grok')
74
- return `agents run ${agent}@${version} -- login`;
75
- if (agent === 'opencode')
76
- return `agents run ${agent}@${version} -- auth login`;
92
+ const sub = loginSubcommand(agent);
93
+ if (sub)
94
+ return `agents run ${agent}@${version} -- ${sub}`;
77
95
  return `agents run ${agent}@${version}`;
78
96
  }
79
97
  /**
@@ -117,16 +117,12 @@ type DeviceAutoHostOptions = {
117
117
  device?: string;
118
118
  on?: string;
119
119
  computer?: string;
120
- /** @deprecated Hidden alias for `--device auto`. */
121
- smart?: boolean;
122
120
  balanced?: boolean;
123
121
  strategy?: string;
124
122
  };
125
123
  export type DeviceAutoApplyResult = {
126
124
  /** True when automatic placement ran successfully. */
127
125
  attempted: boolean;
128
- /** True when deprecated `--smart` was seen. */
129
- deprecationSmart: boolean;
130
126
  /** Present when affinity resolved without error. */
131
127
  banner?: {
132
128
  hostLabel: string;
@@ -135,7 +131,7 @@ export type DeviceAutoApplyResult = {
135
131
  };
136
132
  };
137
133
  /**
138
- * Apply `--device auto` (and deprecated `--smart`) onto run options.
134
+ * Apply `--device auto` onto run options.
139
135
  * Mutates `options` in place. Placement failures propagate without rewriting
140
136
  * `auto`, so callers fail loud instead of silently launching locally.
141
137
  */
@@ -176,7 +176,7 @@ export async function resolveDeviceAuto(agent, opts = {}) {
176
176
  if (eligiblePool.length === 0) {
177
177
  throw new Error(formatNoHealthyDeviceError(pool, signals, agent));
178
178
  }
179
- // `agents devices prefer <name>` boosts a device in the ranking — resolved
179
+ // `agents devices config <name> auto-launch.preferred on` boosts a device in the ranking — resolved
180
180
  // over the same pool so a fleet default reaches a doc-less box.
181
181
  const preferred = opts.preferred ?? autoLaunchPreferredSet(pool, { roster: pool });
182
182
  const picked = pickBestDevice(eligiblePool, [], { signals, agentLabel: agent, preferred });
@@ -260,21 +260,14 @@ export function isDeviceAuto(value) {
260
260
  }
261
261
  const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
262
262
  /**
263
- * Apply `--device auto` (and deprecated `--smart`) onto run options.
263
+ * Apply `--device auto` onto run options.
264
264
  * Mutates `options` in place. Placement failures propagate without rewriting
265
265
  * `auto`, so callers fail loud instead of silently launching locally.
266
266
  */
267
267
  export async function applyDeviceAutoToOptions(options, deps = {}) {
268
- let deprecationSmart = false;
269
- if (options.smart) {
270
- const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
271
- if (!anyHost)
272
- options.device = 'auto';
273
- deprecationSmart = true;
274
- }
275
268
  const hasAuto = HOST_SLOTS.some((k) => isDeviceAuto(options[k]));
276
269
  if (!hasAuto) {
277
- return { attempted: false, deprecationSmart };
270
+ return { attempted: false };
278
271
  }
279
272
  const accountPickerRequested = deps.accountPickerRequested ?? false;
280
273
  const resolve = deps.resolve ?? ((accountPicker) => resolveDeviceAuto(deps.agent, { accountPicker }));
@@ -296,7 +289,6 @@ export async function applyDeviceAutoToOptions(options, deps = {}) {
296
289
  const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
297
290
  return {
298
291
  attempted: true,
299
- deprecationSmart,
300
292
  banner: { hostLabel, deviceHint, acctNote },
301
293
  };
302
294
  }
@@ -173,6 +173,50 @@ interface SshExecRawStreamOptions extends SshExecOptions {
173
173
  * offset-resumed log following where a UTF-8 decode boundary must not shift the
174
174
  * byte cursor.
175
175
  */
176
+ /** Grace between SIGTERM and SIGKILL for a stream that will not stop. */
177
+ export declare const SSH_STREAM_KILL_GRACE_MS = 3000;
178
+ /** Bytes of a stream's stderr retained; the rest is dropped, with a note. */
179
+ export declare const SSH_STREAM_MAX_STDERR: number;
180
+ export interface SshStreamResult {
181
+ code: number | null;
182
+ stderr: Buffer;
183
+ timedOut: boolean;
184
+ /** True when the child had to be SIGKILLed after ignoring SIGTERM. */
185
+ killed: boolean;
186
+ /** True when stderr was truncated at {@link SSH_STREAM_MAX_STDERR}. */
187
+ stderrTruncated: boolean;
188
+ }
189
+ /**
190
+ * Stream one ssh command's stdout, with the args and env a CALLER built.
191
+ *
192
+ * This exists because {@link sshExecRawStream} assembles its own ssh args, which
193
+ * means it cannot carry a device's canonical auth — the askpass shim for a
194
+ * password-auth box, `-i`/`IdentitiesOnly` for an explicit identity file, or the
195
+ * managed known-hosts pinning. A caller that has already built those through
196
+ * `buildSshInvocation` needs a way to run them, and duplicating the transport to
197
+ * get it would be the worse answer. So this takes `args`/`env` verbatim and adds
198
+ * only the lifecycle guarantees:
199
+ *
200
+ * - **SIGTERM then SIGKILL.** A timeout or abort that only sends SIGTERM leaks a
201
+ * child that ignores it — ssh does, mid-handshake — and the promise never
202
+ * settles. After {@link SSH_STREAM_KILL_GRACE_MS} the child is SIGKILLed and
203
+ * `killed` says so.
204
+ * - **Bounded stderr.** A peer that writes endlessly to stderr would otherwise
205
+ * grow this buffer without limit; it is capped and `stderrTruncated` says so.
206
+ * - **`onStdout` cannot break the caller's cleanup.** A throw from the consumer
207
+ * is captured and re-thrown by the awaiting caller, so a write failure ends the
208
+ * transfer instead of escaping as an unhandled error inside a stream event.
209
+ */
210
+ export declare function sshStreamWithArgs(opts: {
211
+ args: string[];
212
+ env?: Record<string, string>;
213
+ onStdout: (chunk: Buffer) => void;
214
+ timeoutMs?: number;
215
+ killGraceMs?: number;
216
+ maxStderrBytes?: number;
217
+ signal?: AbortSignal;
218
+ sshBin?: string;
219
+ }): Promise<SshStreamResult>;
176
220
  export declare function sshExecRawStream(target: string, remoteCmd: string, opts: SshExecRawStreamOptions): Promise<Omit<SshExecRawResult, 'stdout'>>;
177
221
  /** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
178
222
  export declare function sshReachable(target: string, timeoutMs?: number): boolean;
@@ -317,6 +317,125 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
317
317
  * offset-resumed log following where a UTF-8 decode boundary must not shift the
318
318
  * byte cursor.
319
319
  */
320
+ /** Grace between SIGTERM and SIGKILL for a stream that will not stop. */
321
+ export const SSH_STREAM_KILL_GRACE_MS = 3_000;
322
+ /** Bytes of a stream's stderr retained; the rest is dropped, with a note. */
323
+ export const SSH_STREAM_MAX_STDERR = 8 * 1024;
324
+ /**
325
+ * Stream one ssh command's stdout, with the args and env a CALLER built.
326
+ *
327
+ * This exists because {@link sshExecRawStream} assembles its own ssh args, which
328
+ * means it cannot carry a device's canonical auth — the askpass shim for a
329
+ * password-auth box, `-i`/`IdentitiesOnly` for an explicit identity file, or the
330
+ * managed known-hosts pinning. A caller that has already built those through
331
+ * `buildSshInvocation` needs a way to run them, and duplicating the transport to
332
+ * get it would be the worse answer. So this takes `args`/`env` verbatim and adds
333
+ * only the lifecycle guarantees:
334
+ *
335
+ * - **SIGTERM then SIGKILL.** A timeout or abort that only sends SIGTERM leaks a
336
+ * child that ignores it — ssh does, mid-handshake — and the promise never
337
+ * settles. After {@link SSH_STREAM_KILL_GRACE_MS} the child is SIGKILLed and
338
+ * `killed` says so.
339
+ * - **Bounded stderr.** A peer that writes endlessly to stderr would otherwise
340
+ * grow this buffer without limit; it is capped and `stderrTruncated` says so.
341
+ * - **`onStdout` cannot break the caller's cleanup.** A throw from the consumer
342
+ * is captured and re-thrown by the awaiting caller, so a write failure ends the
343
+ * transfer instead of escaping as an unhandled error inside a stream event.
344
+ */
345
+ export function sshStreamWithArgs(opts) {
346
+ const maxStderr = opts.maxStderrBytes ?? SSH_STREAM_MAX_STDERR;
347
+ const grace = opts.killGraceMs ?? SSH_STREAM_KILL_GRACE_MS;
348
+ return new Promise((resolve, reject) => {
349
+ const child = spawn(opts.sshBin ?? 'ssh', opts.args, {
350
+ stdio: ['ignore', 'pipe', 'pipe'],
351
+ env: opts.env ? { ...process.env, ...opts.env } : process.env,
352
+ windowsHide: true,
353
+ // Own process group on POSIX so a kill reaches the whole tree. Killing only
354
+ // the ssh pid is not enough: a descendant that inherited stdout keeps the
355
+ // pipe open, `'close'` never fires, and the promise hangs for as long as
356
+ // that grandchild lives — which defeats the timeout entirely.
357
+ detached: process.platform !== 'win32',
358
+ });
359
+ const stderr = [];
360
+ let stderrBytes = 0;
361
+ let stderrTruncated = false;
362
+ let timedOut = false;
363
+ let killed = false;
364
+ let consumerError;
365
+ let settled = false;
366
+ let killTimer;
367
+ /** Signal the whole group where the platform supports it, else just the child. */
368
+ const signal = (sig) => {
369
+ try {
370
+ if (process.platform !== 'win32' && child.pid)
371
+ process.kill(-child.pid, sig);
372
+ else
373
+ child.kill(sig);
374
+ }
375
+ catch { /* already gone */ }
376
+ };
377
+ const escalate = () => {
378
+ signal('SIGTERM');
379
+ // Escalation, not a second polite ask: a child still alive after the grace
380
+ // is one that is not going to honour SIGTERM.
381
+ killTimer = setTimeout(() => { killed = true; signal('SIGKILL'); }, grace);
382
+ killTimer.unref?.();
383
+ };
384
+ const stop = () => escalate();
385
+ const finish = (code) => {
386
+ if (settled)
387
+ return;
388
+ settled = true;
389
+ if (timer)
390
+ clearTimeout(timer);
391
+ if (killTimer)
392
+ clearTimeout(killTimer);
393
+ opts.signal?.removeEventListener('abort', stop);
394
+ if (consumerError !== undefined) {
395
+ reject(consumerError);
396
+ return;
397
+ }
398
+ resolve({ code, stderr: Buffer.concat(stderr), timedOut, killed, stderrTruncated });
399
+ };
400
+ const timer = opts.timeoutMs
401
+ ? setTimeout(() => { timedOut = true; escalate(); }, opts.timeoutMs)
402
+ : null;
403
+ child.stdout.on('data', (chunk) => {
404
+ if (consumerError !== undefined)
405
+ return;
406
+ try {
407
+ opts.onStdout(chunk);
408
+ }
409
+ catch (error) {
410
+ // Captured, not thrown: a throw here would surface as an unhandled error
411
+ // on the stream and skip the caller's cleanup entirely.
412
+ consumerError = error;
413
+ escalate();
414
+ }
415
+ });
416
+ child.stderr.on('data', (chunk) => {
417
+ const room = maxStderr - stderrBytes;
418
+ if (room <= 0) {
419
+ stderrTruncated = true;
420
+ return;
421
+ }
422
+ if (chunk.length > room) {
423
+ stderrTruncated = true;
424
+ stderr.push(chunk.subarray(0, room));
425
+ stderrBytes = maxStderr;
426
+ return;
427
+ }
428
+ stderr.push(chunk);
429
+ stderrBytes += chunk.length;
430
+ });
431
+ child.once('error', () => finish(null));
432
+ child.once('close', finish);
433
+ if (opts.signal?.aborted)
434
+ escalate();
435
+ else
436
+ opts.signal?.addEventListener('abort', stop, { once: true });
437
+ });
438
+ }
320
439
  export function sshExecRawStream(target, remoteCmd, opts) {
321
440
  assertValidSshTarget(target);
322
441
  const mux = opts.multiplex === false ? [] : controlOpts();
@@ -17,15 +17,17 @@ export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
17
17
  * name is gone, the spellchecker finds a neighbour, and the CLI runs something
18
18
  * the user never asked for instead of saying the command is gone.
19
19
  *
20
- * `set` moved under `agents models`/`agents config` (RUSH-2579); `share` moved
21
- * under `agents artifacts share` (RUSH-2580). login/logout/budget/bench/mine/
20
+ * `set` moved under `agents models`/`agents config` (RUSH-2579); `share`,
21
+ * `unshare`, and the whole `artifacts` group moved OUT to the standalone
22
+ * `artifacts` CLI (`@phnx-labs/artifacts-cli`, PHNX-3992) — artifact sharing is
23
+ * no longer an agents-cli surface at all. login/logout/budget/bench/mine/
22
24
  * cost/output/profiles/snapshot/cp/resume/roster moved under nested homes
23
25
  * (cli-surface-consolidate). `timeline` was removed as a duplicated surface —
24
26
  * use `agents feed --filter updates` (RUSH-2692). `status` moved under
25
27
  * `agents sync status` (RUSH-2864). `tickets` was removed — use `linear`
26
28
  * (linear-cli) (RUSH-2932). `alias` moved under `agents setup alias` (RUSH-2965).
27
- * `inbox` was a pure alias of `agents feed` (RUSH-2984). `unshare` nested under
28
- * `agents artifacts unshare` (RUSH-2989). `audit` nested under `agents events audit`.
29
+ * `inbox` was a pure alias of `agents feed` (RUSH-2984). `audit` nested under
30
+ * `agents events audit`.
29
31
  * `trends` was removed with the insights recipe collapse — the one counter
30
32
  * surface is `agents insights mix` (PHNX-3391). `serve` (the
31
33
  * read-only local web companion + `--control` anchor) was removed with the
@@ -2,14 +2,14 @@ const LOADED_COMMAND_NAMES = [
2
2
  'accounts', 'auth', 'view', 'inspect', 'feedback', 'commands', 'hooks', 'skills', 'rules', 'memory',
3
3
  'permissions', 'mcp', 'clis', 'subagents', 'plugins', 'workflows', 'add', 'use',
4
4
  'remove', 'rm', 'purge', 'update', 'prune', 'import', 'registry', 'search', 'install', 'packages',
5
- 'routines', 'monitors', 'projects', 'run', '_callback', 'open', 'reconnect', 'fork', 'config',
5
+ 'routines', 'monitors', 'projects', 'run', '_callback', 'open', 'fork', 'config',
6
6
  'models', 'modes', 'trash', 'restore', 'doctor',
7
7
  'route', 'routes', 'harness', 'harnesses', 'secrets', 'menubar', 'sync',
8
8
  'refresh-rules', 'factory', 'insights', 'trace', 'reminders',
9
9
  'tmux', 'watchdog', 'browser', 'computer', 'logs', 'events',
10
10
  'ssh', 'devices', 'fleet', 'repos', 'repo', 'setup', 'uninstall', 'upgrade', 'sessions',
11
- 'teams', 'cloud', 'message', 'send', 'notify', 'feed',
12
- 'mailboxes', 'mailbox', 'artifacts', 'webhooks',
11
+ 'teams', 'cloud', 'message', 'send', 'feed',
12
+ 'mailboxes', 'mailbox', 'webhooks',
13
13
  'humans', 'daemon', 'traces',
14
14
  ];
15
15
  const INLINE_COMMAND_NAMES = [
@@ -37,15 +37,17 @@ export const KNOWN_TOP_LEVEL_COMMANDS = new Set([
37
37
  * name is gone, the spellchecker finds a neighbour, and the CLI runs something
38
38
  * the user never asked for instead of saying the command is gone.
39
39
  *
40
- * `set` moved under `agents models`/`agents config` (RUSH-2579); `share` moved
41
- * under `agents artifacts share` (RUSH-2580). login/logout/budget/bench/mine/
40
+ * `set` moved under `agents models`/`agents config` (RUSH-2579); `share`,
41
+ * `unshare`, and the whole `artifacts` group moved OUT to the standalone
42
+ * `artifacts` CLI (`@phnx-labs/artifacts-cli`, PHNX-3992) — artifact sharing is
43
+ * no longer an agents-cli surface at all. login/logout/budget/bench/mine/
42
44
  * cost/output/profiles/snapshot/cp/resume/roster moved under nested homes
43
45
  * (cli-surface-consolidate). `timeline` was removed as a duplicated surface —
44
46
  * use `agents feed --filter updates` (RUSH-2692). `status` moved under
45
47
  * `agents sync status` (RUSH-2864). `tickets` was removed — use `linear`
46
48
  * (linear-cli) (RUSH-2932). `alias` moved under `agents setup alias` (RUSH-2965).
47
- * `inbox` was a pure alias of `agents feed` (RUSH-2984). `unshare` nested under
48
- * `agents artifacts unshare` (RUSH-2989). `audit` nested under `agents events audit`.
49
+ * `inbox` was a pure alias of `agents feed` (RUSH-2984). `audit` nested under
50
+ * `agents events audit`.
49
51
  * `trends` was removed with the insights recipe collapse — the one counter
50
52
  * surface is `agents insights mix` (PHNX-3391). `serve` (the
51
53
  * read-only local web companion + `--control` anchor) was removed with the
@@ -81,6 +83,7 @@ export const RETIRED_TOP_LEVEL_COMMANDS = new Set([
81
83
  'roster',
82
84
  'set',
83
85
  'share',
86
+ 'artifacts',
84
87
  'timeline',
85
88
  'status',
86
89
  'tickets',
package/dist/lib/state.js CHANGED
@@ -215,8 +215,8 @@ export function getOptionalUserAgentsDir() {
215
215
  * Origin `owner/repo` slug (lowercased, `.git` stripped) of a git checkout, or
216
216
  * null when `dir` isn't a git repo / has no origin. Extracts the slug from any
217
217
  * remote URL form: `git@host:owner/repo.git`, `https://host/owner/repo.git`,
218
- * `ssh://git@host/owner/repo`. Sync (mirrors readGitConfigUser in git.ts) so it
219
- * can be used from the synchronous getProjectAgentsDir walk.
218
+ * `ssh://git@host/owner/repo`. Sync (uses `execFileSync`, not the async git
219
+ * helpers) so it can be used from the synchronous getProjectAgentsDir walk.
220
220
  */
221
221
  function gitOriginSlug(dir) {
222
222
  let url;
@@ -13,8 +13,8 @@
13
13
  * covers — those differ (share uses an email-handle namespace with public reads
14
14
  * and covers; a traces/sessions adapter uses the userId and differs again). Each
15
15
  * surface keeps its own DISCRIMINATED, typed adapter that reads this decision and
16
- * returns its own `kind`-tagged backend. See `lib/share/backend.ts` and
17
- * `lib/traces/backend.ts` for the two adapters.
16
+ * returns its own `kind`-tagged backend. See `lib/traces/backend.ts` for a
17
+ * concrete adapter.
18
18
  */
19
19
  import { type PhoenixSession } from '../identity/client.js';
20
20
  /** The two legitimate principals any managed-capable surface can resolve to. */
@@ -13,8 +13,8 @@
13
13
  * covers — those differ (share uses an email-handle namespace with public reads
14
14
  * and covers; a traces/sessions adapter uses the userId and differs again). Each
15
15
  * surface keeps its own DISCRIMINATED, typed adapter that reads this decision and
16
- * returns its own `kind`-tagged backend. See `lib/share/backend.ts` and
17
- * `lib/traces/backend.ts` for the two adapters.
16
+ * returns its own `kind`-tagged backend. See `lib/traces/backend.ts` for a
17
+ * concrete adapter.
18
18
  */
19
19
  import { readSession } from '../identity/client.js';
20
20
  /**
@@ -15,6 +15,7 @@
15
15
  * arrives with `agents secrets vault unlock`, #366/#367)
16
16
  * reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
17
17
  */
18
+ import type { ResourceSelection } from './installations/versions.js';
18
19
  /** The umbrella flags off `agents sync`. */
19
20
  export interface UmbrellaFlags {
20
21
  repos?: boolean;
@@ -87,6 +88,10 @@ interface RunUmbrellaArgs {
87
88
  * `agents sync --json` / fleet fan-out leave stdout as a single JSON object.
88
89
  */
89
90
  quiet?: boolean;
91
+ /** Optional resource-only reconcile across installed versions. */
92
+ selection?: ResourceSelection;
93
+ /** Explicit consent for selected plugins that add executable surfaces. */
94
+ allowExecSurfaces?: boolean;
90
95
  }
91
96
  /**
92
97
  * Execute the planned stages in order: repos -> secrets -> reconcile.
@@ -53,7 +53,7 @@ export function planUmbrellaStages(f) {
53
53
  * reconcile — `agents sync` should make as much current as it can in one pass.
54
54
  */
55
55
  export async function runUmbrellaSync(args) {
56
- const { flags, log, yes, passphrase, quiet = false } = args;
56
+ const { flags, log, yes, passphrase, quiet = false, selection, allowExecSurfaces = false } = args;
57
57
  const plan = planUmbrellaStages(flags);
58
58
  const result = { plan, reconciled: false, declined: [], reconciledVersions: [] };
59
59
  if (plan.fetchRepos) {
@@ -131,7 +131,13 @@ export async function runUmbrellaSync(args) {
131
131
  }
132
132
  if (plan.reconcile) {
133
133
  const { refresh } = await import('./refresh.js');
134
- const refreshed = await refresh({ skipPrompts: yes, quiet });
134
+ const refreshed = await refresh({
135
+ skipPrompts: yes,
136
+ quiet,
137
+ skipClis: selection !== undefined,
138
+ selection,
139
+ allowExecSurfaces,
140
+ });
135
141
  result.reconciled = true;
136
142
  result.declined = refreshed.declined;
137
143
  result.reconciledVersions = refreshed.reconciled;
@@ -140,14 +146,16 @@ export async function runUmbrellaSync(args) {
140
146
  // rather than silently adding them (refresh mode). Soft: a machine without
141
147
  // tailscale is a clean no-op, never a sync failure. First-run population is
142
148
  // `agents setup` / manual `agents devices sync` (bootstrap).
143
- const { runDeviceSync } = await import('./devices/sync.js');
144
- const { reconcilePendingSentinels } = await import('./devices/pending.js');
145
- const dev = await runDeviceSync({ soft: true, mode: 'refresh' });
146
- if (dev.ok)
147
- await reconcilePendingSentinels(dev.pending);
148
- result.devices = { synced: dev.synced, pending: dev.pending.length, skipped: !dev.ok };
149
- if (dev.ok) {
150
- log(`devices: ${dev.synced} refreshed${dev.pending.length ? `, ${dev.pending.length} new pending` : ''}`);
149
+ if (!selection) {
150
+ const { runDeviceSync } = await import('./devices/sync.js');
151
+ const { reconcilePendingSentinels } = await import('./devices/pending.js');
152
+ const dev = await runDeviceSync({ soft: true, mode: 'refresh' });
153
+ if (dev.ok)
154
+ await reconcilePendingSentinels(dev.pending);
155
+ result.devices = { synced: dev.synced, pending: dev.pending.length, skipped: !dev.ok };
156
+ if (dev.ok) {
157
+ log(`devices: ${dev.synced} refreshed${dev.pending.length ? `, ${dev.pending.length} new pending` : ''}`);
158
+ }
151
159
  }
152
160
  }
153
161
  return result;
@@ -0,0 +1,24 @@
1
+ /** The subset of the `term` CLI a screen-scraping drive loop needs — faked in tests. */
2
+ export interface TermDriver {
3
+ start(opts?: {
4
+ rows?: number;
5
+ cols?: number;
6
+ }): Promise<string>;
7
+ exec(id: string, command: string): Promise<void>;
8
+ write(id: string, input: string): Promise<void>;
9
+ screen(id: string): Promise<{
10
+ screen: string;
11
+ exited: boolean;
12
+ }>;
13
+ stop(id: string): Promise<void>;
14
+ }
15
+ /** Real driver over the standalone `term` CLI (`./term-client.js`). */
16
+ export declare function defaultTermDriver(): TermDriver;
17
+ export interface DriveOptions {
18
+ /** Wait after launching before steering / scraping (default 4000ms). */
19
+ initialDelayMs?: number;
20
+ /** Poll cadence for the scrape loop (default 1000ms). */
21
+ pollMs?: number;
22
+ /** Overall scrape deadline (default 90000ms). */
23
+ timeoutMs?: number;
24
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Injectable terminal driver over the standalone `term` CLI.
3
+ *
4
+ * A small seam that lets a flow drive a PTY session — start, exec, write,
5
+ * screen-scrape, stop — through the real `term` CLI in production and a fake in
6
+ * tests. The setup-token mint (`lib/auth-mint.ts`) is the sole consumer today.
7
+ */
8
+ import { termStart, termExec, termWrite, termScreen, termStop } from './term-client.js';
9
+ /** Real driver over the standalone `term` CLI (`./term-client.js`). */
10
+ export function defaultTermDriver() {
11
+ const expectOk = (res, what) => {
12
+ if (!res.ok)
13
+ throw new Error(`term ${what} failed: ${res.error ?? 'unknown'}`);
14
+ };
15
+ return {
16
+ async start(opts) {
17
+ const res = await termStart({ rows: opts?.rows ?? 40, cols: opts?.cols ?? 120 });
18
+ expectOk(res, 'start');
19
+ return res.id;
20
+ },
21
+ async exec(id, command) {
22
+ expectOk(await termExec(id, command), 'exec');
23
+ },
24
+ async write(id, input) {
25
+ expectOk(await termWrite(id, input), 'write');
26
+ },
27
+ async screen(id) {
28
+ const res = await termScreen(id);
29
+ expectOk(res, 'screen');
30
+ return { screen: res.screen ?? '', exited: Boolean(res.exited) };
31
+ },
32
+ async stop(id) {
33
+ await termStop(id).catch(() => undefined);
34
+ },
35
+ };
36
+ }
@@ -14,7 +14,7 @@ export { makeVscodiumAgentBackend, spawnUri, EDITOR_VARIANTS, type EditorVariant
14
14
  export { planLayouts, type Packing } from './policy.js';
15
15
  export { specForRequest, buildRequests, openSurface, openSurfaces, type OpenOptions, type OpenManyOptions, type BuildRequestsOptions, type SurfaceItem, } from './engine.js';
16
16
  export { runLocal, runRemote, runSpec, remoteCommand, type HostResolver, type RunResult } from './transport.js';
17
- export { injectIntoTerminal, tmuxSendKeysArgv, tmuxInjectSpecs, itermInjectScript, ghosttyInjectScript, appleScriptInjectSpec, vscodiumInjectUri, vscodiumInjectSpec, type InjectTarget, type InjectBackend, type InjectOptions, type InjectResult, } from './inject.js';
17
+ export { backendCarriesPaste, injectIntoTerminal, tmuxSendKeysArgv, tmuxInjectSpecs, itermInjectScript, ghosttyInjectScript, appleScriptInjectSpec, vscodiumInjectUri, vscodiumInjectSpec, type InjectTarget, type InjectBackend, type InjectOptions, type InjectResult, } from './inject.js';
18
18
  export { resolveInjectTarget, resolveInjectTargetForSession, type InjectResolution, type InjectRail, type ResolveOptions, } from './resolve.js';
19
19
  export { iLoginShell } from './shell.js';
20
20
  export { shellQuote } from './quote.js';
@@ -5,7 +5,7 @@ export { makeVscodiumAgentBackend, spawnUri, EDITOR_VARIANTS } from './backends/
5
5
  export { planLayouts } from './policy.js';
6
6
  export { specForRequest, buildRequests, openSurface, openSurfaces, } from './engine.js';
7
7
  export { runLocal, runRemote, runSpec, remoteCommand } from './transport.js';
8
- export { injectIntoTerminal, tmuxSendKeysArgv, tmuxInjectSpecs, itermInjectScript, ghosttyInjectScript, appleScriptInjectSpec, vscodiumInjectUri, vscodiumInjectSpec, } from './inject.js';
8
+ export { backendCarriesPaste, injectIntoTerminal, tmuxSendKeysArgv, tmuxInjectSpecs, itermInjectScript, ghosttyInjectScript, appleScriptInjectSpec, vscodiumInjectUri, vscodiumInjectSpec, } from './inject.js';
9
9
  export { resolveInjectTarget, resolveInjectTargetForSession, } from './resolve.js';
10
10
  export { iLoginShell } from './shell.js';
11
11
  export { shellQuote } from './quote.js';
@@ -88,6 +88,21 @@ export interface InjectOptions {
88
88
  ctx?: EngineContext;
89
89
  /** Don't execute — return the spec(s) that WOULD run. Lets the macOS paths be asserted on Linux. */
90
90
  dryRun?: boolean;
91
+ /**
92
+ * Hard bound for the WHOLE injection, in ms. Each spec is run under the
93
+ * remaining budget with process-group cancellation, and a spec is never
94
+ * STARTED once the budget is gone — an operator-facing caller must be able to
95
+ * return a verdict on time without leaving a launcher running behind it
96
+ * (PHNX-3999). Omitted keeps the historical unbounded behaviour.
97
+ */
98
+ deadlineMs?: number;
99
+ /**
100
+ * Frame the text in bracketed-paste markers so a TUI inserts it verbatim
101
+ * instead of reading each embedded newline as a submit. Required for a
102
+ * multiline free-text answer. Only the tmux rail can carry it (see
103
+ * {@link backendCarriesPaste}); every other backend refuses with an error.
104
+ */
105
+ paste?: boolean;
91
106
  }
92
107
  export interface InjectResult {
93
108
  ok: boolean;
@@ -106,10 +121,33 @@ export interface InjectResult {
106
121
  confirmed: boolean;
107
122
  /** Discrete writes delivered: 2 for the Ink-safe text+Enter split, 1 when combined or enter=false. */
108
123
  writes: number;
124
+ /**
125
+ * How many specs were STARTED. This -- not `writes` -- is what says whether
126
+ * anything may have reached the terminal: a spec that failed or timed out may
127
+ * still have written bytes before dying, so only `started === 0` proves
128
+ * nothing landed and the answer is cleanly retryable (PHNX-3999).
129
+ */
130
+ started: number;
109
131
  /** For the tmux / AppleScript backends (or any dryRun), the spec(s) that ran / would run. */
110
132
  specs?: LaunchSpec[];
111
133
  error?: string;
112
134
  }
135
+ /**
136
+ * Bracketed-paste framing (DEC mode 2004) — the pty contract for "insert this
137
+ * verbatim, do not submit". A readline/Ink composer that sees the start marker
138
+ * buffers every byte up to the end marker, so an embedded newline lands as a
139
+ * literal newline in the draft instead of submitting the partial line.
140
+ */
141
+ export declare const BRACKETED_PASTE_START = "\u001B[200~";
142
+ export declare const BRACKETED_PASTE_END = "\u001B[201~";
143
+ /**
144
+ * Only tmux can carry the framing: `send-keys -l` writes the marker bytes
145
+ * straight to the pty. ghostty simulates keystrokes, vscodium hands a JSON
146
+ * payload to the extension, and iTerm's AppleScript `write text` cannot encode
147
+ * a raw ESC byte — so a paste request on those is refused rather than
148
+ * half-delivered as one submit per line.
149
+ */
150
+ export declare function backendCarriesPaste(backend: InjectBackend): boolean;
113
151
  /**
114
152
  * argv for `tmux send-keys` targeting a pane by id. Starts with `tmux` (like the
115
153
  * engine's `tmuxTabArgv`) so the same transport runs it locally or over SSH. The