tailii-host 0.1.0 → 0.3.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 (348) hide show
  1. package/README.md +35 -14
  2. package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
  3. package/dist/{broker.js → approval/broker.js} +42 -6
  4. package/dist/approval/broker.js.map +1 -0
  5. package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
  6. package/dist/{hook.js → approval/hook.js} +359 -50
  7. package/dist/approval/hook.js.map +1 -0
  8. package/dist/backend/herdr.d.ts +226 -0
  9. package/dist/backend/herdr.js +749 -0
  10. package/dist/backend/herdr.js.map +1 -0
  11. package/dist/backend/sessionBackend.d.ts +81 -0
  12. package/dist/backend/sessionBackend.js +114 -0
  13. package/dist/backend/sessionBackend.js.map +1 -0
  14. package/dist/backend/tmux.d.ts +125 -0
  15. package/dist/backend/tmux.js +392 -0
  16. package/dist/backend/tmux.js.map +1 -0
  17. package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
  18. package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
  19. package/dist/chat/chatTailController.js.map +1 -0
  20. package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
  21. package/dist/{imageService.js → chat/imageService.js} +39 -11
  22. package/dist/chat/imageService.js.map +1 -0
  23. package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
  24. package/dist/chat/remoteQuestionMonitor.js +128 -0
  25. package/dist/chat/remoteQuestionMonitor.js.map +1 -0
  26. package/dist/chat/subagentTailer.d.ts +30 -0
  27. package/dist/chat/subagentTailer.js +704 -0
  28. package/dist/chat/subagentTailer.js.map +1 -0
  29. package/dist/chat/subagentTranscript.d.ts +20 -0
  30. package/dist/chat/subagentTranscript.js +273 -0
  31. package/dist/chat/subagentTranscript.js.map +1 -0
  32. package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
  33. package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
  34. package/dist/chat/transcriptTailer.js.map +1 -0
  35. package/dist/cli.js +15 -10
  36. package/dist/cli.js.map +1 -1
  37. package/dist/codex/codexAccountUsage.d.ts +22 -0
  38. package/dist/codex/codexAccountUsage.js +128 -0
  39. package/dist/codex/codexAccountUsage.js.map +1 -0
  40. package/dist/codex/codexAppServer.d.ts +166 -0
  41. package/dist/codex/codexAppServer.js +1297 -0
  42. package/dist/codex/codexAppServer.js.map +1 -0
  43. package/dist/codex/codexNativeTurnController.d.ts +153 -0
  44. package/dist/codex/codexNativeTurnController.js +673 -0
  45. package/dist/codex/codexNativeTurnController.js.map +1 -0
  46. package/dist/codex/codexRolloutTailer.d.ts +100 -0
  47. package/dist/codex/codexRolloutTailer.js +568 -0
  48. package/dist/codex/codexRolloutTailer.js.map +1 -0
  49. package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
  50. package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
  51. package/dist/codex/codexSessionStore.js.map +1 -0
  52. package/dist/codex/codexSubagentTracker.d.ts +21 -0
  53. package/dist/codex/codexSubagentTracker.js +281 -0
  54. package/dist/codex/codexSubagentTracker.js.map +1 -0
  55. package/dist/codex/codexToolActivity.d.ts +69 -0
  56. package/dist/codex/codexToolActivity.js +425 -0
  57. package/dist/codex/codexToolActivity.js.map +1 -0
  58. package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
  59. package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
  60. package/dist/codex/codexUsage.js.map +1 -0
  61. package/dist/commands/doctor.d.ts +65 -0
  62. package/dist/commands/doctor.js +425 -0
  63. package/dist/commands/doctor.js.map +1 -0
  64. package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
  65. package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
  66. package/dist/commands/hookSettings.js.map +1 -0
  67. package/dist/{kick.js → commands/kick.js} +16 -1
  68. package/dist/commands/kick.js.map +1 -0
  69. package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
  70. package/dist/commands/launch.js +716 -0
  71. package/dist/commands/launch.js.map +1 -0
  72. package/dist/commands/legacyHomeMigration.js.map +1 -0
  73. package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
  74. package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
  75. package/dist/commands/pairingCode.js.map +1 -0
  76. package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
  77. package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
  78. package/dist/commands/pushTokenCommand.js.map +1 -0
  79. package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
  80. package/dist/{setup.js → commands/setup.js} +133 -75
  81. package/dist/commands/setup.js.map +1 -0
  82. package/dist/engine/context.d.ts +145 -0
  83. package/dist/engine/context.js +184 -0
  84. package/dist/engine/context.js.map +1 -0
  85. package/dist/engine/engine.d.ts +121 -0
  86. package/dist/engine/engine.js +799 -0
  87. package/dist/engine/engine.js.map +1 -0
  88. package/dist/engine/handlers/accountUsage.d.ts +53 -0
  89. package/dist/engine/handlers/accountUsage.js +203 -0
  90. package/dist/engine/handlers/accountUsage.js.map +1 -0
  91. package/dist/engine/handlers/claudeModels.d.ts +2 -0
  92. package/dist/engine/handlers/claudeModels.js +23 -0
  93. package/dist/engine/handlers/claudeModels.js.map +1 -0
  94. package/dist/engine/handlers/codex.d.ts +2 -0
  95. package/dist/engine/handlers/codex.js +97 -0
  96. package/dist/engine/handlers/codex.js.map +1 -0
  97. package/dist/engine/handlers/conversation.d.ts +2 -0
  98. package/dist/engine/handlers/conversation.js +259 -0
  99. package/dist/engine/handlers/conversation.js.map +1 -0
  100. package/dist/engine/handlers/core.d.ts +2 -0
  101. package/dist/engine/handlers/core.js +66 -0
  102. package/dist/engine/handlers/core.js.map +1 -0
  103. package/dist/engine/handlers/git.d.ts +2 -0
  104. package/dist/engine/handlers/git.js +127 -0
  105. package/dist/engine/handlers/git.js.map +1 -0
  106. package/dist/engine/handlers/index.d.ts +2 -0
  107. package/dist/engine/handlers/index.js +32 -0
  108. package/dist/engine/handlers/index.js.map +1 -0
  109. package/dist/engine/handlers/mode.d.ts +2 -0
  110. package/dist/engine/handlers/mode.js +194 -0
  111. package/dist/engine/handlers/mode.js.map +1 -0
  112. package/dist/engine/handlers/officialApps.d.ts +2 -0
  113. package/dist/engine/handlers/officialApps.js +79 -0
  114. package/dist/engine/handlers/officialApps.js.map +1 -0
  115. package/dist/engine/handlers/preview.d.ts +2 -0
  116. package/dist/engine/handlers/preview.js +59 -0
  117. package/dist/engine/handlers/preview.js.map +1 -0
  118. package/dist/engine/handlers/session.d.ts +2 -0
  119. package/dist/engine/handlers/session.js +385 -0
  120. package/dist/engine/handlers/session.js.map +1 -0
  121. package/dist/engine/handlers/usage.d.ts +2 -0
  122. package/dist/engine/handlers/usage.js +96 -0
  123. package/dist/engine/handlers/usage.js.map +1 -0
  124. package/dist/engine/handlers/workspace.d.ts +2 -0
  125. package/dist/engine/handlers/workspace.js +110 -0
  126. package/dist/engine/handlers/workspace.js.map +1 -0
  127. package/dist/engine/slashCommands.d.ts +3 -0
  128. package/dist/engine/slashCommands.js +211 -0
  129. package/dist/engine/slashCommands.js.map +1 -0
  130. package/dist/hub/engineRelaySocket.d.ts +58 -0
  131. package/dist/hub/engineRelaySocket.js +161 -0
  132. package/dist/hub/engineRelaySocket.js.map +1 -0
  133. package/dist/hub/hubClient.d.ts +28 -0
  134. package/dist/hub/hubClient.js +187 -0
  135. package/dist/hub/hubClient.js.map +1 -0
  136. package/dist/hub/hubDaemon.d.ts +69 -0
  137. package/dist/hub/hubDaemon.js +466 -0
  138. package/dist/hub/hubDaemon.js.map +1 -0
  139. package/dist/hub/hubProtocol.d.ts +161 -0
  140. package/dist/hub/hubProtocol.js +379 -0
  141. package/dist/hub/hubProtocol.js.map +1 -0
  142. package/dist/hub/panePreviewPump.d.ts +65 -0
  143. package/dist/hub/panePreviewPump.js +187 -0
  144. package/dist/hub/panePreviewPump.js.map +1 -0
  145. package/dist/hub/questionInjection.d.ts +19 -0
  146. package/dist/hub/questionInjection.js +126 -0
  147. package/dist/hub/questionInjection.js.map +1 -0
  148. package/dist/hub/reaper.d.ts +74 -0
  149. package/dist/hub/reaper.js +333 -0
  150. package/dist/hub/reaper.js.map +1 -0
  151. package/dist/hub/sessionHub.d.ts +203 -0
  152. package/dist/hub/sessionHub.js +1862 -0
  153. package/dist/hub/sessionHub.js.map +1 -0
  154. package/dist/protocol/common.d.ts +18 -0
  155. package/dist/protocol/common.js +78 -0
  156. package/dist/protocol/common.js.map +1 -0
  157. package/dist/protocol/decode.d.ts +3 -0
  158. package/dist/protocol/decode.js +1036 -0
  159. package/dist/protocol/decode.js.map +1 -0
  160. package/dist/protocol/encode.d.ts +3 -0
  161. package/dist/protocol/encode.js +84 -0
  162. package/dist/protocol/encode.js.map +1 -0
  163. package/dist/protocol/messages.d.ts +1002 -0
  164. package/dist/protocol/messages.js +8 -0
  165. package/dist/protocol/messages.js.map +1 -0
  166. package/dist/protocol.d.ts +4 -329
  167. package/dist/protocol.js +13 -531
  168. package/dist/protocol.js.map +1 -1
  169. package/dist/push/apnsConfigStore.js.map +1 -0
  170. package/dist/push/apnsJwtProvider.js.map +1 -0
  171. package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
  172. package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
  173. package/dist/push/approvalPushNotifier.js.map +1 -0
  174. package/dist/push/deviceTokenStore.js.map +1 -0
  175. package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
  176. package/dist/push/pushTypes.js.map +1 -0
  177. package/dist/push/sendLog.js.map +1 -0
  178. package/dist/services/accountIdentity.d.ts +44 -0
  179. package/dist/services/accountIdentity.js +184 -0
  180. package/dist/services/accountIdentity.js.map +1 -0
  181. package/dist/services/claudeCredentialRefresh.d.ts +30 -0
  182. package/dist/services/claudeCredentialRefresh.js +345 -0
  183. package/dist/services/claudeCredentialRefresh.js.map +1 -0
  184. package/dist/services/claudeModelCatalog.d.ts +21 -0
  185. package/dist/services/claudeModelCatalog.js +181 -0
  186. package/dist/services/claudeModelCatalog.js.map +1 -0
  187. package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
  188. package/dist/services/dirLister.js +169 -0
  189. package/dist/services/dirLister.js.map +1 -0
  190. package/dist/services/fileService.d.ts +10 -0
  191. package/dist/services/fileService.js +123 -0
  192. package/dist/services/fileService.js.map +1 -0
  193. package/dist/services/gitService.d.ts +79 -0
  194. package/dist/services/gitService.js +648 -0
  195. package/dist/services/gitService.js.map +1 -0
  196. package/dist/services/hostVersions.d.ts +38 -0
  197. package/dist/services/hostVersions.js +120 -0
  198. package/dist/services/hostVersions.js.map +1 -0
  199. package/dist/services/officialApps.d.ts +148 -0
  200. package/dist/services/officialApps.js +1079 -0
  201. package/dist/services/officialApps.js.map +1 -0
  202. package/dist/services/planUsageFetcher.d.ts +119 -0
  203. package/dist/services/planUsageFetcher.js +357 -0
  204. package/dist/services/planUsageFetcher.js.map +1 -0
  205. package/dist/services/previewServer.d.ts +31 -0
  206. package/dist/services/previewServer.js +233 -0
  207. package/dist/services/previewServer.js.map +1 -0
  208. package/dist/services/quicGateway.d.ts +143 -0
  209. package/dist/services/quicGateway.js +292 -0
  210. package/dist/services/quicGateway.js.map +1 -0
  211. package/dist/services/serveService.d.ts +50 -0
  212. package/dist/services/serveService.js +302 -0
  213. package/dist/services/serveService.js.map +1 -0
  214. package/dist/services/usageAggregator.js.map +1 -0
  215. package/dist/sessions/claudeSessionStore.d.ts +80 -0
  216. package/dist/sessions/claudeSessionStore.js +477 -0
  217. package/dist/sessions/claudeSessionStore.js.map +1 -0
  218. package/dist/sessions/heartbeat.d.ts +24 -0
  219. package/dist/sessions/heartbeat.js +86 -0
  220. package/dist/sessions/heartbeat.js.map +1 -0
  221. package/dist/sessions/liveSessionJoin.d.ts +20 -0
  222. package/dist/sessions/liveSessionJoin.js +80 -0
  223. package/dist/sessions/liveSessionJoin.js.map +1 -0
  224. package/dist/sessions/sessionActivityProvider.d.ts +14 -0
  225. package/dist/sessions/sessionActivityProvider.js +57 -0
  226. package/dist/sessions/sessionActivityProvider.js.map +1 -0
  227. package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
  228. package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
  229. package/dist/sessions/sessionListService.js.map +1 -0
  230. package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
  231. package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
  232. package/dist/sessions/sessionMetadataStore.js.map +1 -0
  233. package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
  234. package/dist/sessions/sessionSearch.js.map +1 -0
  235. package/dist/shared/hostDisplayName.d.ts +11 -0
  236. package/dist/shared/hostDisplayName.js +50 -0
  237. package/dist/shared/hostDisplayName.js.map +1 -0
  238. package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
  239. package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
  240. package/dist/shared/lineWriter.js.map +1 -0
  241. package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
  242. package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
  243. package/dist/{paths.js → shared/paths.js} +9 -0
  244. package/dist/shared/paths.js.map +1 -0
  245. package/dist/shared/permissionMode.d.ts +10 -0
  246. package/dist/shared/permissionMode.js +51 -0
  247. package/dist/shared/permissionMode.js.map +1 -0
  248. package/dist/shared/skillInjection.d.ts +9 -0
  249. package/dist/shared/skillInjection.js +16 -0
  250. package/dist/shared/skillInjection.js.map +1 -0
  251. package/dist/shared/sleep.js.map +1 -0
  252. package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
  253. package/dist/{socketPath.js → shared/socketPath.js} +22 -0
  254. package/dist/shared/socketPath.js.map +1 -0
  255. package/dist/shared/version.js.map +1 -0
  256. package/package.json +13 -7
  257. package/dist/apnsConfigStore.js.map +0 -1
  258. package/dist/apnsJwtProvider.js.map +0 -1
  259. package/dist/approvalPushNotifier.js.map +0 -1
  260. package/dist/broker.js.map +0 -1
  261. package/dist/chatTailController.js.map +0 -1
  262. package/dist/claudeSessionStore.d.ts +0 -15
  263. package/dist/claudeSessionStore.js +0 -201
  264. package/dist/claudeSessionStore.js.map +0 -1
  265. package/dist/codexRolloutTailer.d.ts +0 -56
  266. package/dist/codexRolloutTailer.js +0 -303
  267. package/dist/codexRolloutTailer.js.map +0 -1
  268. package/dist/codexSessionStore.js.map +0 -1
  269. package/dist/codexUsage.js.map +0 -1
  270. package/dist/deviceTokenStore.js.map +0 -1
  271. package/dist/dirLister.js +0 -98
  272. package/dist/dirLister.js.map +0 -1
  273. package/dist/doctor.d.ts +0 -36
  274. package/dist/doctor.js +0 -162
  275. package/dist/doctor.js.map +0 -1
  276. package/dist/engine.d.ts +0 -85
  277. package/dist/engine.js +0 -962
  278. package/dist/engine.js.map +0 -1
  279. package/dist/hook.js.map +0 -1
  280. package/dist/hookSettings.js.map +0 -1
  281. package/dist/idleReaper.d.ts +0 -13
  282. package/dist/idleReaper.js +0 -30
  283. package/dist/idleReaper.js.map +0 -1
  284. package/dist/imageService.js.map +0 -1
  285. package/dist/kick.js.map +0 -1
  286. package/dist/launch.js +0 -396
  287. package/dist/launch.js.map +0 -1
  288. package/dist/legacyHomeMigration.js.map +0 -1
  289. package/dist/lineWriter.js.map +0 -1
  290. package/dist/mdnsAdvertiser.d.ts +0 -46
  291. package/dist/mdnsAdvertiser.js +0 -268
  292. package/dist/mdnsAdvertiser.js.map +0 -1
  293. package/dist/pairingCode.js.map +0 -1
  294. package/dist/paths.js.map +0 -1
  295. package/dist/permissionMode.d.ts +0 -8
  296. package/dist/permissionMode.js +0 -32
  297. package/dist/permissionMode.js.map +0 -1
  298. package/dist/planUsageFetcher.d.ts +0 -35
  299. package/dist/planUsageFetcher.js +0 -172
  300. package/dist/planUsageFetcher.js.map +0 -1
  301. package/dist/pushTokenCommand.js.map +0 -1
  302. package/dist/pushTypes.js.map +0 -1
  303. package/dist/sendLog.js.map +0 -1
  304. package/dist/sessionActivityProvider.d.ts +0 -7
  305. package/dist/sessionActivityProvider.js +0 -44
  306. package/dist/sessionActivityProvider.js.map +0 -1
  307. package/dist/sessionIdleTracker.d.ts +0 -19
  308. package/dist/sessionIdleTracker.js +0 -42
  309. package/dist/sessionIdleTracker.js.map +0 -1
  310. package/dist/sessionListService.js.map +0 -1
  311. package/dist/sessionMetadataStore.js.map +0 -1
  312. package/dist/sessionSearch.js.map +0 -1
  313. package/dist/setup.js.map +0 -1
  314. package/dist/sleep.js.map +0 -1
  315. package/dist/socketPath.js.map +0 -1
  316. package/dist/subagentTailer.d.ts +0 -15
  317. package/dist/subagentTailer.js +0 -399
  318. package/dist/subagentTailer.js.map +0 -1
  319. package/dist/tmux.d.ts +0 -62
  320. package/dist/tmux.js +0 -163
  321. package/dist/tmux.js.map +0 -1
  322. package/dist/transcriptTailer.js.map +0 -1
  323. package/dist/usageAggregator.js.map +0 -1
  324. package/dist/version.js.map +0 -1
  325. /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
  326. /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
  327. /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
  328. /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
  329. /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
  330. /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
  331. /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
  332. /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
  333. /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
  334. /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
  335. /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
  336. /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
  337. /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
  338. /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
  339. /package/dist/{sendLog.js → push/sendLog.js} +0 -0
  340. /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
  341. /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
  342. /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
  343. /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
  344. /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
  345. /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
  346. /package/dist/{sleep.js → shared/sleep.js} +0 -0
  347. /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
  348. /package/dist/{version.js → shared/version.js} +0 -0
