botmux 2.94.0 → 2.96.0

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 (231) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +3 -3
  3. package/dist/adapters/backend/tmux-backend.d.ts +18 -2
  4. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/tmux-backend.js +44 -11
  6. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  7. package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
  8. package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
  9. package/dist/adapters/backend/types.d.ts +8 -0
  10. package/dist/adapters/backend/types.d.ts.map +1 -1
  11. package/dist/adapters/backend/types.js.map +1 -1
  12. package/dist/adapters/backend/zellij-backend.js +1 -1
  13. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  14. package/dist/adapters/cli/kimi.d.ts +4 -0
  15. package/dist/adapters/cli/kimi.d.ts.map +1 -0
  16. package/dist/adapters/cli/kimi.js +54 -0
  17. package/dist/adapters/cli/kimi.js.map +1 -0
  18. package/dist/adapters/cli/registry.d.ts +2 -1
  19. package/dist/adapters/cli/registry.d.ts.map +1 -1
  20. package/dist/adapters/cli/registry.js +6 -20
  21. package/dist/adapters/cli/registry.js.map +1 -1
  22. package/dist/adapters/cli/types.d.ts +1 -1
  23. package/dist/adapters/cli/types.d.ts.map +1 -1
  24. package/dist/adapters/hook-installer.d.ts.map +1 -1
  25. package/dist/adapters/hook-installer.js +138 -28
  26. package/dist/adapters/hook-installer.js.map +1 -1
  27. package/dist/bot-registry.d.ts +101 -4
  28. package/dist/bot-registry.d.ts.map +1 -1
  29. package/dist/bot-registry.js +192 -3
  30. package/dist/bot-registry.js.map +1 -1
  31. package/dist/cli/stdin-encoding.d.ts +21 -0
  32. package/dist/cli/stdin-encoding.d.ts.map +1 -1
  33. package/dist/cli/stdin-encoding.js +37 -0
  34. package/dist/cli/stdin-encoding.js.map +1 -1
  35. package/dist/cli.d.ts.map +1 -1
  36. package/dist/cli.js +27 -4
  37. package/dist/cli.js.map +1 -1
  38. package/dist/core/ask-hook/opencode.d.ts +15 -17
  39. package/dist/core/ask-hook/opencode.d.ts.map +1 -1
  40. package/dist/core/ask-hook/opencode.js +17 -19
  41. package/dist/core/ask-hook/opencode.js.map +1 -1
  42. package/dist/core/command-handler.d.ts.map +1 -1
  43. package/dist/core/command-handler.js +29 -3
  44. package/dist/core/command-handler.js.map +1 -1
  45. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  46. package/dist/core/dashboard-ipc-server.js +282 -5
  47. package/dist/core/dashboard-ipc-server.js.map +1 -1
  48. package/dist/core/dashboard-rows.d.ts +2 -0
  49. package/dist/core/dashboard-rows.d.ts.map +1 -1
  50. package/dist/core/dashboard-rows.js +2 -0
  51. package/dist/core/dashboard-rows.js.map +1 -1
  52. package/dist/core/session-discovery.d.ts +9 -0
  53. package/dist/core/session-discovery.d.ts.map +1 -1
  54. package/dist/core/session-discovery.js +24 -0
  55. package/dist/core/session-discovery.js.map +1 -1
  56. package/dist/core/session-manager.d.ts.map +1 -1
  57. package/dist/core/session-manager.js +7 -6
  58. package/dist/core/session-manager.js.map +1 -1
  59. package/dist/core/terminal-url.d.ts.map +1 -1
  60. package/dist/core/terminal-url.js +15 -0
  61. package/dist/core/terminal-url.js.map +1 -1
  62. package/dist/core/trigger-session.d.ts.map +1 -1
  63. package/dist/core/trigger-session.js +101 -9
  64. package/dist/core/trigger-session.js.map +1 -1
  65. package/dist/core/types.d.ts +16 -0
  66. package/dist/core/types.d.ts.map +1 -1
  67. package/dist/core/types.js.map +1 -1
  68. package/dist/core/worker-pool.d.ts +1 -1
  69. package/dist/core/worker-pool.d.ts.map +1 -1
  70. package/dist/core/worker-pool.js +69 -11
  71. package/dist/core/worker-pool.js.map +1 -1
  72. package/dist/daemon.d.ts.map +1 -1
  73. package/dist/daemon.js +14 -11
  74. package/dist/daemon.js.map +1 -1
  75. package/dist/dashboard/aggregator.d.ts +5 -0
  76. package/dist/dashboard/aggregator.d.ts.map +1 -1
  77. package/dist/dashboard/aggregator.js +7 -0
  78. package/dist/dashboard/aggregator.js.map +1 -1
  79. package/dist/dashboard/bot-payload.d.ts +8 -0
  80. package/dist/dashboard/bot-payload.d.ts.map +1 -1
  81. package/dist/dashboard/bot-payload.js +8 -1
  82. package/dist/dashboard/bot-payload.js.map +1 -1
  83. package/dist/dashboard/connector-api.d.ts.map +1 -1
  84. package/dist/dashboard/connector-api.js +14 -3
  85. package/dist/dashboard/connector-api.js.map +1 -1
  86. package/dist/dashboard/session-cleanup.d.ts +1 -0
  87. package/dist/dashboard/session-cleanup.d.ts.map +1 -1
  88. package/dist/dashboard/session-cleanup.js +2 -0
  89. package/dist/dashboard/session-cleanup.js.map +1 -1
  90. package/dist/dashboard/settings-write-applier.d.ts +2 -1
  91. package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
  92. package/dist/dashboard/settings-write-applier.js +7 -0
  93. package/dist/dashboard/settings-write-applier.js.map +1 -1
  94. package/dist/dashboard/trigger-api.d.ts +4 -0
  95. package/dist/dashboard/trigger-api.d.ts.map +1 -1
  96. package/dist/dashboard/trigger-api.js +20 -0
  97. package/dist/dashboard/trigger-api.js.map +1 -1
  98. package/dist/dashboard/web/bot-defaults.d.ts +1 -0
  99. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  100. package/dist/dashboard/web/bot-defaults.js +352 -37
  101. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  102. package/dist/dashboard/web/connectors.d.ts +11 -0
  103. package/dist/dashboard/web/connectors.d.ts.map +1 -1
  104. package/dist/dashboard/web/connectors.js +45 -1
  105. package/dist/dashboard/web/connectors.js.map +1 -1
  106. package/dist/dashboard/web/groups.d.ts +7 -0
  107. package/dist/dashboard/web/groups.d.ts.map +1 -1
  108. package/dist/dashboard/web/groups.js +62 -15
  109. package/dist/dashboard/web/groups.js.map +1 -1
  110. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  111. package/dist/dashboard/web/i18n.js +82 -4
  112. package/dist/dashboard/web/i18n.js.map +1 -1
  113. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  114. package/dist/dashboard/web/sessions.js +149 -5
  115. package/dist/dashboard/web/sessions.js.map +1 -1
  116. package/dist/dashboard/web/settings.d.ts.map +1 -1
  117. package/dist/dashboard/web/settings.js +11 -0
  118. package/dist/dashboard/web/settings.js.map +1 -1
  119. package/dist/dashboard/web/whiteboards.d.ts.map +1 -1
  120. package/dist/dashboard/web/whiteboards.js +14 -5
  121. package/dist/dashboard/web/whiteboards.js.map +1 -1
  122. package/dist/dashboard/webhook-routes.d.ts.map +1 -1
  123. package/dist/dashboard/webhook-routes.js +126 -6
  124. package/dist/dashboard/webhook-routes.js.map +1 -1
  125. package/dist/dashboard-web/app.js +814 -725
  126. package/dist/dashboard-web/apple-touch-icon.png +0 -0
  127. package/dist/dashboard-web/favicon.png +0 -0
  128. package/dist/dashboard-web/index.html +2 -0
  129. package/dist/dashboard-web/style.css +47 -0
  130. package/dist/dashboard.js +247 -16
  131. package/dist/dashboard.js.map +1 -1
  132. package/dist/global-config.d.ts +10 -0
  133. package/dist/global-config.d.ts.map +1 -1
  134. package/dist/global-config.js +8 -0
  135. package/dist/global-config.js.map +1 -1
  136. package/dist/i18n/en.d.ts.map +1 -1
  137. package/dist/i18n/en.js +21 -0
  138. package/dist/i18n/en.js.map +1 -1
  139. package/dist/i18n/zh.d.ts.map +1 -1
  140. package/dist/i18n/zh.js +21 -0
  141. package/dist/i18n/zh.js.map +1 -1
  142. package/dist/im/lark/card-builder.d.ts.map +1 -1
  143. package/dist/im/lark/card-builder.js +1 -0
  144. package/dist/im/lark/card-builder.js.map +1 -1
  145. package/dist/im/lark/client.d.ts +12 -0
  146. package/dist/im/lark/client.d.ts.map +1 -1
  147. package/dist/im/lark/client.js +48 -21
  148. package/dist/im/lark/client.js.map +1 -1
  149. package/dist/im/lark/event-dispatcher.d.ts +15 -1
  150. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  151. package/dist/im/lark/event-dispatcher.js +199 -16
  152. package/dist/im/lark/event-dispatcher.js.map +1 -1
  153. package/dist/im/lark/grant-command.js +4 -4
  154. package/dist/im/lark/grant-command.js.map +1 -1
  155. package/dist/im/lark/message-parser.d.ts +40 -1
  156. package/dist/im/lark/message-parser.d.ts.map +1 -1
  157. package/dist/im/lark/message-parser.js +37 -2
  158. package/dist/im/lark/message-parser.js.map +1 -1
  159. package/dist/im/lark/summary-command.d.ts +19 -0
  160. package/dist/im/lark/summary-command.d.ts.map +1 -0
  161. package/dist/im/lark/summary-command.js +276 -0
  162. package/dist/im/lark/summary-command.js.map +1 -0
  163. package/dist/mir-local-runtime.d.ts +40 -0
  164. package/dist/mir-local-runtime.d.ts.map +1 -1
  165. package/dist/mir-local-runtime.js +246 -52
  166. package/dist/mir-local-runtime.js.map +1 -1
  167. package/dist/mir-runner.js +26 -1
  168. package/dist/mir-runner.js.map +1 -1
  169. package/dist/platform/binding.d.ts +6 -0
  170. package/dist/platform/binding.d.ts.map +1 -1
  171. package/dist/platform/binding.js +17 -0
  172. package/dist/platform/binding.js.map +1 -1
  173. package/dist/platform/tunnel-client.d.ts +2 -0
  174. package/dist/platform/tunnel-client.d.ts.map +1 -1
  175. package/dist/platform/tunnel-client.js.map +1 -1
  176. package/dist/services/bot-config-store.d.ts.map +1 -1
  177. package/dist/services/bot-config-store.js +1 -0
  178. package/dist/services/bot-config-store.js.map +1 -1
  179. package/dist/services/card-prefs-store.d.ts +2 -2
  180. package/dist/services/card-prefs-store.d.ts.map +1 -1
  181. package/dist/services/card-prefs-store.js +5 -5
  182. package/dist/services/card-prefs-store.js.map +1 -1
  183. package/dist/services/chat-reply-mode-store.d.ts +6 -4
  184. package/dist/services/chat-reply-mode-store.d.ts.map +1 -1
  185. package/dist/services/chat-reply-mode-store.js +6 -4
  186. package/dist/services/chat-reply-mode-store.js.map +1 -1
  187. package/dist/services/oncall-store.d.ts +25 -0
  188. package/dist/services/oncall-store.d.ts.map +1 -1
  189. package/dist/services/oncall-store.js +56 -0
  190. package/dist/services/oncall-store.js.map +1 -1
  191. package/dist/services/pi-transcript.d.ts +19 -0
  192. package/dist/services/pi-transcript.d.ts.map +1 -0
  193. package/dist/services/pi-transcript.js +222 -0
  194. package/dist/services/pi-transcript.js.map +1 -0
  195. package/dist/services/summary-range-store.d.ts +21 -0
  196. package/dist/services/summary-range-store.d.ts.map +1 -0
  197. package/dist/services/summary-range-store.js +89 -0
  198. package/dist/services/summary-range-store.js.map +1 -0
  199. package/dist/services/transcript-resolver.d.ts.map +1 -1
  200. package/dist/services/transcript-resolver.js +21 -4
  201. package/dist/services/transcript-resolver.js.map +1 -1
  202. package/dist/services/trigger-types.d.ts +14 -2
  203. package/dist/services/trigger-types.d.ts.map +1 -1
  204. package/dist/services/trigger-types.js +15 -1
  205. package/dist/services/trigger-types.js.map +1 -1
  206. package/dist/setup/bot-config-editor.d.ts +7 -0
  207. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  208. package/dist/setup/bot-config-editor.js +35 -0
  209. package/dist/setup/bot-config-editor.js.map +1 -1
  210. package/dist/types.d.ts +21 -1
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/utils/executable.d.ts +3 -0
  213. package/dist/utils/executable.d.ts.map +1 -0
  214. package/dist/utils/executable.js +24 -0
  215. package/dist/utils/executable.js.map +1 -0
  216. package/dist/utils/terminal-favicon.d.ts +2 -0
  217. package/dist/utils/terminal-favicon.d.ts.map +1 -0
  218. package/dist/utils/terminal-favicon.js +5 -0
  219. package/dist/utils/terminal-favicon.js.map +1 -0
  220. package/dist/worker.js +211 -12
  221. package/dist/worker.js.map +1 -1
  222. package/dist/workflows/definition.d.ts +8 -8
  223. package/dist/workflows/events/payloads.d.ts +10 -10
  224. package/dist/workflows/events/schema.d.ts +28 -28
  225. package/dist/workflows/v3/bot-resolve.d.ts +1 -1
  226. package/dist/workflows/v3/bot-resolve.d.ts.map +1 -1
  227. package/dist/workflows/v3/bot-resolve.js +2 -1
  228. package/dist/workflows/v3/bot-resolve.js.map +1 -1
  229. package/dist/workflows/v3/cli-run.js +2 -2
  230. package/dist/workflows/v3/cli-run.js.map +1 -1
  231. package/package.json +2 -2
