@phnx-labs/agents-cli 1.22.46 → 1.22.49

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 (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -10,17 +10,39 @@ import { basename } from 'node:path';
10
10
  import { execFileSync } from 'node:child_process';
11
11
  import { randomBytes } from 'node:crypto';
12
12
  import { hostname as osHostname } from 'node:os';
13
- import { readShareConfig, readWriteToken } from './config.js';
13
+ import { readShareConfig } from './config.js';
14
14
  import { resolveGitHubUsername } from '../git.js';
15
+ import { resolveShareBackend, sanitizeShareNamespace } from './backend.js';
15
16
  import { captureCover, OG_WIDTH, OG_HEIGHT, OG_SCALE } from './capture.js';
16
17
  import { deriveMeta, injectOgMeta } from './og.js';
17
18
  import { injectAnalyticsBeacon } from './analytics.js';
19
+ import { prepareShareHtml } from './html.js';
20
+ /** `--unlisted` / `{ unlisted: true }` map to `unlisted`; otherwise `visibility` (default public). */
21
+ export function resolveShareVisibility(opts = {}) {
22
+ if (opts.unlisted === true)
23
+ return 'unlisted';
24
+ if (opts.visibility === 'unlisted')
25
+ return 'unlisted';
26
+ return 'public';
27
+ }
18
28
  /**
19
- * Reserved `customMetadata` keys the CLI sets automatically from the exec env,
20
- * git, and the local clock (plus `label`/`label-source`) a `--meta key=value`
21
- * may not target any of these (see {@link parseMetaEntries}).
29
+ * Reserved `customMetadata` keys a `--meta key=value` may not target (see
30
+ * {@link parseMetaEntries}). Matches the Worker's `RESERVED_METADATA_KEYS`:
31
+ * provenance the CLI sets automatically, plus `expires-at` / `visibility` /
32
+ * `owner` which the Worker stamps itself.
22
33
  */
23
- export const RESERVED_META_KEYS = ['agent', 'session', 'host', 'repo', 'date', 'label', 'label-source'];
34
+ export const RESERVED_META_KEYS = [
35
+ 'expires-at',
36
+ 'visibility',
37
+ 'owner',
38
+ 'agent',
39
+ 'session',
40
+ 'host',
41
+ 'repo',
42
+ 'date',
43
+ 'label',
44
+ 'label-source',
45
+ ];
24
46
  const META_KEY_RE = /^[a-z0-9-]{1,64}$/;
25
47
  /**
26
48
  * Auto-capture publish provenance from the exec env, git, and the local clock.
@@ -43,8 +65,9 @@ export function resolveShareProvenance(opts = {}) {
43
65
  /**
44
66
  * Parse repeated `--meta key=value` CLI args into a validated metadata record.
45
67
  * Keys are lowercase `[a-z0-9-]`, up to 64 characters, and may not collide with
46
- * {@link RESERVED_META_KEYS} (the provenance the CLI sets automatically, plus
47
- * label/label-source) — throws naming the offending pair on any violation.
68
+ * {@link RESERVED_META_KEYS} (Worker-stamped owner/visibility/expires-at, plus
69
+ * the provenance the CLI sets automatically, plus label/label-source) — throws
70
+ * naming the offending pair on any violation.
48
71
  */
49
72
  export function parseMetaEntries(pairs) {
50
73
  const meta = {};
@@ -59,7 +82,7 @@ export function parseMetaEntries(pairs) {
59
82
  throw new Error(`Bad --meta key '${key}'. Keys are lowercase letters, digits, and hyphens, up to 64 characters.`);
60
83
  }
61
84
  if (RESERVED_META_KEYS.includes(key)) {
62
- throw new Error(`--meta ${key}=… is reserved (set automatically from your session/git) — pass a different key.`);
85
+ throw new Error(`--meta ${key}=… is reserved (Worker-stamped, or set automatically from your session/git) — pass a different key.`);
63
86
  }
64
87
  meta[key] = value;
65
88
  }
@@ -293,7 +316,7 @@ export function slugify(name) {
293
316
  .slice(0, 60) || 'page');
294
317
  }