@@ -0,0 +1,716 @@
1
+ // launch.ts
2
+ // tailii (TS host) — tmux 上での claude 起動(launchCore)と engine 用 launcher の組み立て
3
+ // Swift 版 Launch.swift の移植。
4
+ // 指定 dir で tmux 上に claude をデタッチ起動し、承認フックは起動時 `--settings '<json>'` で
5
+ // この起動プロセス限定に渡し(settings.json は書かない)、SessionMetadataStore に cwd を権威記録
6
+ // する。SSH exec は非ログインシェルのため PATH を明示注入する。
7
+ import { execFile, spawn } from "node:child_process";
8
+ import * as fs from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+ import { CodexAppServerManager } from "../codex/codexAppServer.js";
12
+ import { HERDR_TAILII_SESSION, defaultHerdrPath, parseHerdrForegroundCommand, parseHerdrPaneList, parseHerdrCreatedTabPaneId, } from "../backend/herdr.js";
13
+ import { claudeHookLaunchSettings, removeCodexHookSettings } from "./hookSettings.js";
14
+ import { tailiiWorktreeRepoRoot } from "../services/gitService.js";
15
+ import { ClaudeSessionStore } from "../sessions/claudeSessionStore.js";
16
+ import { claudeProjectSlug, isInsideBase, standardize, expandTilde } from "../shared/paths.js";
17
+ import { resolveSessionBackendKind } from "../backend/sessionBackend.js";
18
+ import { SessionMetadataStore } from "../sessions/sessionMetadataStore.js";
19
+ import { sleep } from "../shared/sleep.js";
20
+ import { DEFAULT_TMUX_PATH, paneCommandLooksLikeAgent } from "../backend/tmux.js";
21
+ /** tmux 内で起動する既定コマンド(claude TUI)。テストでは無害な値に差し替える。 */
22
+ export const DEFAULT_INNER_COMMAND = "claude";
23
+ /**
24
+ * codex(OpenAI Codex CLI)モードの既定コマンド。
25
+ * 承認統合は後続 Milestone のため、本スライスでは端末で承認応答できない前提で
26
+ * `-a never -s workspace-write`(workspace 内サンドボックスで承認プロンプトを出さず自律実行)にする。
27
+ */
28
+ export const DEFAULT_CODEX_COMMAND = "codex -a never -s workspace-write";
29
+ /**
30
+ * codex の既存会話 resume 用フラグ(サブコマンド `resume <SESSION_ID>` に前置)。
31
+ * `-a`/`-s` は resume サブコマンドでも有効(`codex resume --help` で確認済み, agent-tag)。
32
+ * resume は cwd+mtime で rollout を解決する tail の性質上、厳密な per-session 追尾は保証しない
33
+ * (同一 cwd の新しい rollout があるとそちらを追い得る。既知の制約)。
34
+ */
35
+ export const CODEX_RESUME_FLAGS = "-a never -s workspace-write";
36
+ /** モデル slug の安全文字(コマンド注入防止。カタログ由来だが二重に検証する)。 */
37
+ const CODEX_MODEL_SAFE = /^[A-Za-z0-9._-]+$/;
38
+ /** claude の permission mode 既知値(--permission-mode に安全に渡せる値のみ)。 */
39
+ const CLAUDE_PERMISSION_MODES = new Set(["default", "acceptEdits", "plan", "auto"]);
40
+ /**
41
+ * claude の resume/continue は古く大きいセッションで TUI 専用設問
42
+ * `Resume from summary?` を出してブロックする。この設問は transcript にもフックにも乗らず
43
+ * リモートから見えないため、閾値を実質無効化して常に従来どおりフル resume にする。
44
+ * claude 2.1.207 で確認した環境変数。
45
+ */
46
+ const CLAUDE_RESUME_THRESHOLD_ENV = "CLAUDE_CODE_RESUME_THRESHOLD_MINUTES=525600 CLAUDE_CODE_RESUME_TOKEN_THRESHOLD=1000000000";
47
+ /**
48
+ * claude 新規起動の inner コマンドを組み立てる(起動前モデル/モード選択の反映)。
49
+ * @param model 省略/不正文字は無視(アカウント既定モデル)。alias('opus' 等)と完全 id の両方可。
50
+ * @param permissionMode 既知 4 値のみ採用。"default" はフラグ無しと等価なので付けない。
51
+ */
52
+ export function claudeInnerCommand(opts) {
53
+ let cmd = DEFAULT_INNER_COMMAND;
54
+ if (opts.model && CODEX_MODEL_SAFE.test(opts.model))
55
+ cmd += ` --model ${opts.model}`;
56
+ if (opts.permissionMode &&
57
+ opts.permissionMode !== "default" &&
58
+ CLAUDE_PERMISSION_MODES.has(opts.permissionMode)) {
59
+ cmd += ` --permission-mode ${opts.permissionMode}`;
60
+ }
61
+ return cmd;
62
+ }
63
+ /**
64
+ * codex 新規起動の inner コマンドを組み立てる(codex-input)。
65
+ * 承認は App Server の JSON-RPC で処理する。ここでは TUI 単体起動用のsandboxとモデルを組み立てる。
66
+ * @param model 省略/不正文字は無視(既定モデル)。
67
+ * @param sandbox TUI 単体起動では省略時 workspace-write。remote resume ではこの文字列を使わない。
68
+ */
69
+ export function codexInnerCommand(opts) {
70
+ const sandbox = opts.sandbox ?? "workspace-write";
71
+ let cmd = `codex -a never -s ${sandbox}`;
72
+ if (opts.model && CODEX_MODEL_SAFE.test(opts.model))
73
+ cmd += ` -m ${opts.model}`;
74
+ return cmd;
75
+ }
76
+ /** SSH exec(非ログインシェル)向けに注入する PATH。 */
77
+ export function defaultInjectedPath() {
78
+ return [
79
+ "/opt/homebrew/bin",
80
+ path.join(os.homedir(), ".local/bin"),
81
+ // mise 管理ツール(例: codex)の解決用シムディレクトリ。非ログインシェルの SSH exec でも届くよう明示。
82
+ path.join(os.homedir(), ".local/share/mise/shims"),
83
+ "/usr/local/bin",
84
+ "/usr/bin",
85
+ "/bin",
86
+ "/usr/sbin",
87
+ "/sbin",
88
+ ].join(":");
89
+ }
90
+ /** シェル single-quote で安全に包む(内部の `'` は `'\''` へ)。tmux new に渡す inner コマンド用。 */
91
+ export function shellSingleQuote(value) {
92
+ return `'${value.replaceAll("'", "'\\''")}'`;
93
+ }
94
+ function defaultProcessRunner() {
95
+ return (executable, args, options) => new Promise((resolve, reject) => {
96
+ execFile(executable, args, { cwd: options.cwd, env: options.env, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
97
+ if (error && typeof error.code === "string") {
98
+ reject(error);
99
+ return;
100
+ }
101
+ const exitCode = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
102
+ resolve({ exitCode, stdout: String(stdout) });
103
+ });
104
+ });
105
+ }
106
+ /** launch サブコマンドのエントリポイント(`--dir <path> --session <name>`)。 */
107
+ export async function runLaunchCommand(args) {
108
+ let dirArg = null;
109
+ let sessionArg = null;
110
+ for (let i = 0; i < args.length; i += 1) {
111
+ if (args[i] === "--dir" && i + 1 < args.length) {
112
+ dirArg = args[i + 1];
113
+ i += 1;
114
+ }
115
+ else if (args[i] === "--session" && i + 1 < args.length) {
116
+ sessionArg = args[i + 1];
117
+ i += 1;
118
+ }
119
+ }
120
+ if (!dirArg) {
121
+ process.stderr.write("tailii launch: --dir <path> が必要です\n");
122
+ return 2;
123
+ }
124
+ if (!sessionArg) {
125
+ process.stderr.write("tailii launch: --session <name> が必要です\n");
126
+ return 2;
127
+ }
128
+ return launchCore({
129
+ dir: dirArg,
130
+ session: sessionArg,
131
+ baseDir: null,
132
+ binaryPath: currentBinaryPath(),
133
+ tmuxPath: DEFAULT_TMUX_PATH,
134
+ innerCommand: DEFAULT_INNER_COMMAND,
135
+ path: defaultInjectedPath(),
136
+ store: new SessionMetadataStore(),
137
+ now: () => Math.floor(Date.now() / 1000),
138
+ errorSink: (message) => process.stderr.write(message),
139
+ backend: resolveSessionBackendKind(),
140
+ });
141
+ }
142
+ /**
143
+ * `session_start` を `launchCore` へ橋渡しする EngineLauncher を組み立てる。
144
+ * `store` は TmuxSessionManager と同一インスタンスを渡すこと(起動直後の一覧に cwd が反映されるように)。
145
+ */
146
+ export function makeSessionLauncher(options = {}) {
147
+ const store = options.store ?? new SessionMetadataStore();
148
+ const agent = options.agent ?? "claude";
149
+ const inner = options.innerCommand ?? (agent === "codex" ? DEFAULT_CODEX_COMMAND : DEFAULT_INNER_COMMAND);
150
+ const tmux = options.tmuxPath ?? DEFAULT_TMUX_PATH;
151
+ const backendOption = options.backend ?? "tmux";
152
+ const herdr = options.herdrPath ?? defaultHerdrPath();
153
+ const binary = currentBinaryPath();
154
+ const runner = options.runner ?? defaultProcessRunner();
155
+ const codexAppServer = agent === "codex" ? (options.codexAppServer ?? new CodexAppServerManager()) : null;
156
+ return async (cwd, name, baseDir, resumeSessionId, newSessionId, title, launchOptions) => {
157
+ let errorText = "";
158
+ // resume 先の worktree が削除済みなら repo ルートへフォールバック(deleted-worktree-resume)。
159
+ // session_start(resume) と session_reattach の両経路がこの閉包を通る。
160
+ if (resumeSessionId !== null && resumeSessionId !== undefined) {
161
+ const rescued = rescueMissingWorktreeCwd({
162
+ cwd,
163
+ claudeSessionId: agent === "claude" ? resumeSessionId : null,
164
+ ...(options.claudeProjectsDir !== undefined
165
+ ? { projectsDir: options.claudeProjectsDir }
166
+ : {}),
167
+ });
168
+ if (rescued !== null)
169
+ cwd = rescued;
170
+ }
171
+ // resume: claude は `<inner> --resume <id>`。resume でない新規起動は
172
+ // `<inner> --session-id <uuid>` で会話 id を固定し、host が tail 対象 jsonl を
173
+ // 事前に確定できるようにする(同一 cwd の別会話ログの流入を防ぐ)。会話名を付けた
174
+ // 新規起動は `--name '<title>'`(/resume ピッカー等に出る表示名, lazy-session)を添える。
175
+ // codex はこれらの制御を持たないため従来どおり素の inner(resume 指定も本スライスでは無視)。
176
+ let effectiveInner;
177
+ let effectiveProviderSessionId = agent === "claude" ? (resumeSessionId ?? newSessionId ?? null) : resumeSessionId;
178
+ let effectiveCwd = cwd;
179
+ let effectiveBaseDir = baseDir;
180
+ if (agent === "claude") {
181
+ if (resumeSessionId) {
182
+ effectiveInner = `${inner} --resume ${resumeSessionId}`;
183
+ }
184
+ else if (newSessionId) {
185
+ effectiveInner = `${inner} --session-id ${newSessionId}`;
186
+ if (title)
187
+ effectiveInner += ` --name ${shellSingleQuote(title)}`;
188
+ }
189
+ else {
190
+ effectiveInner = inner;
191
+ }
192
+ effectiveInner = `${CLAUDE_RESUME_THRESHOLD_ENV} ${effectiveInner}`;
193
+ }
194
+ else {
195
+ if (codexAppServer === null) {
196
+ return { exitCode: 1, errorText: "Codex App Server が構成されていません。" };
197
+ }
198
+ // App Serverのthread/startには絶対cwdが必要。launchCoreと同じ規則で先に解決する。
199
+ const resolved = resolveWorkdir(cwd, baseDir, (message) => {
200
+ errorText += message;
201
+ });
202
+ if (resolved === null)
203
+ return { exitCode: 1, errorText };
204
+ effectiveCwd = resolved;
205
+ effectiveBaseDir = null;
206
+ try {
207
+ if (resumeSessionId !== null) {
208
+ await codexAppServer.ensureRunning();
209
+ effectiveProviderSessionId = resumeSessionId;
210
+ }
211
+ else {
212
+ effectiveProviderSessionId = await codexAppServer.startThread({
213
+ cwd: resolved,
214
+ model: launchOptions?.codexModel && CODEX_MODEL_SAFE.test(launchOptions.codexModel)
215
+ ? launchOptions.codexModel
216
+ : null,
217
+ sandbox: launchOptions?.codexSandbox ?? null,
218
+ });
219
+ }
220
+ }
221
+ catch (error) {
222
+ return { exitCode: 1, errorText: `Codex App Server 起動失敗: ${String(error)}` };
223
+ }
224
+ const selectedInner = launchOptions?.codexModel !== undefined || launchOptions?.codexSandbox !== undefined
225
+ ? codexInnerCommand({
226
+ model: launchOptions.codexModel ?? null,
227
+ sandbox: launchOptions.codexSandbox ?? null,
228
+ })
229
+ : inner;
230
+ effectiveInner = codexRemoteResumeCommand(selectedInner, effectiveProviderSessionId, codexAppServer.remoteEndpoint);
231
+ }
232
+ const exitCode = await launchCore({
233
+ dir: effectiveCwd,
234
+ session: name,
235
+ baseDir: effectiveBaseDir,
236
+ binaryPath: binary,
237
+ tmuxPath: tmux,
238
+ backend: typeof backendOption === "function" ? backendOption() : backendOption,
239
+ herdrPath: herdr,
240
+ innerCommand: effectiveInner,
241
+ path: defaultInjectedPath(),
242
+ store,
243
+ now: () => Math.floor(Date.now() / 1000),
244
+ errorSink: (message) => {
245
+ errorText += message;
246
+ },
247
+ runner,
248
+ agent,
249
+ // resume は同一会話 id の jsonl に書き続ける(engine の tail 束縛と同じ前提)ため、
250
+ // resume 起動でも実効会話 id を権威記録し、以後の reattach で厳密束縛できるようにする。
251
+ claudeSessionId: agent === "claude" ? (resumeSessionId ?? newSessionId ?? null) : null,
252
+ providerSessionId: effectiveProviderSessionId,
253
+ displayTitle: title ?? null,
254
+ });
255
+ return {
256
+ exitCode,
257
+ errorText,
258
+ ...(effectiveProviderSessionId !== null
259
+ ? { providerSessionId: effectiveProviderSessionId }
260
+ : {}),
261
+ };
262
+ };
263
+ }
264
+ /** Codex TUIを共有App Serverの同一threadへ接続する安全なresumeコマンド。 */
265
+ export function codexRemoteResumeCommand(_innerCommand, sessionId, remoteEndpoint) {
266
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId))
267
+ throw new Error("invalid Codex session ID");
268
+ if (!/^unix:\/\/[A-Za-z0-9_./-]*$/.test(remoteEndpoint)) {
269
+ throw new Error("invalid Codex App Server endpoint");
270
+ }
271
+ // model/sandbox/approval は thread/start/thread/resume の App Server 側を権威にする。
272
+ // TUI の resume 引数で `-a never` 等を再指定すると native approval 設定を上書きするため、
273
+ // remote TUI は同一 thread の表示クライアントとして必要最小限の引数だけで接続する。
274
+ // thread/start 直後は最初の user turn まで rollout が存在せず、running thread でも TUI の
275
+ // thread/resume は `no rollout found` になる。tmux pane を生かしたまま rollout 生成を待ち、
276
+ // App Server turn/start が materialize した直後に TUI を同じ thread へ接続する。
277
+ const sessionsRoot = '"${CODEX_HOME:-$HOME/.codex}/sessions"';
278
+ const waitForRollout = `while ! find ${sessionsRoot} -type f -name '*${sessionId}*.jsonl' -print -quit 2>/dev/null | grep -q .; do sleep 0.2; done`;
279
+ return `${waitForRollout}; exec codex resume --remote ${remoteEndpoint} --no-alt-screen ${sessionId}`;
280
+ }
281
+ /**
282
+ * launch の純ロジック(プロセス起動とパス類を注入できるためテスト可能)。
283
+ * 1. cwd 解決 + base 配下限定自動作成 → 2. フォルダ事前信頼 → 3. `--settings` へ承認フックを合成 →
284
+ * 4. 死んだ同名セッション掃除 → 5. tmux デタッチ起動 → 6. cwd 権威記録。
285
+ */
286
+ export async function launchCore(options) {
287
+ const { session, binaryPath, tmuxPath, store, now, errorSink } = options;
288
+ const agent = options.agent ?? "claude";
289
+ const runner = options.runner ?? defaultProcessRunner();
290
+ // --- 1. cwd 解決 + base 配下限定自動作成 ---
291
+ const dir = resolveWorkdir(options.dir, options.baseDir, errorSink);
292
+ if (dir === null)
293
+ return 1;
294
+ // エージェント別の起動前準備。
295
+ // claude: 「フォルダを信頼しますか?」の事前回避(~/.claude.json)と settings.json フック注入。
296
+ // codex: Claude 固有処理は行わない。App Server が承認要求を JSON-RPC で配信するため、
297
+ // 旧版 Tailii の Codex hook を除去し、プロジェクト信頼だけを起動引数で付与する。
298
+ let innerCommand = options.innerCommand;
299
+ if (agent === "codex") {
300
+ // App Server の native approval と二重化しないよう、旧版が追加した hook だけを消す。
301
+ // 併せて `-c` で信頼を事前付与しトラストダイアログを回避(config.toml は書き換えない)。
302
+ try {
303
+ removeCodexHookSettings({ dir, binaryPath });
304
+ }
305
+ catch (error) {
306
+ errorSink(`tailii launch: 旧 Codex hook の除去失敗: ${String(error)}\n`);
307
+ return 1;
308
+ }
309
+ // App Server thread/start が cwd/permission を構造化設定する。remote TUI は表示購読者なので、
310
+ // CLI 側の trust/approval override を重ねず thread 設定をそのまま使う。
311
+ }
312
+ else {
313
+ // --- 1.5. claude の初回「フォルダを信頼しますか?」プロンプトを事前回避する ---
314
+ preTrustFolder(dir, options.claudeJsonPath);
315
+ // --- 2. 承認フックを settings.json へは書かず、この起動限定で `--settings` 経由で渡す ---
316
+ // (settings.json に書くと同 dir の「tailii 非経由の通常 claude 起動」もフックを拾い、
317
+ // 承認ブローカー不在で全ツールがハングするため。2026-07-07 ユーザー指摘の根治)
318
+ const hookSettings = claudeHookLaunchSettings({
319
+ dir,
320
+ binaryPath,
321
+ session,
322
+ ...(options.hookGlobalMarkerPath !== undefined && {
323
+ globalMarkerPath: options.hookGlobalMarkerPath,
324
+ }),
325
+ });
326
+ if (hookSettings !== null) {
327
+ innerCommand = `${innerCommand} --settings ${shellSingleQuote(hookSettings)}`;
328
+ }
329
+ }
330
+ const env = { ...process.env, PATH: options.path };
331
+ // --- herdr backend: tmux セッションの代わりに herdr pane としてデタッチ起動する ---
332
+ if ((options.backend ?? "tmux") === "herdr") {
333
+ return launchHerdrPane({
334
+ session,
335
+ dir,
336
+ innerCommand,
337
+ agent,
338
+ herdrPath: options.herdrPath ?? defaultHerdrPath(),
339
+ injectedPath: options.path,
340
+ env,
341
+ runner,
342
+ store,
343
+ now,
344
+ errorSink,
345
+ ...(options.claudeSessionId !== undefined ? { claudeSessionId: options.claudeSessionId } : {}),
346
+ ...(options.providerSessionId !== undefined
347
+ ? { providerSessionId: options.providerSessionId }
348
+ : {}),
349
+ ...(options.displayTitle !== undefined ? { displayTitle: options.displayTitle } : {}),
350
+ ...(options.spawnDetached !== undefined ? { spawnDetached: options.spawnDetached } : {}),
351
+ ...(options.ensurePollMs !== undefined ? { ensurePollMs: options.ensurePollMs } : {}),
352
+ });
353
+ }
354
+ const runTmux = async (args) => {
355
+ try {
356
+ const result = await runner(tmuxPath, args, { env });
357
+ return { code: result.exitCode, out: result.stdout };
358
+ }
359
+ catch {
360
+ return { code: 127, out: "" };
361
+ }
362
+ };
363
+ // --- 2.5. 既存の「死んだ」同名セッションを掃除する(同名再利用を可能にする) ---
364
+ if ((await runTmux(["has-session", "-t", session])).code === 0) {
365
+ const panes = await runTmux(["list-panes", "-t", session, "-F", "#{pane_dead}"]);
366
+ const hasLive = panes.out.split("\n").some((line) => line.trim() === "0");
367
+ // Claude が終了してシェルだけ残った pane も stale。これを生存扱いすると --resume を
368
+ // 起動せず、iOS からの会話入力がシェルへ送られて無反応になる。
369
+ // Codex は App Server がターンを駆動し、rollout 待ち中の TUI が shell に見えるため除外する。
370
+ let hasAgent = true;
371
+ if (hasLive && agent === "claude") {
372
+ const current = await runTmux([
373
+ "display-message", "-p", "-t", session, "#{pane_current_command}",
374
+ ]);
375
+ hasAgent = current.code !== 0 || paneCommandLooksLikeAgent(current.out);
376
+ }
377
+ if (!hasLive || !hasAgent) {
378
+ await runTmux(["kill-session", "-t", session]);
379
+ }
380
+ }
381
+ // --- 3. tmux で claude をデタッチ起動 ---
382
+ // まだ存在するなら生きた pane がある(claude 稼働中)=再作成不要。
383
+ // `-A` は付けない(sshd exec に TTY が無く attach 試行が非0終了するため)。
384
+ const alreadyLive = (await runTmux(["has-session", "-t", session])).code === 0;
385
+ if (!alreadyLive) {
386
+ let status;
387
+ try {
388
+ const result = await runner(tmuxPath, ["new", "-d", "-s", session, innerCommand], {
389
+ cwd: dir,
390
+ env,
391
+ });
392
+ status = result.exitCode;
393
+ }
394
+ catch (error) {
395
+ errorSink(`tailii launch: tmux 起動失敗 (${tmuxPath}): ${String(error)}\n`);
396
+ return 1;
397
+ }
398
+ if (status !== 0) {
399
+ errorSink(`tailii launch: tmux が非0終了 (${status})\n`);
400
+ return status;
401
+ }
402
+ }
403
+ // session名はrenameやmulti-pane化で曖昧になるため、以後の入出力target用に安定pane IDを保存する。
404
+ const paneResult = await runTmux(["display-message", "-p", "-t", session, "#{pane_id}"]);
405
+ const paneIdCandidate = paneResult.code === 0 ? paneResult.out.trim() : "";
406
+ const tmuxPaneId = /^%\d+$/.test(paneIdCandidate) ? paneIdCandidate : null;
407
+ // --- 4. cwd を権威記録(tmux は起動 cwd を安定に返さないため本ストアを権威とする) ---
408
+ try {
409
+ // 任意メタは必要なときだけ記録する(古いメタ形式との後方互換を保つ)。
410
+ store.put({
411
+ name: session,
412
+ cwd: dir,
413
+ createdAt: now(),
414
+ ...(agent === "codex" ? { agent } : {}),
415
+ ...(agent === "claude" && options.claudeSessionId
416
+ ? { claudeSessionId: options.claudeSessionId }
417
+ : {}),
418
+ ...(options.providerSessionId ? { providerSessionId: options.providerSessionId } : {}),
419
+ ...(tmuxPaneId !== null ? { tmuxPaneId } : {}),
420
+ });
421
+ }
422
+ catch (error) {
423
+ errorSink(`tailii launch: メタデータ保存失敗: ${String(error)}\n`);
424
+ return 1;
425
+ }
426
+ return 0;
427
+ }
428
+ /**
429
+ * herdr backend の起動実体。tmux の「死んだ同名セッション掃除 → デタッチ起動 → pane ID 記録」に
430
+ * 対応する herdr 版。ユーザーの default セッションを汚さないよう、専用 named session
431
+ * `tailii`(`--session` を全コマンドに前置)へ収容する:
432
+ * 1. tailii セッションサーバーの ensure(不在ならヘッドレス起動)→
433
+ * 2. stale pane 掃除(claude が終了してシェル化した pane)→
434
+ * 3. `tab create --cwd --env` で新タブ+pane 生成 → 4. `pane rename` で label=session 名 →
435
+ * 5. `pane run "exec /bin/zsh -lc '<inner>'"` で起動 → 6. backend/herdrPaneId 込みでメタ権威記録。
436
+ * (herdr 0.7.5 で `agent start` の pane 生成/cwd/env 指定が廃止されたため tab create 方式)
437
+ */
438
+ async function launchHerdrPane(options) {
439
+ const { session, dir, agent, store, now, errorSink } = options;
440
+ const runHerdr = async (args) => {
441
+ try {
442
+ const result = await options.runner(options.herdrPath, ["--session", HERDR_TAILII_SESSION, ...args], { env: options.env });
443
+ return { code: result.exitCode, out: result.stdout };
444
+ }
445
+ catch (error) {
446
+ errorSink(`tailii launch: herdr 起動失敗 (${options.herdrPath}): ${String(error)}\n`);
447
+ return { code: 127, out: "" };
448
+ }
449
+ };
450
+ const spawnDetached = options.spawnDetached ??
451
+ ((executable, args) => {
452
+ const child = spawn(executable, args, { detached: true, stdio: "ignore" });
453
+ child.unref();
454
+ });
455
+ // --- 1. tailii セッションサーバーの ensure(`pane list` が通れば稼働中) ---
456
+ let paneList = await runHerdr(["pane", "list"]);
457
+ if (paneList.code !== 0) {
458
+ spawnDetached(options.herdrPath, ["--session", HERDR_TAILII_SESSION, "server"]);
459
+ const deadline = Date.now() + 5000;
460
+ const pollMs = options.ensurePollMs ?? 150;
461
+ while (Date.now() <= deadline) {
462
+ await sleep(pollMs);
463
+ paneList = await runHerdr(["pane", "list"]);
464
+ if (paneList.code === 0)
465
+ break;
466
+ }
467
+ if (paneList.code !== 0) {
468
+ errorSink(`tailii launch: herdr セッション '${HERDR_TAILII_SESSION}' を起動できませんでした\n`);
469
+ return 1;
470
+ }
471
+ }
472
+ // --- 2. 既存 pane の解決と stale 掃除(tmux の dead-session 掃除に対応) ---
473
+ const panes = parseHerdrPaneList(paneList.out);
474
+ const recordedPaneId = store.get(session)?.herdrPaneId;
475
+ let existing = (recordedPaneId !== undefined ? panes.find((pane) => pane.paneId === recordedPaneId) : undefined) ??
476
+ panes.find((pane) => pane.label === session) ??
477
+ null;
478
+ if (existing !== null && agent === "claude") {
479
+ // Claude が終了してシェルだけ残った pane は入力先として無効なので閉じて作り直す。
480
+ const info = await runHerdr(["pane", "process-info", "--pane", existing.paneId]);
481
+ const hasAgent = info.code !== 0 || paneCommandLooksLikeAgent(parseHerdrForegroundCommand(info.out));
482
+ if (!hasAgent) {
483
+ await runHerdr(["pane", "close", existing.paneId]);
484
+ existing = null;
485
+ }
486
+ }
487
+ let herdrPaneId = existing?.paneId ?? null;
488
+ if (existing === null) {
489
+ // --- 3. tab create で新タブ+pane を生成(0.7.5 で agent start の pane 生成/cwd/env 指定が
490
+ // 廃止されたため。anchor pane 不要なので pane ゼロの空セッションでも成立する)。
491
+ // タブラベルは付けず herdr 既定表示(連番)に任せる(session-title のタブ反映は
492
+ // 2026-07-30 撤去)。 ---
493
+ const created = await runHerdr([
494
+ "tab", "create",
495
+ "--cwd", dir,
496
+ "--env", `PATH=${options.injectedPath}`,
497
+ "--no-focus",
498
+ ]);
499
+ if (created.code !== 0) {
500
+ errorSink(`tailii launch: herdr が非0終了 (${created.code})\n`);
501
+ return created.code === 0 ? 1 : created.code;
502
+ }
503
+ herdrPaneId = parseHerdrCreatedTabPaneId(created.out);
504
+ if (herdrPaneId === null) {
505
+ errorSink("tailii launch: herdr tab create の pane_id を解決できませんでした\n");
506
+ return 1;
507
+ }
508
+ // --- 4. pane label = session 名(セッション解決の権威: findPane fallback / list 生存判定) ---
509
+ const renamed = await runHerdr(["pane", "rename", herdrPaneId, session]);
510
+ if (renamed.code !== 0) {
511
+ await runHerdr(["pane", "close", herdrPaneId]);
512
+ errorSink(`tailii launch: herdr pane rename が非0終了 (${renamed.code})\n`);
513
+ return renamed.code;
514
+ }
515
+ // --- 5. innerCommand を起動。pane run はタイプ注入で argv のクォートを保持しないため
516
+ // 単一文字列で渡す。exec 前置で claude 終了と同時に pane が閉じる
517
+ // (旧 agent start の「コマンド終了 = pane close」と同義。stale shell を残さない)。 ---
518
+ const ran = await runHerdr([
519
+ "pane", "run", herdrPaneId,
520
+ `exec /bin/zsh -lc ${shellSingleQuote(options.innerCommand)}`,
521
+ ]);
522
+ if (ran.code !== 0) {
523
+ await runHerdr(["pane", "close", herdrPaneId]);
524
+ errorSink(`tailii launch: herdr pane run が非0終了 (${ran.code})\n`);
525
+ return ran.code;
526
+ }
527
+ }
528
+ // --- cwd と backend を権威記録(tmux 版の store.put と同じ役割) ---
529
+ try {
530
+ store.put({
531
+ name: session,
532
+ cwd: dir,
533
+ createdAt: now(),
534
+ backend: "herdr",
535
+ ...(agent === "codex" ? { agent } : {}),
536
+ ...(agent === "claude" && options.claudeSessionId
537
+ ? { claudeSessionId: options.claudeSessionId }
538
+ : {}),
539
+ ...(options.providerSessionId ? { providerSessionId: options.providerSessionId } : {}),
540
+ ...(herdrPaneId !== null ? { herdrPaneId } : {}),
541
+ });
542
+ }
543
+ catch (error) {
544
+ errorSink(`tailii launch: メタデータ保存失敗: ${String(error)}\n`);
545
+ return 1;
546
+ }
547
+ return 0;
548
+ }
549
+ /**
550
+ * `~/.claude.json` の `projects[<dir>].hasTrustDialogAccepted` を true にしておく。
551
+ * claude は canonical パス(シンボリックリンク解決後)をキーに信頼を記録する。失敗は握り潰す。
552
+ */
553
+ export function preTrustFolder(dir, claudeJsonPath) {
554
+ try {
555
+ const target = claudeJsonPath ?? path.join(os.homedir(), ".claude.json");
556
+ let key;
557
+ try {
558
+ key = fs.realpathSync.native(dir);
559
+ }
560
+ catch {
561
+ key = standardize(dir);
562
+ }
563
+ let root = {};
564
+ try {
565
+ const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
566
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
567
+ root = parsed;
568
+ }
569
+ }
570
+ catch {
571
+ // 不在/壊れは空から作る。
572
+ }
573
+ const projects = typeof root["projects"] === "object" && root["projects"] !== null
574
+ ? root["projects"]
575
+ : {};
576
+ const entry = typeof projects[key] === "object" && projects[key] !== null
577
+ ? projects[key]
578
+ : {};
579
+ if (entry["hasTrustDialogAccepted"] === true)
580
+ return; // 既に信頼済み
581
+ entry["hasTrustDialogAccepted"] = true;
582
+ projects[key] = entry;
583
+ root["projects"] = projects;
584
+ const tmp = target + ".tmp";
585
+ fs.writeFileSync(tmp, JSON.stringify(root));
586
+ fs.renameSync(tmp, target);
587
+ }
588
+ catch {
589
+ // 失敗時は claude 側プロンプトへフォールバック。
590
+ }
591
+ }
592
+ /**
593
+ * cwd 入力を baseDir 基準で実パスへ解決し、baseDir 配下限定で未存在を自動作成する。
594
+ * `/` 始まり → 絶対採用 / `~` 始まり → ホーム展開 / 相対(空含む)→ `baseDir/<入力>`。
595
+ * `..` 脱出は拒否。不在 + 相対(= base 内側)のみ自動作成。失敗は errorSink へ出し null。
596
+ */
597
+ export function resolveWorkdir(cwd, baseDir, errorSink) {
598
+ let resolved;
599
+ let isRelative;
600
+ if (cwd.startsWith("/")) {
601
+ resolved = standardize(cwd);
602
+ isRelative = false;
603
+ }
604
+ else if (cwd.startsWith("~")) {
605
+ resolved = standardize(expandTilde(cwd));
606
+ isRelative = false;
607
+ }
608
+ else {
609
+ if (!baseDir) {
610
+ errorSink(`tailii launch: 相対パスにはベースディレクトリの設定が必要です: '${cwd}'\n`);
611
+ return null;
612
+ }
613
+ const joined = cwd === "" ? baseDir : baseDir + "/" + cwd;
614
+ const candidate = standardize(joined);
615
+ if (!isInsideBase(candidate, baseDir)) {
616
+ errorSink(`tailii launch: 解決先がベースディレクトリの外側です: ${candidate}\n`);
617
+ return null;
618
+ }
619
+ resolved = candidate;
620
+ isRelative = true;
621
+ }
622
+ // 存在チェック。
623
+ let stat = null;
624
+ try {
625
+ stat = fs.statSync(resolved);
626
+ }
627
+ catch {
628
+ stat = null;
629
+ }
630
+ if (stat !== null) {
631
+ if (!stat.isDirectory()) {
632
+ errorSink(`tailii launch: 作業ディレクトリがファイルです: ${resolved}\n`);
633
+ return null;
634
+ }
635
+ return resolved;
636
+ }
637
+ // 不在: base 配下(相対)のみ自動作成。絶対/`~` の不在は作成せず error。
638
+ if (!isRelative) {
639
+ errorSink(`tailii launch: 作業ディレクトリが存在しません: ${resolved}\n`);
640
+ return null;
641
+ }
642
+ try {
643
+ fs.mkdirSync(resolved, { recursive: true });
644
+ return resolved;
645
+ }
646
+ catch (error) {
647
+ errorSink(`tailii launch: 作業ディレクトリの作成に失敗しました: ${resolved}: ${String(error)}\n`);
648
+ return null;
649
+ }
650
+ }
651
+ /**
652
+ * resume 対象の cwd(絶対/`~`)が削除済みの Tailii worktree なら repo ルートへ振り替える。
653
+ * 公式 Claude Code CLI の「worktree が消えていたら起動元ディレクトリで再開」と同じ挙動
654
+ * (deleted-worktree-resume)。振り替え不要・不能なら null。
655
+ * claude の `--resume <id>` はカレントプロジェクトの slug に閉じて検索するため、
656
+ * transcript jsonl を repo ルート側 slug へ移設してから振り替える。
657
+ * 移設に失敗しても振り替え自体は行う(claude 側の未検出エラーの方が dir 不在より情報が多い)。
658
+ */
659
+ export function rescueMissingWorktreeCwd(options) {
660
+ const { cwd, claudeSessionId } = options;
661
+ if (!cwd.startsWith("/") && !cwd.startsWith("~"))
662
+ return null;
663
+ const resolved = standardize(cwd);
664
+ if (fs.existsSync(resolved))
665
+ return null;
666
+ const root = tailiiWorktreeRepoRoot(resolved);
667
+ if (root === null)
668
+ return null;
669
+ let rootStat;
670
+ try {
671
+ rootStat = fs.statSync(root);
672
+ }
673
+ catch {
674
+ return null;
675
+ }
676
+ if (!rootStat.isDirectory())
677
+ return null;
678
+ if (claudeSessionId !== null) {
679
+ const projectsDir = options.projectsDir ?? path.join(os.homedir(), ".claude", "projects");
680
+ const destDir = path.join(projectsDir, claudeProjectSlug(root));
681
+ const dest = path.join(destDir, `${claudeSessionId}.jsonl`);
682
+ if (!fs.existsSync(dest)) {
683
+ const src = new ClaudeSessionStore(projectsDir).transcriptPath(claudeSessionId);
684
+ if (src !== null && src !== dest) {
685
+ try {
686
+ fs.mkdirSync(destDir, { recursive: true });
687
+ fs.renameSync(src, dest);
688
+ }
689
+ catch {
690
+ // 移設失敗は claude の resume エラーに委ねる(上のコメント参照)。
691
+ }
692
+ // セッション付帯ディレクトリ(`<slug>/<id>/`、/rewind のファイルスナップショット等)も同居させる。
693
+ const srcAssets = src.slice(0, -".jsonl".length);
694
+ const destAssets = path.join(destDir, claudeSessionId);
695
+ try {
696
+ if (fs.existsSync(srcAssets) && !fs.existsSync(destAssets)) {
697
+ fs.renameSync(srcAssets, destAssets);
698
+ }
699
+ }
700
+ catch {
701
+ // 付帯ディレクトリの移設失敗は無視する(transcript 本体が正)。
702
+ }
703
+ }
704
+ }
705
+ }
706
+ return root;
707
+ }
708
+ /** THIS 実行中エントリスクリプトの絶対パスを解決する(フック command 埋め込み用)。 */
709
+ export function currentBinaryPath() {
710
+ const argv1 = process.argv[1];
711
+ if (argv1 && argv1.length > 0) {
712
+ return standardize(argv1);
713
+ }
714
+ return "tailii";
715
+ }
716
+ //# sourceMappingURL=launch.js.map