package/dist/worker.js CHANGED
@@ -15,7 +15,7 @@
15
15
  import { randomBytes } from 'node:crypto';
16
16
  import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, readdirSync, readlinkSync, readFileSync, watch as fsWatch, createWriteStream } from 'node:fs';
17
17
  import { atomicWriteFileSync } from './utils/atomic-write.js';
18
- import { isAbsolute, join } from 'node:path';
18
+ import { isAbsolute, join, basename } from 'node:path';
19
19
  import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
20
20
  import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
21
21
  import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
@@ -34,6 +34,7 @@ import { findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/t
34
34
  import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
35
35
  import { currentHermesStateOffset, drainHermesStateDb } from './services/hermes-transcript.js';
36
36
  import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
37
+ import { drainPiTranscript, findPiTranscriptByPid, findPiTranscriptBySessionId } from './services/pi-transcript.js';
37
38
  import { drainCursorTranscript, findCursorChatIdByPid, findCursorTranscriptByChatId, findCursorTranscriptByPid } from './services/cursor-transcript.js';
38
39
  import { shouldObserveCursorChatId, shouldPersistObservedCursorChatId } from './services/cursor-resume-policy.js';
39
40
  import { baselineJsonlCursor } from './services/jsonl-cursor.js';
@@ -41,12 +42,13 @@ import { dirname } from 'node:path';
41
42
  import { createServer as createHttpServer } from 'node:http';
42
43
  import { WebSocketServer, WebSocket } from 'ws';
43
44
  import { listenWebTerminalWithFallback } from './utils/web-terminal-listen.js';
45
+ import { TERMINAL_FAVICON_DATA_URI } from './utils/terminal-favicon.js';
44
46
  import { t, setDefaultLocale } from './i18n/index.js';
45
47
  import { TerminalRenderer } from './utils/terminal-renderer.js';
46
48
  import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_ROWS, MIN_RENDER_COLS, MIN_RENDER_ROWS, clamp, resolveRenderDimensions, } from './utils/render-dimensions.js';