295
318
  function sanitizeSlugPart(s) {
296
- return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
319
+ return sanitizeShareNamespace(s);
297
320
  }
298
321
  /** The current repo's name, or undefined outside a git checkout — never a
299
322
  * fallback guess, since callers that want one (detectProject) supply it themselves. */
@@ -317,18 +340,19 @@ export function detectProject(dir = process.cwd()) {
317
340
  return gitRepoName(dir) ?? (sanitizeSlugPart(basename(dir)) || 'share');
318
341
  }
319
342
  /**
320
- * Notion-style default slug: `<project>-<feature>-<16hex>`. Project scopes the link
321
- * to the repo the agent is in; the random tail keeps it unguessable + collision-free.
322
- * A leading `plan-` on the filename is dropped (it's redundant under the project).
343
+ * Readable slug + view id: `<feature>-<16hex>`. A leading `plan-` on the
344
+ * filename is dropped. The project no longer prefixes the slug the URL
345
+ * namespace is already the publisher's handle (`/<you>/<slug>`).
323
346
  *
324
347
  * The tail is 8 random bytes (64-bit, 16 hex chars). Reads are public — the URL is
325
348
  * the only capability — so the nonce must be genuinely infeasible to brute-force,
326
- * not merely unlisted; 64 bits puts a blind guess out of reach. (See docs/share.md
349
+ * not merely unlisted; 64 bits puts a blind guess out of reach. (See docs/distribution.md
327
350
  * §Security for the threat model and `--expire` for sensitive content.)
351
+ * `--slug` still publishes that exact slug, no view-id appended.
328
352
  */