47
49
  import { createCliAdapterSync, locateOnPath } from './adapters/cli/registry.js';
48
50
  import { buildWrappedLaunch, parseWrapperCli, isTtadkWrapper } from './setup/cli-selection.js';
49
- import { findLaunchedCliPid, scheduleWrapperRealCliPid } from './core/session-discovery.js';
51
+ import { findLaunchedCliPid, scheduleWrapperRealCliPid, readComm, isBareShellComm, bareShellLaunchKind } from './core/session-discovery.js';
50
52
  import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
51
53
  import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
52
54
  import { HerdrBackend } from './adapters/backend/herdr-backend.js';
@@ -134,6 +136,24 @@ const authedClients = new WeakSet();
134
136
  /** Per-WS-client tmux/zellij attach PTYs. */
135
137
  const clientPtys = new Map();
136
138
  const writeToken = randomBytes(16).toString('hex');
139
+ /**
140
+ * Resolve terminal write permission for one request, honoring a platform-injected
141
+ * `X-Botmux-Role` header. The central platform fronts `/s/*` and sets the role
142
+ * (owner | teammate | guest) after authenticating the viewer, stripping any
143
+ * client-supplied header; it reaches the worker via dashboard /s bridge →
144
+ * terminal-proxy → here. When the header is present we trust it (platform-fronted
145
+ * access): only `owner` may drive the terminal; everything else (teammate / guest
146
+ * / anything else) is read-only. When the header is absent (local direct access,
147
+ * no platform in front), fall back to the legacy write-token query param.
148
+ */
149
+ function resolveTerminalWrite(req, tokenMatches) {
150
+ const role = req.headers['x-botmux-role'];
151
+ if (typeof role === 'string' && role) {
152
+ const hasWrite = role === 'owner';
153
+ return { hasWrite, platformReadonly: !hasWrite };
154
+ }
155
+ return { hasWrite: tokenMatches, platformReadonly: false };
156
+ }
137
157
  /** Lazily-written locked-mode zellij config for per-WS web-terminal attach
138
158
  * clients: cleared keybinds + locked mode so every keystroke passes straight
139
159
  * to the focused (codex) pane, never intercepted as a zellij shortcut. */