329
- export function defaultSlug(filePath, dir) {
353
+ export function defaultSlug(filePath, _dir) {
330
354
  const feature = slugify(filePath).replace(/^plan-/, '') || 'page';
331
- return `${detectProject(dir)}-${feature}-${randomBytes(8).toString('hex')}`;
355
+ return `${feature}-${randomBytes(8).toString('hex')}`;
332
356
  }
333
357
  function guessContentType(filePath) {
334
358
  if (/\.html?$/i.test(filePath))
@@ -419,14 +443,12 @@ export function buildShareKey(username, slugPart) {
419
443
  return `${user}/${part}`;
420
444
  }
421
445
  export async function publishFile(filePath, opts = {}) {
422
- const cfg = opts.config ?? readShareConfig();
423
- if (!cfg) {
424
- throw new Error("Not set up yet. Run 'agents artifacts setup' (provision your own endpoint) or 'agents artifacts share join' (use an existing one).");
425
- }
426
- const token = opts.writeToken ?? readWriteToken();
427
- const username = await resolveShareUsername(opts);
428
- const analyticsToken = opts.analyticsToken ?? cfg.analyticsToken;
429
- return publishToEndpoint(filePath, { baseUrl: cfg.baseUrl, token }, {
446
+ const backend = resolveShareBackend(opts);
447
+ const username = backend.kind === 'managed'
448
+ ? backend.namespace
449
+ : await resolveShareUsername({ githubUser: opts.githubUser || backend.namespace || undefined });
450
+ const analyticsToken = opts.analyticsToken ?? (backend.kind === 'byo' ? (opts.config ?? readShareConfig())?.analyticsToken : undefined);
451
+ return publishToEndpoint(filePath, { baseUrl: backend.baseUrl, token: backend.token }, {
430
452
  ...opts,
431
453
  githubUser: username,
432
454
  analyticsToken,
@@ -437,7 +459,8 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
437
459
  const slugPart = (opts.slug ?? defaultSlug(filePath)).replace(/^\/+/, '');
438
460
  const key = buildShareKey(username, slugPart);
439
461
  const expiresAt = resolveExpire(opts.expire);
440
- const unlisted = opts.unlisted === true;
462
+ const visibility = resolveShareVisibility(opts);
463
+ const unlisted = visibility === 'unlisted';
441
464
  const pageUrl = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}`;
442
465
  const provenance = opts.provenance ?? resolveShareProvenance();
443
466
  const meta = opts.meta ?? {};
@@ -449,6 +472,9 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
449
472
  let body = readFileSync(filePath);
450
473
  let coverUrl;
451
474
  const isHtml = /\.html?$/i.test(filePath);
475
+ if (isHtml) {
476
+ body = Buffer.from(prepareShareHtml(body.toString('utf8'), filePath), 'utf8');
477
+ }
452
478
  const explicitLabel = opts.label?.trim();
453
479
  // sanitizeLabel here (not just inside deriveLabel) covers an explicit
454
480
  // --label/--title the caller typed with an embedded newline — deriveLabel
@@ -492,8 +518,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
492
518
  const h = { authorization: `Bearer ${endpoint.token}`, 'content-type': contentType };
493
519
  if (expiresAt)
494
520
  h['x-share-expires-at'] = expiresAt;
495
- if (unlisted)
496
- h['x-share-visibility'] = 'unlisted';
521
+ h['x-share-visibility'] = visibility;
497
522
  // Every free-text header goes through toHeaderValue: a non-latin1 code point
498
523
  // anywhere in a label, a repo name, or a --meta value throws inside fetch and
499
524
  // crashes the publish outright.
@@ -541,6 +566,9 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
541
566
  }
542
567
  const r = await put(pageUrl, body, authHeaders(opts.contentType ?? guessContentType(filePath)));
543
568
  if (!r.ok) {
569
+ if (r.status === 409) {
570
+ throw new Error(`Handle '${username}' is already claimed by another account. The public URL namespace is the email local-part; two Phoenix users cannot share it.`);
571
+ }
544
572
  throw new Error(`Publish failed (${r.status}) for ${pageUrl}. Check the write token, or that 'agents artifacts setup' completed.`);
545
573
  }
546
574
  return {
@@ -549,6 +577,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
549
577
  coverUrl,
550
578
  label,
551
579
  labelSource,
580
+ visibility,
552
581
  ...(unlisted ? { unlisted: true } : {}),
553
582
  };
554
583
  }
@@ -1,8 +1,21 @@
1
1
  // The Cloudflare Worker that fronts the R2 share bucket.
2
2
  //
3
3
  // One tiny Worker does both sides:
4
- // - PUT /<username>/<slug> — bearer-gated (WRITE_TOKEN secret); writes the body
5
- // to R2 via the BUCKET binding, storing an optional expires-at value plus
4
+ // - PUT /<username>/<slug> — bearer-gated by TWO principals (not a fallback):
5
+ // 1. Static WRITE_TOKEN (BYO Cloudflare) checked first when the presented
6
+ // bearer equals env.WRITE_TOKEN. owner = env.SHARE_NAMESPACE or the
7
+ // path's first segment.
8
+ // 2. Phoenix bearer — otherwise GET ${env.PHOENIX_ID_BASE}/api/v1/auth/me
9
+ // with that bearer → {userId,email}; 401 if absent/invalid.
10
+ // customMetadata.owner = userId (stable). The path's first segment MUST
11
+ // equal handleFromEmail(email) (the public handle, e.g. muqsitnawaz),
12
+ // so one user cannot write another's prefix (403 namespace mismatch).
13
+ // A __handles/<handle> claim binds the handle to the first userId that
14
+ // writes it; a different userId gets 409 handle taken.
15
+ // A managed deployment sets PHOENIX_ID_BASE; BYO sets WRITE_TOKEN; the
16
+ // platform endpoint may set both. Fail loud (401) when neither authenticates.
17
+ // Writes the body to R2 via the BUCKET binding, storing visibility
18
+ // (public|unlisted; org → 400 in P1), owner, an optional expires-at, plus
6
19
  // provenance (agent/session/host/repo/date), a label, and any `--meta`
7
20
  // entries in object metadata. Overwriting an existing slug first copies the
8
21
  // current object to <slug>/rev-<ts>-<rand> (revision history) unless
@@ -39,6 +52,10 @@
39
52
  export function renderWorkerScript() {
40
53
  return `// GENERATED by agents-cli agents share setup — do not edit here; edit
41
54
  // src/lib/share/worker-template.ts and re-run setup.
55
+ export const hooks = {
56
+ verifyPhoenixToken: defaultVerifyPhoenixToken,
57
+ };
58
+
42
59
  export default {
43
60
  async fetch(request, env) {
44
61
  const url = new URL(request.url);
@@ -50,15 +67,30 @@ export default {
50
67
  });
51
68
  }
52
69
 
70
+ const firstSeg = path.split('/').filter(Boolean)[0] || '';
71
+ if ((request.method === 'GET' || request.method === 'HEAD') && firstSeg.startsWith('__')) {
72
+ return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
73
+ }
74
+
53
75
  if (request.method === 'PUT') {
54
- const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
55
- if (!env.WRITE_TOKEN || !safeEqual(presented, env.WRITE_TOKEN)) {
56
- return json({ error: 'unauthorized' }, 401);
76
+ const auth = await authorizeWrite(request, env);
77
+ if (auth.error) return auth.error;
78
+ const vis = normalizeVisibility(request.headers.get('x-share-visibility'));
79
+ if (vis.error) return vis.error;
80
+ const visibility = vis.value;
81
+ const segments = path.split('/').filter(Boolean);
82
+ if (auth.kind === 'phoenix') {
83
+ const expected = phoenixHandle(auth);
84
+ if (!expected || segments[0] !== expected) {
85
+ return json({ error: 'namespace mismatch', owner: expected }, 403);
86
+ }
87
+ const claimed = await claimHandle(env.BUCKET, expected, auth.owner);
88
+ if (claimed.error) return claimed.error;
57
89
  }
58
90
  const expiresAt = request.headers.get('x-share-expires-at') || '';
59
91
  // 'unlisted' hides the page from the public gallery + JSON listing while
60
92
  // keeping the direct URL world-readable (capability URL, not secret).
61
- const visibility = (request.headers.get('x-share-visibility') || '').toLowerCase();
93
+ // GET also sends X-Robots-Tag: noindex for unlisted objects.
62
94
  const contentType = request.headers.get('content-type') || 'text/html; charset=utf-8';
63
95
  // Provenance (RUSH-2683): captured client-side from the exec env/git/clock,
64
96
  // never invented here — a header is simply absent when the CLI had nothing
@@ -93,7 +125,12 @@ export default {
93
125
  delete customMetadata[reservedKey];
94
126
  }
95
127
  if (expiresAt) customMetadata['expires-at'] = expiresAt;
96
- if (visibility === 'unlisted') customMetadata['visibility'] = 'unlisted';
128
+ customMetadata['visibility'] = visibility;
129
+ const owner =
130
+ auth.kind === 'phoenix'
131
+ ? auth.owner
132
+ : (env.SHARE_NAMESPACE || segments[0] || 'byo');
133
+ if (owner) customMetadata['owner'] = owner;
97
134
  if (agent) customMetadata['agent'] = agent;
98
135
  if (session) customMetadata['session'] = session;
99
136
  if (host) customMetadata['host'] = host;
@@ -129,7 +166,7 @@ export default {
129
166
  httpMetadata: { contentType },
130
167
  customMetadata,
131
168
  });
132
- return json({ ok: true, url: url.origin + '/' + path, expiresAt: expiresAt || null, unlisted: visibility === 'unlisted' }, 200);
169
+ return json({ ok: true, url: url.origin + '/' + path, expiresAt: expiresAt || null, unlisted: visibility === 'unlisted', visibility }, 200);
133
170
  }
134
171
 
135
172
  if (request.method === 'GET' || request.method === 'HEAD') {
@@ -174,12 +211,32 @@ export default {
174
211
  headers.set('etag', obj.httpEtag);
175
212
  if (!headers.has('content-type')) headers.set('content-type', 'text/html; charset=utf-8');
176
213
  headers.set('cache-control', 'public, max-age=60');
214
+ if (obj.customMetadata && obj.customMetadata.visibility === 'unlisted') {
215
+ headers.set('X-Robots-Tag', 'noindex');
216
+ }
177
217
  return new Response(request.method === 'HEAD' ? null : obj.body, { status: 200, headers });
178
218
  }
179
219
 
180
220
  if (request.method === 'DELETE') {
181
- const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
182
- if (!env.WRITE_TOKEN || !safeEqual(presented, env.WRITE_TOKEN)) return json({ error: 'unauthorized' }, 401);
221
+ const auth = await authorizeWrite(request, env);
222
+ if (auth.error) return auth.error;
223
+ if (auth.kind === 'phoenix') {
224
+ const handle = phoenixHandle(auth);
225
+ const uid = sanitizeNamespace(auth.owner);
226
+ const delSegments = path.split('/').filter(Boolean);
227
+ // Handle is the public namespace; userId prefix is still deletable so
228
+ // P1 UUID-namespaced objects the same account published can be taken down.
229
+ // A colliding local-part (same handle, different userId) must not be
230
+ // able to DELETE the claimant's pages — consult the claim the same way PUT does.
231
+ if (delSegments[0] && delSegments[0] === uid) {
232
+ // own leftover UUID prefix
233
+ } else if (delSegments[0] && delSegments[0] === handle) {
234
+ const owned = await assertHandleOwner(env.BUCKET, handle, auth.owner);
235
+ if (owned.error) return owned.error;
236
+ } else {
237
+ return json({ error: 'namespace mismatch', owner: handle || uid }, 403);
238
+ }
239
+ }
183
240
  await env.BUCKET.delete(path);
184
241
  return json({ ok: true, deleted: path }, 200);
185
242
  }
@@ -347,7 +404,7 @@ async function renderRevisions(bucket, origin, key, method) {
347
404
  // before it ever reaches this Worker; see RESERVED_META_KEYS in publish.ts).
348
405
  // One list, reused both to strip a same-named --meta collision on write and
349
406
  // to split arbitrary --meta entries back out on read.
350
- var RESERVED_METADATA_KEYS = ['expires-at', 'visibility', 'agent', 'session', 'host', 'repo', 'date', 'label', 'label-source'];
407
+ var RESERVED_METADATA_KEYS = ['expires-at', 'visibility', 'owner', 'agent', 'session', 'host', 'repo', 'date', 'label', 'label-source'];
351
408
 
352
409
  // Everything in customMetadata that ISN'T one of the reserved provenance/label
353
410
  // keys above — i.e. the caller's own \`--meta key=value\` entries. Surfaced on
@@ -383,5 +440,108 @@ function safeEqual(a, b) {
383
440
  for (let i = 0; i < la; i++) out |= a.charCodeAt(i) ^ b.charCodeAt(i % lb || 0);
384
441
  return out === 0 && la === lb;
385
442
  }
443
+
444
+ // URL-safety, not collision-resistance. Two values that differ only in
445
+ // case/punctuation share a prefix.
446
+ function sanitizeNamespace(s) {
447
+ return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
448
+ }
449
+
450
+ // Public handle: email local-part, plus-tag stripped. Must match CLI handleFromEmail.
451
+ function handleFromEmail(email) {
452
+ if (!email) return '';
453
+ const local = String(email).split('@')[0] || '';
454
+ const beforePlus = local.split('+')[0] || local;
455
+ return sanitizeNamespace(beforePlus);
456
+ }
457
+
458
+ function phoenixHandle(auth) {
459
+ return handleFromEmail(auth.email) || sanitizeNamespace(auth.owner);
460
+ }
461
+
462
+ // First writer of a handle owns it. Later PUTs from the same userId are fine;
463
+ // a different userId whose email local-part collides gets 409, not a silent overwrite.
464
+ async function assertHandleOwner(bucket, handle, userId) {
465
+ // Pages under the handle already stamp customMetadata.owner = userId.
466
+ // Treat that as the source of truth so a colliding local-part cannot
467
+ // PUT/DELETE another account's objects even if the claim object is missing.
468
+ const list = await bucket.list({ prefix: handle + '/', include: ['customMetadata'] });
469
+ for (const o of list.objects || []) {
470
+ const owner = o.customMetadata && o.customMetadata.owner;
471
+ if (owner && owner !== userId) {
472
+ return { error: json({ error: 'handle taken', handle: handle }, 409) };
473
+ }
474
+ }
475
+ const key = '__handles/' + handle;
476
+ const existing = await bucket.get(key);
477
+ if (!existing) return {};
478
+ const claimed = existing.customMetadata && existing.customMetadata.userId;
479
+ if (claimed && claimed !== userId) {
480
+ return { error: json({ error: 'handle taken', handle: handle }, 409) };
481
+ }
482
+ return {};
483
+ }
484
+
485
+ async function claimHandle(bucket, handle, userId) {
486
+ const owned = await assertHandleOwner(bucket, handle, userId);
487
+ if (owned.error) return owned;
488
+ const key = '__handles/' + handle;
489
+ // Write (or rewrite) so a same-user republish resets object Age against the
490
+ // bucket's 366-day lifecycle — otherwise the claim can expire while pages stay live.
491
+ await bucket.put(key, JSON.stringify({ userId: userId }), {
492
+ httpMetadata: { contentType: 'application/json' },
493
+ customMetadata: { userId: userId, visibility: 'unlisted' },
494
+ });
495
+ return {};
496
+ }
497
+
498
+ // P1 visibility: public | unlisted. 'org' (and anything else) is 400 — org is Phase 2.
499
+ function normalizeVisibility(raw) {
500
+ const v = (raw || '').trim().toLowerCase();
501
+ if (!v || v === 'public') return { value: 'public' };
502
+ if (v === 'unlisted') return { value: 'unlisted' };
503
+ if (v === 'org') return { error: json({ error: 'visibility org is not supported yet' }, 400) };
504
+ return { error: json({ error: 'visibility must be public or unlisted' }, 400) };
505
+ }
506
+
507
+ async function defaultVerifyPhoenixToken(request, env) {
508
+ const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
509
+ if (!presented) return null;
510
+ const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
511
+ if (!base) return null;
512
+ let res;
513
+ try {
514
+ res = await fetch(base + '/api/v1/auth/me', {
515
+ method: 'GET',
516
+ headers: { Authorization: 'Bearer ' + presented },
517
+ });
518
+ } catch {
519
+ return null;
520
+ }
521
+ if (!res.ok) return null;
522
+ let body;
523
+ try {
524
+ body = await res.json();
525
+ } catch {
526
+ return null;
527
+ }
528
+ if (!body || typeof body.userId !== 'string' || !body.userId) return null;
529
+ return { userId: body.userId, email: typeof body.email === 'string' ? body.email : '' };
530
+ }
531
+
532
+ // Two legitimate principals: WRITE_TOKEN equality first (admin/BYO), otherwise
533
+ // the bearer is a Phoenix token and we verify it. 401 when neither authenticates.
534
+ async function authorizeWrite(request, env) {
535
+ const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
536
+ if (!presented) return { error: json({ error: 'unauthorized' }, 401) };
537
+ if (env.WRITE_TOKEN && safeEqual(presented, env.WRITE_TOKEN)) {
538
+ return { kind: 'byo', owner: env.SHARE_NAMESPACE || 'byo' };
539
+ }
540
+ const claims = await hooks.verifyPhoenixToken(request, env);
541
+ if (claims && typeof claims.userId === 'string' && claims.userId) {
542
+ return { kind: 'phoenix', owner: claims.userId, email: typeof claims.email === 'string' ? claims.email : '' };
543
+ }
544
+ return { error: json({ error: 'unauthorized' }, 401) };
545
+ }
386
546
  `;
387
547
  }
@@ -41,10 +41,23 @@ function loadNodeSqlite() {
41
41
  process.emitWarning = original;
42
42
  }
43
43
  }
44
- // Keep Node on createRequire() so Vitest doesn't try to prebundle the built-in
45
- // sqlite module as a userland package during test collection.
44
+ // Keep BOTH runtimes on createRequire() so Vitest doesn't try to prebundle the
45
+ // built-in sqlite module as a userland package during test collection.
46
+ //
47
+ // The Bun arm used to be `await import('bun:sqlite' as string)`, which made this
48
+ // a TOP-LEVEL AWAIT. esbuild cannot lower top-level await to CJS, so every test
49
+ // that spawns a subprocess through `tsx` (CJS mode) died at transform time with
50
+ // `Top-level await is currently not supported with the "cjs" output format` --
51
+ // 49 failures across 7 files, none of which touch sqlite. `require` is
52
+ // synchronous, so the await disappears and with it the whole failure class.
53
+ //
54
+ // The specifier is held in a variable, not written inline: a literal
55
+ // `require('bun:sqlite')` is statically analyzable, so bundlers and Vitest's
56
+ // collector try to resolve a module that does not exist off-Bun. That
57
+ // indirection is what the old `as string` cast was doing for the dynamic import.
58
+ const BUN_SQLITE = 'bun:sqlite';
46
59
  const sqliteMod = isBun
47
- ? await import('bun:sqlite')
60
+ ? require(BUN_SQLITE)
48
61
  : loadNodeSqlite();
49
62
  // bun:sqlite exports `Database`; node:sqlite exports `DatabaseSync`.
50
63
  const NativeDatabase = sqliteMod.Database
@@ -13,6 +13,14 @@
13
13
  * smuggled in as part of a remote command, and `sshExec` additionally rejects a
14
14
  * leading `-` so it can never be parsed as an ssh argv flag.
15
15
  */
16
+ /**
17
+ * ssh's own connection-layer failure code — the transport dropped or never came
18
+ * up, as opposed to the remote command choosing an exit status. Defined here,
19
+ * in the layer that owns the ssh invocation, and re-exported by
20
+ * `lib/hosts/reconnect.ts` as `SSH_CONN_FAILURE` so there is one constant for
21
+ * the concept rather than two that can drift apart.
22
+ */
23
+ export declare const SSH_CONN_FAILURE_CODE = 255;
16
24
  export declare const SSH_TARGET_RE: RegExp;
17
25
  export declare function assertValidSshTarget(host: string): void;
18
26
  /** POSIX single-quote a string for safe interpolation into a remote shell command. */
@@ -156,11 +164,68 @@ export interface SshStreamOptions {
156
164
  /** Additional OpenSSH argv placed before the target (for example `-i <path>`). */
157
165
  extraSshArgs?: string[];
158
166
  }
167
+ /**
168
+ * The DEC private modes a full-screen remote app turns ON and, when it exits
169
+ * cleanly, turns back off itself. A remote agent TUI killed by a dropped link
170
+ * never gets to, so they stay enabled on the LOCAL terminal after ssh is gone:
171
+ * focus reporting (1004) and the mode/colour-scheme reports (996/997) then make
172
+ * the terminal emit answerback sequences at a shell that is not expecting them,
173
+ * which is the `^[[?997;1n ^[[I ^[[O` litter seen after a drop (RUSH-3125). Also
174
+ * resets bracketed paste (2004), the alternate screen (1049), mouse tracking
175
+ * (1000/1002/1003/1006), and re-shows the cursor (25) — every one of which a TUI
176
+ * leaves armed the same way.
177
+ */
178
+ export declare const TERMINAL_MODE_RESET: string;
179
+ /**
180
+ * Put the local terminal back the way ssh found it.
181
+ *
182
+ * Two independent kinds of state survive a hard ssh death, and BOTH have to be
183
+ * undone or the next thing the user sees is corrupted:
184
+ *
185
+ * - **termios** (kernel side): `-tt` puts the tty in raw mode, so a killed ssh
186
+ * leaves it with echo and line-editing off. `saved` is the `stty -g` snapshot
187
+ * taken before the spawn; restoring it is exact, unlike guessing at `sane`.
188
+ * - **DEC private modes** (terminal side): see {@link TERMINAL_MODE_RESET}.
189
+ *
190
+ * Then drain whatever the terminal already answered back into the input buffer.
191
+ * Without the drain those bytes are not merely cosmetic — they are still queued
192
+ * on the tty, so the NEXT attach hands them straight to the agent as if the user
193
+ * had typed them.
194
+ *
195
+ * Best-effort by construction: this runs while recovering from a failure, so a
196
+ * missing `stty`, a non-TTY stdin, or a closed stream must never turn a
197
+ * recoverable drop into a thrown error.
198
+ */
199
+ export declare function restoreLocalTerminal(saved: string | undefined, opts: {
200
+ drainStdin: boolean;
201
+ }): void;
202
+ /** `stty -g` snapshot of the local tty, or undefined when there is nothing to snapshot. */
203
+ export declare function saveLocalTerminal(): string | undefined;
159
204
  /**
160
205
  * Foreground counterpart to `sshExec`: run `remoteCmd` on `target` with the
161
206
  * local stdio wired straight through (`stdio: 'inherit'`), so output streams as
162
207
  * it is produced and — with `tty` — keystrokes reach a remote picker. Blocks
163
208
  * until the remote command exits and returns its exit code (255 is ssh's own
164
209
  * connection-layer failure; any other non-zero is the remote command's code).
210
+ *
211
+ * On a `tty` stream the local terminal state is snapshotted before the spawn and
212
+ * restored after it. ssh restores termios itself when it exits cleanly, so this
213
+ * is for the case that matters here: the link dying under a full-screen agent
214
+ * TUI, which leaves the tty raw and the TUI's DEC modes armed. Doing it HERE
215
+ * rather than in the reconnect loop means every caller that opens an interactive
216
+ * remote stream is covered, not just the one that noticed (RUSH-3125).
217
+ *
218
+ * **The stdin drain is gated on an ABNORMAL exit, and the distinction is not
219
+ * cosmetic.** Resetting termios and the DEC modes is idempotent — on a clean
220
+ * exit ssh and the remote TUI have already done it, so re-doing it changes
221
+ * nothing. Draining stdin is destructive: it discards whatever the user has
222
+ * typed ahead. On a clean exit those bytes are legitimate type-ahead the
223
+ * resuming shell should receive, and eating them would be a new bug in every
224
+ * caller of this function (`runInteractiveOnHost`, the remote-tmux attach in
225
+ * commands/go.ts, the remote secrets browse) rather than a fix. Only an
226
+ * abnormal exit — ssh killed by a signal (`status === null`) or its own
227
+ * connection-layer failure (255) — produces the answerback storm the drain
228
+ * exists to clear, because only then did the TUI die without sending its own
229
+ * mode resets.
165
230
  */
166
231
  export declare function sshStream(target: string, remoteCmd: string, opts?: SshStreamOptions): number;