@@ -153,7 +173,7 @@ function ensureZellijAttachConfig() {
153
173
  }
154
174
  let sessionId = '';
155
175
  let lastInitConfig = null;
156
- const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi' };
176
+ const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', kimi: 'Kimi' };
157
177
  function cliName() { return CLI_DISPLAY_NAMES[lastInitConfig?.cliId ?? ''] ?? 'CLI'; }
158
178
  let isPromptReady = false;
159
179
  /** Mutex for async flushPending — prevents concurrent flush loops. */
@@ -164,6 +184,19 @@ let isFlushing = false;
164
184
  * when the CLI restarts. Consumed inside flushPending right before the first
165
185
  * user prompt is drained, so the commands always precede it (see runStartupCommands). */
166
186
  let hasRunStartupCommands = false;
187
+ /** Per-spawn latch: set once the launch-failure detector has decided the pane
188
+ * leaf is a bare shell (the CLI never launched — e.g. a user rcfile that
189
+ * `exec`-trampolines into another shell pre-empted the wrapper's `exec <cli>`).
190
+ * Once set, flushPending refuses to type prompts into the bare shell (which
191
+ * would just produce `zsh: parse error`) and the user gets one diagnostic
192
+ * instead. Reset per spawn in spawnCli. */
193
+ let bareShellLaunchBlocked = false;
194
+ /** Per-spawn one-shot: has the bare-shell launch check already run for this
195
+ * spawn? Gates detectBareShellLaunch() to the FIRST flush only (the
196
+ * "about to type the first prompt" moment), independent of the startup-commands
197
+ * one-shot so it also covers a reattach onto a pane that degraded to a bare
198
+ * shell. Reset per spawn in spawnCli. */
199
+ let bareShellChecked = false;
167
200
  /** Ready-gate (Claude-family): holds the first prompt until the SessionStart
168
201
  * hook fires a true-ready signal, so a cjadk-style startup selector's ❯ (which
169
202
  * falsely matches readyPattern) can't eat the first message. Recreated + armed
@@ -1693,7 +1726,7 @@ function codexBridgeFallbackActive() {
1693
1726
  // True for transcript-backed CLIs whose final output can be harvested
1694
1727
  // when the model forgets to call `botmux send`.
1695
1728
  const id = lastInitConfig?.cliId;
1696
- if (id === 'codex' || id === 'traex' || id === 'coco' || id === 'hermes' || id === 'mtr')
1729
+ if (id === 'codex' || id === 'traex' || id === 'coco' || id === 'hermes' || id === 'mtr' || id === 'pi')
1697
1730
  return true;
1698
1731
  // Cursor only harvests its transcript in adopt mode: a botmux-spawned
1699
1732
  // cursor session carries the botmux skill and replies via `botmux send`,
@@ -1714,6 +1747,9 @@ function structuredBridgeIsHermes() {
1714
1747
  function structuredBridgeIsMtr() {
1715
1748
  return lastInitConfig?.cliId === 'mtr';
1716
1749
  }
1750
+ function structuredBridgeIsPi() {
1751
+ return lastInitConfig?.cliId === 'pi';
1752
+ }
1717
1753
  function codexBridgeIsCursor() {
1718
1754
  return lastInitConfig?.cliId === 'cursor';
1719
1755
  }
@@ -1722,6 +1758,8 @@ function structuredBridgeIngestPath(path, offset) {
1722
1758
  return drainCodexRollout(path, offset);
1723
1759
  if (codexBridgeIsCursor())
1724
1760
  return drainCursorTranscript(path, offset);
1761
+ if (structuredBridgeIsPi())
1762
+ return drainPiTranscript(path, offset);
1725
1763
  if (structuredBridgeIsHermes()) {
1726
1764
  const result = drainHermesStateDb(offset);
1727
1765
  return { events: result.events, newOffset: result.newOffset, pendingTail: '' };
@@ -1809,6 +1847,7 @@ function codexBridgeStartTimer() {
1809
1847
  // sid, so the lookup is just a path computation + existence check.
1810
1848
  const isCoco = lastInitConfig?.cliId === 'coco';
1811
1849
  const isTraex = lastInitConfig?.cliId === 'traex';
1850
+ const isPi = lastInitConfig?.cliId === 'pi';
1812
1851
  let path;
1813
1852
  if (codexBridgePendingSessionId) {
1814
1853
  if (isCoco) {
@@ -1816,6 +1855,9 @@ function codexBridgeStartTimer() {
1816
1855
  if (path && !existsSync(path))
1817
1856
  path = undefined;
1818
1857
  }
1858
+ else if (isPi) {
1859
+ path = findPiTranscriptBySessionId(codexBridgePendingSessionId, lastInitConfig?.workingDir);
1860
+ }
1819
1861
  else if (isTraex) {
1820
1862
  path = findTraexRolloutBySessionId(codexBridgePendingSessionId);
1821
1863
  }
@@ -1829,6 +1871,11 @@ function codexBridgeStartTimer() {
1829
1871
  if (probed && existsSync(probed.eventsPath))
1830
1872
  path = probed.eventsPath;
1831
1873
  }
1874
+ else if (isPi) {
1875
+ const probed = findPiTranscriptByPid(codexAdoptPendingPid);
1876
+ if (probed)
1877
+ path = probed.path;
1878
+ }
1832
1879
  else if (isTraex) {
1833
1880
  const probed = findTraexRolloutByPid(codexAdoptPendingPid);
1834
1881
  if (probed)
@@ -2075,7 +2122,9 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
2075
2122
  }
2076
2123
  const path = lastInitConfig?.cliId === 'traex'
2077
2124
  ? findTraexRolloutBySessionId(cliSessionId)
2078
- : findCodexRolloutBySessionId(cliSessionId);
2125
+ : lastInitConfig?.cliId === 'pi'
2126
+ ? findPiTranscriptBySessionId(cliSessionId, lastInitConfig?.workingDir)
2127
+ : findCodexRolloutBySessionId(cliSessionId);
2079
2128
  if (path) {
2080
2129
  codexBridgePendingSessionId = undefined;
2081
2130
  codexBridgeAttach(path, 'fresh-empty');
@@ -3264,6 +3313,69 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
3264
3313
  });
3265
3314
  }, SUBMIT_DEFERRED_RECHECK_MS);
3266
3315
  }
3316
+ /**
3317
+ * Launch-failure guard. Right before the FIRST prompt is typed, confirm the
3318
+ * pane's leaf process is the agent CLI — not a bare interactive shell. The
3319
+ * failure this catches: a user's login `$SHELL` (e.g. bash) whose rcfile
3320
+ * `exec`-trampolines into another shell (`[ -t 1 ] && exec zsh`). botmux's
3321
+ * tmux wrapper launches `<shell> -i -c '… exec /usr/bin/env <cli>'`; the `-i`
3322
+ * sources the rcfile, the `exec zsh` replaces the shell BEFORE the `-c` body
3323
+ * runs, and the pane is left at a bare shell. Typing the multi-line prompt into
3324
+ * it just yields `zsh: parse error near '\n'` and the user is stuck (the exact
3325
+ * bug this guards). Instead of typing into the shell we surface ONE actionable
3326
+ * diagnostic and latch the session so no further prompt is mis-typed.
3327
+ *
3328
+ * Why this is the right moment / low false-positive: the first prompt is held
3329
+ * until the CLI signals ready OR the 15s/45s first-prompt timeout fires, so by
3330
+ * the time we get here a healthy CLI has long since `exec`'d (leaf comm =
3331
+ * codex/node/…) — only a trampolined/failed launch is still a bare shell. We
3332
+ * skip wrapperCli/adopt (their leaf is legitimately a launcher/observed pane)
3333
+ * and the pty/herdr backends (which `exec` the CLI directly — getChildPid is the
3334
+ * CLI itself, never a shell).
3335
+ *
3336
+ * Returns true when a bare-shell launch was detected (caller must NOT flush).
3337
+ */
3338
+ function detectBareShellLaunch() {
3339
+ if (bareShellLaunchBlocked)
3340
+ return true;
3341
+ if (lastInitConfig?.adoptMode)
3342
+ return false; // observing an existing pane, not launching
3343
+ if (lastInitConfig?.wrapperCli)
3344
+ return false; // launcher legitimately wraps the CLI (transient shell shim)
3345
+ const pid = backend?.getChildPid?.();
3346
+ if (!pid)
3347
+ return false;
3348
+ const comm = readComm(pid);
3349
+ if (!isBareShellComm(comm))
3350
+ return false; // CLI (rust/go/node) is running — healthy launch
3351
+ // Bare shell is the pane leaf → the CLI never launched. Tier the message on
3352
+ // whether the leaf shell differs from the one botmux launched with: a
3353
+ // mismatch is the unmistakable signature of an rcfile `exec`-trampoline.
3354
+ const launchShell = (lastInitConfig?.launchShell || process.env.SHELL || '').trim();
3355
+ const expectedShell = launchShell ? basename(launchShell) : '';
3356
+ const trampolined = bareShellLaunchKind(comm, expectedShell) === 'trampoline';
3357
+ bareShellLaunchBlocked = true;
3358
+ log(`Bare-shell launch detected: pane leaf comm=${comm}, expected launch shell=${expectedShell || '?'}, ` +
3359
+ `cli=${lastInitConfig?.cliId}; suppressing first-prompt write (${trampolined ? 'rc trampoline' : 'CLI did not start'})`);
3360
+ const cli = cliName();
3361
+ let message;
3362
+ if (trampolined) {
3363
+ message =
3364
+ `⚠️ 会话没能启动:pane 里现在是裸 \`${comm}\`,${cli} 没真正跑起来——所以我没把你的消息打进去(否则会被当 shell 命令执行,报 \`parse error\`)。\n\n` +
3365
+ `最可能原因:botmux 用 \`${expectedShell}\` 启动 CLI,但 pane 落到了 \`${comm}\`。通常是 rc 文件(如 \`~/.${expectedShell}rc\`)里有 \`exec ${comm}\` 这类跳转——\`${expectedShell} -i\` 会 source rc,于是 shell 被顶替,CLI 的启动命令没机会跑。\n\n` +
3366
+ `两种修法(任选其一,改完重启 daemon 再发一条消息):\n` +
3367
+ `① 给那行加守卫,只在手动开终端时切:\`[ -z "$BASH_EXECUTION_STRING" ] && [ -t 1 ] && exec ${comm}\`(注意 PATH/nvm 等导出放在它之前)\n` +
3368
+ `② 给这个 bot 配 \`launchShell: ${comm}\`(dashboard 机器人配置,或 \`/config launchShell ${comm}\`),直接用 \`${comm}\` 启动绕开 \`${expectedShell}\` 的 rc——但要确保 PATH/nvm 在 \`${comm}\` 的 rc 里。`;
3369
+ }
3370
+ else {
3371
+ message =
3372
+ `⚠️ 会话没能启动:pane 里还停在 \`${comm}\`,${cli} 没真正跑起来——我没把消息打进去(否则会被当 shell 命令执行)。\n\n` +
3373
+ `可能原因:rc 文件启动过慢/报错,或 \`${cli}\` 的可执行文件不在 PATH 上(CLI 没找到)。\n` +
3374
+ `建议:在 web 终端里手动敲一下启动命令看报什么错;确认 CLI 二进制能在 PATH 上找到;或精简 rc 启动逻辑后重启 daemon 再试。`;
3375
+ }
3376
+ send({ type: 'user_notify', turnId: currentBotmuxTurnId, message });
3377
+ return true;
3378
+ }
3267
3379
  /**
3268
3380
  * Drain the pending message queue sequentially.
3269
3381
  * Async with isFlushing mutex: awaits each writeInput, then immediately
@@ -3277,6 +3389,8 @@ async function flushPending() {
3277
3389
  return;
3278
3390
  if (pendingMessages.length === 0)
3279
3391
  return; // nothing to flush — keep isPromptReady
3392
+ if (bareShellLaunchBlocked)
3393
+ return; // launch failed into a bare shell — don't type prompts into it
3280
3394
  // Ready-gate: hold the FIRST prompt until the SessionStart hook fires a true-
3281
3395
  // ready signal. A cjadk-style startup selector's ❯ falsely matches readyPattern
3282
3396
  // and would otherwise eat this message. releaseReadyGate() re-invokes us once
@@ -3326,6 +3440,21 @@ async function flushPending() {
3326
3440
  idleDetector?.reset();
3327
3441
  }
3328
3442
  try {
3443
+ // Launch-failure guard, run ONCE per spawn on the first flush, BEFORE startup
3444
+ // commands or any user prompt: if the pane leaf is a bare shell (the CLI never
3445
+ // launched — e.g. a user rcfile that `exec`-trampolines into another shell, or
3446
+ // a reattached persistent pane that has dropped back to a shell), don't type
3447
+ // anything into it (it would just be `zsh: parse error`); surface one
3448
+ // diagnostic and bail. Gated by its own one-shot (NOT hasRunStartupCommands)
3449
+ // so it also covers reattach, where startup commands are intentionally
3450
+ // skipped. Must precede runStartupCommands so a bot with startupCommands
3451
+ // doesn't get them typed into the bare shell first.
3452
+ if (!bareShellChecked) {
3453
+ bareShellChecked = true;
3454
+ if (detectBareShellLaunch()) {
3455
+ return; // finally{} releases the mutex; pendingMessages stay queued, untouched
3456
+ }
3457
+ }
3329
3458
  // One-shot per spawn: type the bot's startup commands (e.g. `/effort
3330
3459
  // ultracode`) into the CLI before the first user prompt drains. Both ready
3331
3460
  // paths funnel through flushPending — the ready-gate settle for Claude-family
@@ -3693,16 +3822,38 @@ function setupAdoptTranscriptBridges(cfg) {
3693
3822
  codexBridgeStartTimer();
3694
3823
  }
3695
3824
  }
3825
+ else if (cfg.cliId === 'pi') {
3826
+ const adoptStartMs = Date.now();
3827
+ codexAdoptStartMs = adoptStartMs;
3828
+ codexBridgeQueue.setLocalTurns(true, adoptStartMs);
3829
+ let path;
3830
+ if (cfg.cliSessionId)
3831
+ path = findPiTranscriptBySessionId(cfg.cliSessionId, cfg.adoptCwd ?? cfg.workingDir);
3832
+ if (!path && cfg.adoptCliPid) {
3833
+ const probed = findPiTranscriptByPid(cfg.adoptCliPid);
3834
+ if (probed)
3835
+ path = probed.path;
3836
+ }
3837
+ if (path) {
3838
+ codexBridgeAttach(path, 'split-live');
3839
+ }
3840
+ else {
3841
+ if (cfg.cliSessionId)
3842
+ codexBridgePendingSessionId = cfg.cliSessionId;
3843
+ codexAdoptPendingPid = cfg.adoptCliPid;
3844
+ codexBridgeStartTimer();
3845
+ }
3846
+ }
3696
3847
  }
3697
3848
  function adoptIdleAdapter(cfg) {
3698
3849
  return cfg.bridgeJsonlPath
3699
3850
  ? createCliAdapterSync('claude-code', undefined)
3700
- : cfg.cliId === 'codex' || cfg.cliId === 'traex' || cfg.cliId === 'coco' || cfg.cliId === 'mtr'
3851
+ : cfg.cliId === 'codex' || cfg.cliId === 'traex' || cfg.cliId === 'coco' || cfg.cliId === 'mtr' || cfg.cliId === 'pi'
3701
3852
  ? createCliAdapterSync(cfg.cliId, undefined)
3702
3853
  : { completionPattern: undefined, readyPattern: undefined };
3703
3854
  }
3704
3855
  function setupAdoptInputAdapter(cfg) {
3705
- if (cfg.cliId === 'codex' || cfg.cliId === 'traex') {
3856
+ if (cfg.cliId === 'codex' || cfg.cliId === 'traex' || cfg.cliId === 'pi') {
3706
3857
  cliAdapter = createCliAdapterSync(cfg.cliId, cfg.cliPathOverride);
3707
3858
  }
3708
3859
  else if (cfg.cliId === 'mtr') {
@@ -3998,6 +4149,13 @@ function spawnCli(cfg) {
3998
4149
  // arm it. spawnCli is synchronous up to backend spawn, so this lands before
3999
4150
  // any flushPending consumes the flag.
4000
4151
  hasRunStartupCommands = !shouldRunStartupCommandsOnSpawn({ willReattachPersistent });
4152
+ // Re-arm the bare-shell launch detector for this spawn (fresh OR reattach). It
4153
+ // runs once on the first flush and only fires when the pane leaf is actually a
4154
+ // bare shell, so a healthy reattach (leaf = the live CLI) self-excludes while a
4155
+ // reattach onto a pane that has degraded to a bare shell still gets the
4156
+ // diagnostic instead of having the prompt typed into it.
4157
+ bareShellLaunchBlocked = false;
4158
+ bareShellChecked = false;
4001
4159
  // ── Resume pre-flight check + two-tier fallback ──────────────────────────
4002
4160
  // Tier 1 (adapter probe): adapter.checkResumeTargetExists returns false
4003
4161
  // → skip --resume, spawn FRESH.
@@ -4365,6 +4523,7 @@ function spawnCli(cfg) {
4365
4523
  rows: PTY_ROWS,
4366
4524
  env: childEnv,
4367
4525
  injectEnv: perBotInjectKeys.length ? perBotInjectEnv : undefined,
4526
+ launchShell: lastInitConfig?.launchShell,
4368
4527
  });
4369
4528
  // Write CLI PID marker so agent-facing subcommands (`botmux send`, etc.)
4370
4529
  // can verify they were spawned inside a botmux session by walking the
@@ -4565,6 +4724,22 @@ function spawnCli(cfg) {
4565
4724
  codexBridgeStartTimer();
4566
4725
  }
4567
4726
  }
4727
+ else if (cfg.cliId === 'pi') {
4728
+ const piSessionId = effectiveCliSessionId ?? effectiveAdapterSessionId;
4729
+ if (piSessionId) {
4730
+ const transcriptPath = findPiTranscriptBySessionId(piSessionId, cfg.workingDir);
4731
+ if (transcriptPath) {
4732
+ codexBridgeAttach(transcriptPath, effectiveResume ? 'baseline-existing' : 'fresh-empty');
4733
+ }
4734
+ else {
4735
+ codexBridgePendingSessionId = piSessionId;
4736
+ codexBridgeStartTimer();
4737
+ }
4738
+ }
4739
+ else {
4740
+ codexBridgeStartTimer();
4741
+ }
4742
+ }
4568
4743
  // Arm the ready-gate for FRESH Claude-family spawns (which inject the
4569
4744
  // SessionStart hook via --settings; see claude-code.ts buildArgs). Until
4570
4745
  // `botmux session-ready` fires (daemon → 'session_ready' IPC → releaseReadyGate)
@@ -4765,9 +4940,12 @@ function startWebServer(host, preferredPort) {
4765
4940
  res.end('Bad Request');
4766
4941
  return;
4767
4942
  }
4768
- const hasWrite = url.searchParams.get('token') === writeToken;
4943
+ const tokenMatches = url.searchParams.get('token') === writeToken;
4944
+ const { hasWrite, platformReadonly } = resolveTerminalWrite(req, tokenMatches);
4945
+ const loginHdr = req.headers['x-botmux-login-url'];
4946
+ const loginUrl = typeof loginHdr === 'string' && /^https?:\/\/[^"'<>\s]+$/.test(loginHdr) ? loginHdr : '';
4769
4947
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
4770
- res.end(getTerminalHtml(hasWrite));
4948
+ res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl));
4771
4949
  });
4772
4950
  wss = new WebSocketServer({ server: httpServer });
4773
4951
  wss.on('connection', (ws, req) => {
@@ -4780,7 +4958,8 @@ function startWebServer(host, preferredPort) {
4780
4958
  ws.close(1008, 'Bad Request');
4781
4959
  return;
4782
4960
  }
4783
- const hasWrite = url.searchParams.get('token') === writeToken;
4961
+ const tokenMatches = url.searchParams.get('token') === writeToken;
4962
+ const { hasWrite } = resolveTerminalWrite(req, tokenMatches);
4784
4963
  if (hasWrite)
4785
4964
  authedClients.add(ws);
4786
4965
  log(`WS client connected (total: ${wsClients.size}, write: ${hasWrite})`);
@@ -5026,7 +5205,7 @@ function startWebServer(host, preferredPort) {
5026
5205
  .then(resolve, reject);
5027
5206
  });
5028
5207
  }
5029
- function getTerminalHtml(hasWrite) {
5208
+ function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '') {
5030
5209
  const label = sessionId.substring(0, 8);
5031
5210
  return `<!DOCTYPE html>
5032
5211
  <html>
@@ -5034,6 +5213,7 @@ function getTerminalHtml(hasWrite) {
5034
5213
  <meta charset="utf-8">
5035
5214
  <meta id="vp" name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
5036
5215
  <title>${cliName()} - ${label}</title>
5216
+ <link rel="icon" type="image/png" href="${TERMINAL_FAVICON_DATA_URI}">
5037
5217
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
5038
5218
  <style>
5039
5219
  *{margin:0;padding:0;box-sizing:border-box}
@@ -5073,11 +5253,17 @@ body.touch #terminal .xterm-screen *{
5073
5253
  background:rgba(247,118,142,0.12);border:1px solid rgba(247,118,142,0.35);border-radius:4px;
5074
5254
  backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
5075
5255
  #readonly-banner.show{display:inline-block}
5256
+ #login-banner{display:none;position:fixed;top:8px;left:50%;transform:translateX(-50%);z-index:50;
5257
+ padding:4px 10px;font:12px monospace;color:#e0af68;white-space:nowrap;text-decoration:none;cursor:pointer;
5258
+ background:rgba(224,175,104,0.12);border:1px solid rgba(224,175,104,0.35);border-radius:4px;
5259
+ backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}
5260
+ #login-banner.show{display:inline-block}
5076
5261
  </style>
5077
5262
  </head>
5078
5263
  <body>
5079
5264
  <div id="terminal"></div>
5080
5265
  <div id="readonly-banner">只读模式 · 无写入权限</div>
5266
+ ${loginUrl ? `<a id="login-banner" href="${loginUrl}" target="_top" rel="noopener">owner 登录后可操作 →</a>` : '<div id="login-banner">owner 登录后可操作</div>'}
5081
5267
  <div id="toolbar">
5082
5268
  <button data-k="esc">Esc</button>
5083
5269
  <button data-k="ctrlc">^C</button>
@@ -5099,7 +5285,11 @@ body.touch #terminal .xterm-screen *{
5099
5285
  var isTouch='ontouchstart'in window||navigator.maxTouchPoints>0;
5100
5286
  if(isTouch){document.getElementById('vp').content='width=1100,viewport-fit=cover';document.body.classList.add('touch');}
5101
5287
  var hasToken=${hasWrite};
5102
- if(!hasToken){var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
5288
+ var platformReadonly=${platformReadonly};
5289
+ if(!hasToken){
5290
+ if(platformReadonly){var _lb=document.getElementById('login-banner');_lb.classList.add('show');}
5291
+ else{var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
5292
+ }
5103
5293
 
5104
5294
  var term=new Terminal({
5105
5295
  theme:{background:'#1a1b26',foreground:'#a9b1d6',cursor:'#c0caf5',
@@ -5395,6 +5585,15 @@ process.on('message', async (raw) => {
5395
5585
  adoptMode: msg.adoptMode === true,
5396
5586
  passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
5397
5587
  });
5588
+ if (msg.prompt && cliAdapter?.passesInitialPromptViaArgs && !deferInitialPrompt && codexBridgeFallbackActive()) {
5589
+ // Args-baked first prompts (notably Pi) never pass through the normal
5590
+ // 'message' IPC path, so the structured bridge would otherwise see the
5591
+ // transcript final answer with no pending turn to attribute it to.
5592
+ // Mark it here before the CLI starts processing; late-attach is fine
5593
+ // because CodexBridgeQueue is path-agnostic until ingest discovers the
5594
+ // transcript file.
5595
+ codexBridgeMarkPendingTurn(msg.prompt, msg.turnId);
5596
+ }
5398
5597
  if (msg.prompt && (!cliAdapter?.passesInitialPromptViaArgs || deferInitialPrompt)) {
5399
5598
  pendingMessages.push({ content: msg.prompt, turnId: msg.turnId });
5400
5599
  }