tailii-host 0.1.1 → 0.4.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 (354) hide show
  1. package/README.md +28 -10
  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 +260 -0
  9. package/dist/backend/herdr.js +838 -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 +24 -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/commands/selfUpdate.d.ts +71 -0
  80. package/dist/commands/selfUpdate.js +536 -0
  81. package/dist/commands/selfUpdate.js.map +1 -0
  82. package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
  83. package/dist/{setup.js → commands/setup.js} +133 -75
  84. package/dist/commands/setup.js.map +1 -0
  85. package/dist/engine/context.d.ts +145 -0
  86. package/dist/engine/context.js +184 -0
  87. package/dist/engine/context.js.map +1 -0
  88. package/dist/engine/engine.d.ts +121 -0
  89. package/dist/engine/engine.js +808 -0
  90. package/dist/engine/engine.js.map +1 -0
  91. package/dist/engine/handlers/accountUsage.d.ts +53 -0
  92. package/dist/engine/handlers/accountUsage.js +203 -0
  93. package/dist/engine/handlers/accountUsage.js.map +1 -0
  94. package/dist/engine/handlers/claudeModels.d.ts +2 -0
  95. package/dist/engine/handlers/claudeModels.js +23 -0
  96. package/dist/engine/handlers/claudeModels.js.map +1 -0
  97. package/dist/engine/handlers/codex.d.ts +2 -0
  98. package/dist/engine/handlers/codex.js +97 -0
  99. package/dist/engine/handlers/codex.js.map +1 -0
  100. package/dist/engine/handlers/conversation.d.ts +2 -0
  101. package/dist/engine/handlers/conversation.js +259 -0
  102. package/dist/engine/handlers/conversation.js.map +1 -0
  103. package/dist/engine/handlers/core.d.ts +2 -0
  104. package/dist/engine/handlers/core.js +66 -0
  105. package/dist/engine/handlers/core.js.map +1 -0
  106. package/dist/engine/handlers/git.d.ts +2 -0
  107. package/dist/engine/handlers/git.js +127 -0
  108. package/dist/engine/handlers/git.js.map +1 -0
  109. package/dist/engine/handlers/index.d.ts +2 -0
  110. package/dist/engine/handlers/index.js +34 -0
  111. package/dist/engine/handlers/index.js.map +1 -0
  112. package/dist/engine/handlers/mode.d.ts +2 -0
  113. package/dist/engine/handlers/mode.js +194 -0
  114. package/dist/engine/handlers/mode.js.map +1 -0
  115. package/dist/engine/handlers/officialApps.d.ts +2 -0
  116. package/dist/engine/handlers/officialApps.js +79 -0
  117. package/dist/engine/handlers/officialApps.js.map +1 -0
  118. package/dist/engine/handlers/preview.d.ts +2 -0
  119. package/dist/engine/handlers/preview.js +59 -0
  120. package/dist/engine/handlers/preview.js.map +1 -0
  121. package/dist/engine/handlers/selfUpdate.d.ts +2 -0
  122. package/dist/engine/handlers/selfUpdate.js +52 -0
  123. package/dist/engine/handlers/selfUpdate.js.map +1 -0
  124. package/dist/engine/handlers/session.d.ts +2 -0
  125. package/dist/engine/handlers/session.js +385 -0
  126. package/dist/engine/handlers/session.js.map +1 -0
  127. package/dist/engine/handlers/usage.d.ts +2 -0
  128. package/dist/engine/handlers/usage.js +96 -0
  129. package/dist/engine/handlers/usage.js.map +1 -0
  130. package/dist/engine/handlers/workspace.d.ts +2 -0
  131. package/dist/engine/handlers/workspace.js +110 -0
  132. package/dist/engine/handlers/workspace.js.map +1 -0
  133. package/dist/engine/slashCommands.d.ts +3 -0
  134. package/dist/engine/slashCommands.js +211 -0
  135. package/dist/engine/slashCommands.js.map +1 -0
  136. package/dist/hub/engineRelaySocket.d.ts +58 -0
  137. package/dist/hub/engineRelaySocket.js +161 -0
  138. package/dist/hub/engineRelaySocket.js.map +1 -0
  139. package/dist/hub/hubClient.d.ts +28 -0
  140. package/dist/hub/hubClient.js +187 -0
  141. package/dist/hub/hubClient.js.map +1 -0
  142. package/dist/hub/hubDaemon.d.ts +69 -0
  143. package/dist/hub/hubDaemon.js +466 -0
  144. package/dist/hub/hubDaemon.js.map +1 -0
  145. package/dist/hub/hubProtocol.d.ts +161 -0
  146. package/dist/hub/hubProtocol.js +379 -0
  147. package/dist/hub/hubProtocol.js.map +1 -0
  148. package/dist/hub/panePreviewPump.d.ts +65 -0
  149. package/dist/hub/panePreviewPump.js +187 -0
  150. package/dist/hub/panePreviewPump.js.map +1 -0
  151. package/dist/hub/questionInjection.d.ts +19 -0
  152. package/dist/hub/questionInjection.js +126 -0
  153. package/dist/hub/questionInjection.js.map +1 -0
  154. package/dist/hub/reaper.d.ts +74 -0
  155. package/dist/hub/reaper.js +333 -0
  156. package/dist/hub/reaper.js.map +1 -0
  157. package/dist/hub/sessionHub.d.ts +203 -0
  158. package/dist/hub/sessionHub.js +1862 -0
  159. package/dist/hub/sessionHub.js.map +1 -0
  160. package/dist/protocol/common.d.ts +18 -0
  161. package/dist/protocol/common.js +78 -0
  162. package/dist/protocol/common.js.map +1 -0
  163. package/dist/protocol/decode.d.ts +3 -0
  164. package/dist/protocol/decode.js +1058 -0
  165. package/dist/protocol/decode.js.map +1 -0
  166. package/dist/protocol/encode.d.ts +3 -0
  167. package/dist/protocol/encode.js +84 -0
  168. package/dist/protocol/encode.js.map +1 -0
  169. package/dist/protocol/messages.d.ts +1025 -0
  170. package/dist/protocol/messages.js +8 -0
  171. package/dist/protocol/messages.js.map +1 -0
  172. package/dist/protocol.d.ts +4 -329
  173. package/dist/protocol.js +13 -531
  174. package/dist/protocol.js.map +1 -1
  175. package/dist/push/apnsConfigStore.js.map +1 -0
  176. package/dist/push/apnsJwtProvider.js.map +1 -0
  177. package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
  178. package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
  179. package/dist/push/approvalPushNotifier.js.map +1 -0
  180. package/dist/push/deviceTokenStore.js.map +1 -0
  181. package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
  182. package/dist/push/pushTypes.js.map +1 -0
  183. package/dist/push/sendLog.js.map +1 -0
  184. package/dist/services/accountIdentity.d.ts +44 -0
  185. package/dist/services/accountIdentity.js +184 -0
  186. package/dist/services/accountIdentity.js.map +1 -0
  187. package/dist/services/claudeCredentialRefresh.d.ts +30 -0
  188. package/dist/services/claudeCredentialRefresh.js +345 -0
  189. package/dist/services/claudeCredentialRefresh.js.map +1 -0
  190. package/dist/services/claudeModelCatalog.d.ts +21 -0
  191. package/dist/services/claudeModelCatalog.js +181 -0
  192. package/dist/services/claudeModelCatalog.js.map +1 -0
  193. package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
  194. package/dist/services/dirLister.js +169 -0
  195. package/dist/services/dirLister.js.map +1 -0
  196. package/dist/services/fileService.d.ts +10 -0
  197. package/dist/services/fileService.js +123 -0
  198. package/dist/services/fileService.js.map +1 -0
  199. package/dist/services/gitService.d.ts +79 -0
  200. package/dist/services/gitService.js +648 -0
  201. package/dist/services/gitService.js.map +1 -0
  202. package/dist/services/hostVersions.d.ts +38 -0
  203. package/dist/services/hostVersions.js +120 -0
  204. package/dist/services/hostVersions.js.map +1 -0
  205. package/dist/services/officialApps.d.ts +148 -0
  206. package/dist/services/officialApps.js +1079 -0
  207. package/dist/services/officialApps.js.map +1 -0
  208. package/dist/services/planUsageFetcher.d.ts +119 -0
  209. package/dist/services/planUsageFetcher.js +357 -0
  210. package/dist/services/planUsageFetcher.js.map +1 -0
  211. package/dist/services/previewServer.d.ts +31 -0
  212. package/dist/services/previewServer.js +233 -0
  213. package/dist/services/previewServer.js.map +1 -0
  214. package/dist/services/quicGateway.d.ts +143 -0
  215. package/dist/services/quicGateway.js +292 -0
  216. package/dist/services/quicGateway.js.map +1 -0
  217. package/dist/services/serveService.d.ts +50 -0
  218. package/dist/services/serveService.js +302 -0
  219. package/dist/services/serveService.js.map +1 -0
  220. package/dist/services/usageAggregator.js.map +1 -0
  221. package/dist/sessions/claudeSessionStore.d.ts +80 -0
  222. package/dist/sessions/claudeSessionStore.js +477 -0
  223. package/dist/sessions/claudeSessionStore.js.map +1 -0
  224. package/dist/sessions/heartbeat.d.ts +24 -0
  225. package/dist/sessions/heartbeat.js +86 -0
  226. package/dist/sessions/heartbeat.js.map +1 -0
  227. package/dist/sessions/liveSessionJoin.d.ts +20 -0
  228. package/dist/sessions/liveSessionJoin.js +80 -0
  229. package/dist/sessions/liveSessionJoin.js.map +1 -0
  230. package/dist/sessions/sessionActivityProvider.d.ts +14 -0
  231. package/dist/sessions/sessionActivityProvider.js +57 -0
  232. package/dist/sessions/sessionActivityProvider.js.map +1 -0
  233. package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
  234. package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
  235. package/dist/sessions/sessionListService.js.map +1 -0
  236. package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
  237. package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
  238. package/dist/sessions/sessionMetadataStore.js.map +1 -0
  239. package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
  240. package/dist/sessions/sessionSearch.js.map +1 -0
  241. package/dist/shared/hostDisplayName.d.ts +11 -0
  242. package/dist/shared/hostDisplayName.js +50 -0
  243. package/dist/shared/hostDisplayName.js.map +1 -0
  244. package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
  245. package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
  246. package/dist/shared/lineWriter.js.map +1 -0
  247. package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
  248. package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
  249. package/dist/{paths.js → shared/paths.js} +9 -0
  250. package/dist/shared/paths.js.map +1 -0
  251. package/dist/shared/permissionMode.d.ts +10 -0
  252. package/dist/shared/permissionMode.js +51 -0
  253. package/dist/shared/permissionMode.js.map +1 -0
  254. package/dist/shared/skillInjection.d.ts +9 -0
  255. package/dist/shared/skillInjection.js +16 -0
  256. package/dist/shared/skillInjection.js.map +1 -0
  257. package/dist/shared/sleep.js.map +1 -0
  258. package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
  259. package/dist/{socketPath.js → shared/socketPath.js} +22 -0
  260. package/dist/shared/socketPath.js.map +1 -0
  261. package/dist/shared/version.js.map +1 -0
  262. package/package.json +13 -7
  263. package/dist/apnsConfigStore.js.map +0 -1
  264. package/dist/apnsJwtProvider.js.map +0 -1
  265. package/dist/approvalPushNotifier.js.map +0 -1
  266. package/dist/broker.js.map +0 -1
  267. package/dist/chatTailController.js.map +0 -1
  268. package/dist/claudeSessionStore.d.ts +0 -15
  269. package/dist/claudeSessionStore.js +0 -201
  270. package/dist/claudeSessionStore.js.map +0 -1
  271. package/dist/codexRolloutTailer.d.ts +0 -56
  272. package/dist/codexRolloutTailer.js +0 -303
  273. package/dist/codexRolloutTailer.js.map +0 -1
  274. package/dist/codexSessionStore.js.map +0 -1
  275. package/dist/codexUsage.js.map +0 -1
  276. package/dist/deviceTokenStore.js.map +0 -1
  277. package/dist/dirLister.js +0 -98
  278. package/dist/dirLister.js.map +0 -1
  279. package/dist/doctor.d.ts +0 -36
  280. package/dist/doctor.js +0 -162
  281. package/dist/doctor.js.map +0 -1
  282. package/dist/engine.d.ts +0 -85
  283. package/dist/engine.js +0 -962
  284. package/dist/engine.js.map +0 -1
  285. package/dist/hook.js.map +0 -1
  286. package/dist/hookSettings.js.map +0 -1
  287. package/dist/idleReaper.d.ts +0 -13
  288. package/dist/idleReaper.js +0 -30
  289. package/dist/idleReaper.js.map +0 -1
  290. package/dist/imageService.js.map +0 -1
  291. package/dist/kick.js.map +0 -1
  292. package/dist/launch.js +0 -396
  293. package/dist/launch.js.map +0 -1
  294. package/dist/legacyHomeMigration.js.map +0 -1
  295. package/dist/lineWriter.js.map +0 -1
  296. package/dist/mdnsAdvertiser.d.ts +0 -46
  297. package/dist/mdnsAdvertiser.js +0 -268
  298. package/dist/mdnsAdvertiser.js.map +0 -1
  299. package/dist/pairingCode.js.map +0 -1
  300. package/dist/paths.js.map +0 -1
  301. package/dist/permissionMode.d.ts +0 -8
  302. package/dist/permissionMode.js +0 -32
  303. package/dist/permissionMode.js.map +0 -1
  304. package/dist/planUsageFetcher.d.ts +0 -35
  305. package/dist/planUsageFetcher.js +0 -172
  306. package/dist/planUsageFetcher.js.map +0 -1
  307. package/dist/pushTokenCommand.js.map +0 -1
  308. package/dist/pushTypes.js.map +0 -1
  309. package/dist/sendLog.js.map +0 -1
  310. package/dist/sessionActivityProvider.d.ts +0 -7
  311. package/dist/sessionActivityProvider.js +0 -44
  312. package/dist/sessionActivityProvider.js.map +0 -1
  313. package/dist/sessionIdleTracker.d.ts +0 -19
  314. package/dist/sessionIdleTracker.js +0 -42
  315. package/dist/sessionIdleTracker.js.map +0 -1
  316. package/dist/sessionListService.js.map +0 -1
  317. package/dist/sessionMetadataStore.js.map +0 -1
  318. package/dist/sessionSearch.js.map +0 -1
  319. package/dist/setup.js.map +0 -1
  320. package/dist/sleep.js.map +0 -1
  321. package/dist/socketPath.js.map +0 -1
  322. package/dist/subagentTailer.d.ts +0 -15
  323. package/dist/subagentTailer.js +0 -399
  324. package/dist/subagentTailer.js.map +0 -1
  325. package/dist/tmux.d.ts +0 -62
  326. package/dist/tmux.js +0 -163
  327. package/dist/tmux.js.map +0 -1
  328. package/dist/transcriptTailer.js.map +0 -1
  329. package/dist/usageAggregator.js.map +0 -1
  330. package/dist/version.js.map +0 -1
  331. /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
  332. /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
  333. /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
  334. /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
  335. /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
  336. /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
  337. /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
  338. /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
  339. /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
  340. /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
  341. /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
  342. /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
  343. /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
  344. /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
  345. /package/dist/{sendLog.js → push/sendLog.js} +0 -0
  346. /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
  347. /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
  348. /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
  349. /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
  350. /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
  351. /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
  352. /package/dist/{sleep.js → shared/sleep.js} +0 -0
  353. /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
  354. /package/dist/{version.js → shared/version.js} +0 -0
package/dist/engine.js DELETED
@@ -1,962 +0,0 @@
1
- // engine.ts
2
- // tailii (TS host) — engine サブコマンド実装(EngineControl)
3
- // Swift 版 Engine.swift の移植。
4
- //
5
- // セッション横断制御チャネル。stdin/stdout(SSH exec の stdio)で NDJSON 制御メッセージを
6
- // 送受信し、session_* を TmuxSessionManager に橋渡しする。承認 socket は経由しない
7
- // (横断制御を承認中継から位相分離)。
8
- //
9
- // 責務:
10
- // - チャネル確立直後に channel_hello(maxVersion) を送出し、相手 hello 受信で
11
- // 採用版 = min(双方 maxVersion) を決める(4.3)。
12
- // - session_list_request / session_reattach / session_kill / session_start /
13
- // session_idle_hint / usage / mode / image / dir / browse / claude-sessions の橋渡し。
14
- // - decode 失敗行は破棄(承認文脈でないので単に無視、クラッシュしない)。
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
- import * as readline from "node:readline";
18
- import * as fs from "node:fs";
19
- import { randomUUID } from "node:crypto";
20
- import { ChatTailController } from "./chatTailController.js";
21
- import { ClaudeSessionStore } from "./claudeSessionStore.js";
22
- import { CodexSessionStore } from "./codexSessionStore.js";
23
- import { dirChildren, dirCreate, dirList } from "./dirLister.js";
24
- import { runIdleReaper } from "./idleReaper.js";
25
- import { ImageService } from "./imageService.js";
26
- import { makeSessionLauncher, codexInnerCommand } from "./launch.js";
27
- import { LineWriter } from "./lineWriter.js";
28
- import { parsePermissionMode } from "./permissionMode.js";
29
- import { fetchPlanUsage } from "./planUsageFetcher.js";
30
- import { decodeControlMessage, PROTOCOL_MAX_SUPPORTED, PROTOCOL_V1, } from "./protocol.js";
31
- import { claudeTranscriptActivityProvider } from "./sessionActivityProvider.js";
32
- import { SessionIdleTracker } from "./sessionIdleTracker.js";
33
- import { SessionListService } from "./sessionListService.js";
34
- import { searchClaudeSessions } from "./sessionSearch.js";
35
- import { SessionMetadataStore } from "./sessionMetadataStore.js";
36
- import { sleep } from "./sleep.js";
37
- import { TranscriptTailer } from "./transcriptTailer.js";
38
- import { TmuxSessionManager } from "./tmux.js";
39
- import { aggregateUsage, emptyUsageTotals } from "./usageAggregator.js";
40
- import { aggregateCodexUsage } from "./codexUsage.js";
41
- import { createStaleDistGuard, isStaleDist, readPackageVersion } from "./version.js";
42
- // MARK: - エントリポイント(cli から呼ばれる)
43
- /**
44
- * engine サブコマンドのエントリポイント。実 stdin/stdout を使って EngineControl を起動する。
45
- * 引数(省略可、統合テスト注入用): --sessions-dir / --inner-command / --images-dir /
46
- * --transcript / --claude-projects-dir / --idle-timeout / --resume-command。
47
- */
48
- /**
49
- * 既定エージェントを host 側設定から解決する(`--agent` 未指定時のフォールバック)。
50
- * `~/.tailii/agent` の内容が `codex` なら codex、それ以外/不在は claude。
51
- * これにより iOS を改修せず(engine 起動コマンドは `<binary> engine` のまま)codex に切替できる。
52
- */
53
- export function resolveDefaultAgent(agentFilePath = path.join(os.homedir(), ".tailii", "agent")) {
54
- try {
55
- const value = fs.readFileSync(agentFilePath, "utf8").trim().toLowerCase();
56
- if (value === "codex")
57
- return "codex";
58
- }
59
- catch {
60
- // 不在/読取失敗は既定 claude。
61
- }
62
- return "claude";
63
- }
64
- export async function runEngineCommand(args) {
65
- process.stderr.write("[tailii-host engine] 起動(横断制御チャネル)\n");
66
- let sessionsDirArg = null;
67
- let innerCommandArg = null;
68
- let imagesDirArg = null;
69
- let transcriptArg = null;
70
- let claudeProjectsDirArg = null;
71
- let idleTimeoutArg = null;
72
- let resumeCommandArg = null;
73
- // 既定エージェントは host 側設定ファイルで切替可能(iOS 改修不要のトグル)。
74
- // `--agent` フラグが渡ればそちらが優先する。
75
- let agentArg = resolveDefaultAgent();
76
- for (let i = 0; i < args.length; i += 1) {
77
- const next = () => (i + 1 < args.length ? args[++i] : null);
78
- switch (args[i]) {
79
- case "--sessions-dir":
80
- sessionsDirArg = next();
81
- break;
82
- case "--inner-command":
83
- innerCommandArg = next();
84
- break;
85
- case "--agent": {
86
- const raw = next();
87
- if (raw === "codex" || raw === "claude")
88
- agentArg = raw;
89
- break;
90
- }
91
- case "--images-dir":
92
- imagesDirArg = next();
93
- break;
94
- case "--transcript":
95
- transcriptArg = next();
96
- break;
97
- case "--claude-projects-dir":
98
- claudeProjectsDirArg = next();
99
- break;
100
- case "--idle-timeout": {
101
- const raw = next();
102
- const parsed = raw === null ? Number.NaN : Number.parseInt(raw, 10);
103
- if (Number.isFinite(parsed))
104
- idleTimeoutArg = parsed;
105
- break;
106
- }
107
- case "--resume-command":
108
- resumeCommandArg = next();
109
- break;
110
- default:
111
- break;
112
- }
113
- }
114
- // list と launch が同一の cwd 権威ストアを共有するよう、ここで1つだけ構築して両者へ渡す。
115
- const store = new SessionMetadataStore(sessionsDirArg ?? undefined);
116
- // 画像シーム: hook が投入した pending の drain と image_fetch_request の index 逆引き配信。
117
- const imageService = imagesDirArg
118
- ? new ImageService({
119
- pendingBase: path.join(imagesDirArg, "pending"),
120
- indexBase: path.join(imagesDirArg, "index"),
121
- })
122
- : new ImageService();
123
- // 会話出力シーム: --transcript 注入時だけ TranscriptTailer を構築する(後方互換)。
124
- const transcriptTailer = transcriptArg ? new TranscriptTailer({ tailIndefinitely: true }) : null;
125
- // claude 会話ルート(`~/.claude/projects`)。セッション連動 chat_output tail の解決基点。
126
- const claudeProjectsRoot = claudeProjectsDirArg ?? path.join(os.homedir(), ".claude", "projects");
127
- // 一覧整形(session-list-lifecycle): updatedAt 付与・整列・ページング。
128
- const sessionManager = new TmuxSessionManager({ store });
129
- const sessionListService = new SessionListService(sessionManager, claudeTranscriptActivityProvider());
130
- // アイドルライフサイクル: timeout は host 側設定(既定 30 分)。
131
- const idleTimeout = idleTimeoutArg ?? 1800;
132
- const idleTracker = new SessionIdleTracker(idleTimeout);
133
- const reaperInterval = Math.min(idleTimeout, 60);
134
- // resume 再起動 launcher(kill 済みセッションを記録 cwd で claude --continue 再起動)。
135
- const resumeLauncher = makeSessionLauncher({
136
- store,
137
- innerCommand: resumeCommandArg ?? "claude --continue",
138
- agent: "claude",
139
- });
140
- // per-session: agentType=codex のセッション用に codex launcher / resume launcher を用意する。
141
- // codex は resume 未対応のため既定コマンドで新規起動する(新しい rollout を tail)。
142
- const codexLauncher = makeSessionLauncher({ store, agent: "codex" });
143
- const codexResumeLauncher = makeSessionLauncher({ store, agent: "codex" });
144
- try {
145
- await runEngine({
146
- input: process.stdin,
147
- output: process.stdout,
148
- sessionManager,
149
- imageService,
150
- transcriptTailer,
151
- transcriptPath: transcriptArg,
152
- // launcher は常に claude 版。codex は codexLauncher で分岐する(per-session)。
153
- // agentType 未指定時にどちらへ倒すかは engine 内 defaultAgent(=agentArg) が決める。
154
- launcher: makeSessionLauncher({ store, innerCommand: innerCommandArg, agent: "claude" }),
155
- codexLauncher,
156
- sessionListService,
157
- metadataStore: store,
158
- idleTracker,
159
- resumeLauncher,
160
- codexResumeLauncher,
161
- claudeSessionStore: new ClaudeSessionStore(claudeProjectsRoot),
162
- codexSessionStore: new CodexSessionStore(),
163
- reaperCheckIntervalSeconds: reaperInterval,
164
- chatTailProjectsRoot: claudeProjectsRoot,
165
- agent: agentArg,
166
- });
167
- return 0;
168
- }
169
- catch (error) {
170
- process.stderr.write(`[tailii-host engine] 異常終了: ${String(error)}\n`);
171
- return 1;
172
- }
173
- }
174
- const DEFAULT_MODE_TIMING = {
175
- getPollMs: 250,
176
- getAttempts: 4,
177
- setInitialPollMs: 300,
178
- setInitialTimeoutMs: 10_000,
179
- setChangePollMs: 150,
180
- setChangeTimeoutMs: 1_500,
181
- };
182
- /**
183
- * EngineControl の本体。`input` から NDJSON 行を読み、session_* を `sessionManager` に
184
- * 橋渡しし、応答を `output` へ NDJSON 行で書く。EOF(input end)で正常終了する。
185
- */
186
- export async function runEngine(options) {
187
- const { sessionManager, imageService = null, transcriptTailer = null, transcriptPath = null, launcher = null, codexLauncher = null, sessionListService = null, metadataStore = null, idleTracker = null, resumeLauncher = null, codexResumeLauncher = null, claudeSessionStore = null, codexSessionStore = null, reaperCheckIntervalSeconds = 60, chatTailProjectsRoot = null, agent = "claude", codexTailer = undefined, maxVersion = PROTOCOL_MAX_SUPPORTED, planUsage = () => fetchPlanUsage(), homeDir = os.homedir(), modeTiming = {}, staleDistGuard = createStaleDistGuard(), onStaleDist = undefined, } = options;
188
- const resolvedModeTiming = { ...DEFAULT_MODE_TIMING, ...modeTiming };
189
- // 出力の直列化(Node の Writable は書込順序を保証する)。
190
- const writer = new LineWriter(options.output);
191
- // 採用版(negotiated version)。相手 hello 受信で min を採る。
192
- const state = {
193
- negotiatedVersion: maxVersion,
194
- ownMaxVersion: maxVersion,
195
- modeSetInFlight: new Set(),
196
- };
197
- // ---- セッション連動 chat_output tail(本番配線)----
198
- // `--transcript` 明示指定が無く projectsRoot が注入されたときだけ controller を構築する。
199
- const chatTailController = transcriptPath === null && chatTailProjectsRoot !== null
200
- ? new ChatTailController({
201
- writer,
202
- tailer: transcriptTailer ??
203
- new TranscriptTailer({ tailIndefinitely: true, emitReplayDoneMarker: true }),
204
- projectsRoot: chatTailProjectsRoot,
205
- imageService,
206
- protocolVersion: () => state.negotiatedVersion,
207
- agent,
208
- ...(codexTailer !== undefined && { codexTailer }),
209
- })
210
- : null;
211
- const lifecycleAbort = new AbortController();
212
- const background = [];
213
- try {
214
- // ---- 1. channel_hello を送出(確立直後)----
215
- const helloVersion = staleDistGuard?.startupVersion ?? readPackageVersion() ?? undefined;
216
- writer.write({
217
- type: "channel_hello",
218
- v: PROTOCOL_V1,
219
- maxVersion,
220
- ...(helloVersion !== undefined ? { serverVersion: helloVersion } : {}),
221
- });
222
- // ---- 1.5 画像 pending を drain し image_available を送出 ----
223
- if (imageService !== null) {
224
- for (const message of await imageService.drainPending()) {
225
- writer.write(message);
226
- }
227
- }
228
- // ---- 1.6 会話出力 tail を開始し chat_output を engine チャネルへ逐次送出 ----
229
- if (transcriptTailer !== null && transcriptPath !== null) {
230
- background.push((async () => {
231
- for await (const message of transcriptTailer.streamTranscript(transcriptPath, lifecycleAbort.signal)) {
232
- if (lifecycleAbort.signal.aborted)
233
- break;
234
- try {
235
- writer.write(message);
236
- }
237
- catch (error) {
238
- process.stderr.write(`[tailii-host engine] chat_output 書込失敗: ${String(error)}\n`);
239
- break;
240
- }
241
- }
242
- })());
243
- }
244
- // ---- 1.7 アイドル reaper を常駐起動 ----
245
- if (idleTracker !== null) {
246
- background.push(runIdleReaper({
247
- tracker: idleTracker,
248
- sessionManager,
249
- checkIntervalSeconds: reaperCheckIntervalSeconds,
250
- signal: lifecycleAbort.signal,
251
- }));
252
- }
253
- // ---- 2. 行読み取りループ ----
254
- ChatTailController.diag(`engine readLoop 開始 pid=${process.pid}`);
255
- const rl = readline.createInterface({ input: options.input, crlfDelay: Number.POSITIVE_INFINITY });
256
- try {
257
- for await (const line of rl) {
258
- const didProcessMessage = await handleLine(line, {
259
- writer,
260
- state,
261
- sessionManager,
262
- imageService,
263
- launcher,
264
- codexLauncher,
265
- sessionListService,
266
- metadataStore,
267
- idleTracker,
268
- resumeLauncher,
269
- codexResumeLauncher,
270
- claudeSessionStore,
271
- codexSessionStore,
272
- chatTailController,
273
- planUsage,
274
- homeDir,
275
- modeTiming: resolvedModeTiming,
276
- defaultAgent: agent,
277
- });
278
- if (didProcessMessage && isStaleDist(staleDistGuard)) {
279
- process.stderr.write("[tailii-host engine] stale dist を検出、再起動のため終了\n");
280
- onStaleDist?.();
281
- break;
282
- }
283
- }
284
- }
285
- finally {
286
- rl.close();
287
- }
288
- ChatTailController.diag(`engine readLoop EOF(チャネル断)pid=${process.pid}`);
289
- }
290
- finally {
291
- // ---- 3. チャネル断で chat_output tail / reaper を確実に停止する(全経路) ----
292
- lifecycleAbort.abort();
293
- chatTailController?.stop();
294
- await Promise.allSettled(background);
295
- }
296
- }
297
- /** 1行(改行なし)をデコードし、メッセージ種別ごとに処理する。decode 失敗は破棄。 */
298
- async function handleLine(rawLine, ctx) {
299
- const trimmed = rawLine.replaceAll("\r", "");
300
- if (!trimmed)
301
- return false;
302
- let message;
303
- try {
304
- message = decodeControlMessage(trimmed);
305
- }
306
- catch (error) {
307
- // 破棄(不正 JSON / 未知 type / 非対応版 / 必須欠落)。承認文脈でないので無視。
308
- ChatTailController.diag(`engine decode 失敗(行破棄): ${String(error)} 生=${trimmed.slice(0, 120)}`);
309
- process.stderr.write(`[tailii-host engine] decode 失敗、行破棄: ${String(error)}\n`);
310
- return false;
311
- }
312
- ChatTailController.diag(`engine 受信 type=${message.type}`);
313
- const { writer, state, sessionManager, metadataStore, chatTailController } = ctx;
314
- const v = state.negotiatedVersion;
315
- switch (message.type) {
316
- case "channel_hello": {
317
- // 採用版 = min(自分の maxVersion, 相手の maxVersion)(4.3)。
318
- state.negotiatedVersion = Math.min(state.ownMaxVersion, message.maxVersion);
319
- process.stderr.write(`[tailii-host engine] channel_hello negotiated v=${state.negotiatedVersion}\n`);
320
- break;
321
- }
322
- case "session_list_request": {
323
- // ページング応答。service 未注入時は従来の全件 list にフォールバックする(後方互換)。
324
- try {
325
- if (ctx.sessionListService !== null) {
326
- const page = await ctx.sessionListService.page(message.limit, message.cursor);
327
- writeSessionListResponse(writer, v, message.id, page.sessions, page.nextCursor);
328
- }
329
- else {
330
- const sessions = await sessionManager.list();
331
- writeSessionListResponse(writer, v, message.id, sessions, null);
332
- }
333
- }
334
- catch (error) {
335
- writeError(writer, v, message.id, "tmux_error", String(error));
336
- }
337
- break;
338
- }
339
- case "session_reattach": {
340
- // 再アクティブ化(アイドル計時解除)→ 生存なら即 reattach / メタあり tmux 不在は
341
- // 記録 cwd で resume 再起動 → attached / メタ無しは従来の not_found。
342
- ctx.idleTracker?.markActive(message.name);
343
- try {
344
- const result = await sessionManager.reattach(message.name);
345
- if (result.kind === "attached") {
346
- writeSessionListResponse(writer, v, message.id, [result.info], null);
347
- const meta = metadataStore?.get(message.name) ?? null;
348
- const openCwd = meta?.cwd ?? result.info.cwd;
349
- // セッション記録の agent(codex/claude)で tail mode を選ぶ。
350
- chatTailController?.open(openCwd, meta?.claudeSessionId ?? null, null, meta?.agent ?? ctx.defaultAgent);
351
- }
352
- else {
353
- const meta = metadataStore?.get(message.name) ?? null;
354
- if (ctx.resumeLauncher !== null && meta !== null) {
355
- // codex セッションは codex 用 resume launcher で再起動する。
356
- const reAgent = meta.agent ?? ctx.defaultAgent;
357
- // claude で会話 id が記録済みなら通常 launcher の `--resume <id>` で同一会話を
358
- // 厳密に再開する(resumeLauncher の `--continue` は cwd の最新会話を拾うため、
359
- // 別会話を再開して tail 束縛と食い違い得る)。id 未記録の旧メタは従来経路。
360
- const strictClaudeResume = reAgent === "claude" && meta.claudeSessionId != null && ctx.launcher !== null;
361
- const chosenResume = reAgent === "codex"
362
- ? (ctx.codexResumeLauncher ?? ctx.resumeLauncher)
363
- : strictClaudeResume
364
- ? ctx.launcher
365
- : ctx.resumeLauncher;
366
- const res = await chosenResume(meta.cwd, message.name, null, strictClaudeResume ? (meta.claudeSessionId ?? null) : null);
367
- if (res.exitCode === 0) {
368
- const info = { name: message.name, cwd: meta.cwd, alive: true };
369
- writeSessionListResponse(writer, v, message.id, [info], null);
370
- chatTailController?.open(meta.cwd, meta.claudeSessionId ?? null, null, reAgent);
371
- }
372
- else {
373
- const m = res.errorText || `resume 失敗 (exit ${res.exitCode})`;
374
- writeError(writer, v, message.id, "launch_failed", m);
375
- }
376
- }
377
- else if (result.error.type === "error") {
378
- writeError(writer, v, message.id, result.error.code, result.error.message);
379
- }
380
- else {
381
- writeError(writer, v, message.id, "session_not_found", `セッション '${message.name}' は存在しません。`);
382
- }
383
- }
384
- }
385
- catch (error) {
386
- writeError(writer, v, message.id, "tmux_error", String(error));
387
- }
388
- break;
389
- }
390
- case "session_kill": {
391
- try {
392
- await sessionManager.kill(message.name);
393
- // kill 成功は list 応答(現況一覧)で返す(疎通確認)。
394
- let sessions = [];
395
- try {
396
- sessions = await sessionManager.list();
397
- }
398
- catch {
399
- sessions = [];
400
- }
401
- writeSessionListResponse(writer, v, message.id, sessions, null);
402
- }
403
- catch (error) {
404
- writeError(writer, v, message.id, "tmux_error", String(error));
405
- }
406
- break;
407
- }
408
- case "session_idle_hint": {
409
- // アイドル起点を記録する(chat 離脱, 要件 4.2)。以後 reaper が timeout 超過で kill する。
410
- ctx.idleTracker?.markIdle(message.name, Math.floor(Date.now() / 1000));
411
- break;
412
- }
413
- case "session_start": {
414
- // session_start → launch() 結線。agentType でセッション毎に claude/codex を選ぶ
415
- //(未指定は host 既定 defaultAgent)。codex は agentType=codex 時の専用 launcher を使う。
416
- const sessionAgent = message.agentType ?? ctx.defaultAgent;
417
- // codex 新規起動でモデル/サンドボックス指定があれば、その flags を持つ launcher をその場で組む
418
- //(codex-input)。resume は元セッションの設定を継ぐため既定 codexLauncher を使う。
419
- const perSessionCodexLauncher = sessionAgent === "codex" &&
420
- message.resumeSessionId === undefined &&
421
- (message.codexModel !== undefined || message.codexSandbox !== undefined)
422
- ? makeSessionLauncher({
423
- ...(ctx.metadataStore !== null && { store: ctx.metadataStore }),
424
- agent: "codex",
425
- innerCommand: codexInnerCommand({
426
- model: message.codexModel ?? null,
427
- sandbox: message.codexSandbox ?? null,
428
- }),
429
- })
430
- : null;
431
- const chosenLauncher = sessionAgent === "codex"
432
- ? (perSessionCodexLauncher ?? ctx.codexLauncher ?? ctx.launcher)
433
- : ctx.launcher;
434
- if (chosenLauncher === null) {
435
- // 未注入(テスト構成漏れ等): 安全側 — 実起動せず構造化 error を返す。
436
- process.stderr.write("[tailii-host engine] session_start: launcher 未構成\n");
437
- writeError(writer, v, message.id, "launch_failed", "launch 機能が構成されていません。");
438
- break;
439
- }
440
- const resumeSessionId = message.resumeSessionId ?? null;
441
- // 新規起動は host 生成の session-id で claude を起動し(`--session-id <uuid>`)、
442
- // 会話 jsonl 名を事前に確定させる。tail はその id の jsonl だけを追うため、同一 cwd に
443
- // 別の稼働セッションがあっても、そのログが新セッションへ流れ込まない(取り違え防止)。
444
- const newSessionId = resumeSessionId === null ? randomUUID() : null;
445
- // codex は session-id 固定を持たず mtime で rollout を解決するため、新規起動は
446
- // 「起動時刻より後に更新された rollout」に限定する(古い rollout の流入防止)。
447
- // claude は preferred=newSessionId で厳密束縛するため newerThanMs は効かない(無害)。
448
- const launchedAtMs = Date.now();
449
- ChatTailController.diag(`session_start launcher 呼出前 cwd=${message.cwd} name=${message.name} resume=${resumeSessionId ?? "nil"} newId=${newSessionId ?? "nil"}`);
450
- const result = await chosenLauncher(message.cwd, message.name, message.baseDir ?? null, resumeSessionId, newSessionId, message.title ?? null);
451
- ChatTailController.diag(`session_start launcher 結果 exit=${result.exitCode} err=${result.errorText.slice(0, 100)} tailCtrl=${chatTailController === null ? "nil" : "ok"}`);
452
- if (result.exitCode === 0) {
453
- // cwd は launcher が権威記録した解決後 cwd(メタ)を優先。
454
- const openCwd = metadataStore?.get(message.name)?.cwd ?? message.cwd;
455
- // tail は確定した会話 id(resume=既存 id / 新規=生成 id)だけを追う。newerThanMs は
456
- // codex(mtime 解決)の新規起動でのみ効き、claude は preferred で厳密束縛される。
457
- chatTailController?.open(openCwd, resumeSessionId ?? newSessionId, resumeSessionId === null ? launchedAtMs : null, sessionAgent);
458
- // 成功: 現況一覧で応答する(kill と同じ疎通様式)。
459
- try {
460
- const sessions = await sessionManager.list();
461
- writeSessionListResponse(writer, v, message.id, sessions, null);
462
- }
463
- catch {
464
- // 起動自体は成功しているため、一覧取得失敗時は当該セッション単独で応答する。
465
- try {
466
- writeSessionListResponse(writer, v, message.id, [{ name: message.name, cwd: message.cwd, alive: true }], null);
467
- }
468
- catch {
469
- // 書込失敗は握り潰す(Swift 版 try? と同じ)。
470
- }
471
- }
472
- }
473
- else {
474
- const m = result.errorText || `launch 失敗 (exit ${result.exitCode})`;
475
- writeError(writer, v, message.id, "launch_failed", m);
476
- }
477
- break;
478
- }
479
- case "usage_request": {
480
- // 開いている会話が codex なら常に codex 集計で応答する(rate_limits を含む, codex-input)。
481
- // 分岐は「rollout が解決済みか」ではなく「tail 中の会話が codex か」で行う。rollout 未解決でも
482
- // Claude の OAuth プラン使用量 API へは絶対に落とさない(codex 会話に Claude の状態が出る不具合
483
- // の防止, 2026-07-07 ユーザー指摘)。codex は OAuth プラン使用量非対応なので planUsage は使わない。
484
- const tailAgent = chatTailController?.currentAgent() ?? ctx.defaultAgent;
485
- if (tailAgent === "codex") {
486
- const codexRollout = chatTailController?.currentCodexRolloutPath() ?? null;
487
- // rollout 未解決(起動直後等)は空集計で返す。Claude 分岐へは落とさない。
488
- const cu = codexRollout !== null ? aggregateCodexUsage(codexRollout) : { ...emptyUsageTotals() };
489
- try {
490
- writer.write({
491
- type: "usage_response",
492
- v,
493
- id: message.id,
494
- inputTokens: cu.inputTokens,
495
- outputTokens: cu.outputTokens,
496
- cacheReadTokens: cu.cacheReadTokens,
497
- cacheCreationTokens: cu.cacheCreationTokens,
498
- turns: cu.turns,
499
- ...(cu.fiveHourUtilization !== undefined && { fiveHourUtilization: cu.fiveHourUtilization }),
500
- ...(cu.fiveHourResetsAt !== undefined && { fiveHourResetsAt: cu.fiveHourResetsAt }),
501
- ...(cu.sevenDayUtilization !== undefined && { sevenDayUtilization: cu.sevenDayUtilization }),
502
- ...(cu.sevenDayResetsAt !== undefined && { sevenDayResetsAt: cu.sevenDayResetsAt }),
503
- });
504
- }
505
- catch {
506
- // 書込失敗は握り潰す。
507
- }
508
- break;
509
- }
510
- // 使用量(claude): プラン使用率(OAuth 使用量 API, ベストエフォート)+ tail 中会話の usage 合算。
511
- const transcript = chatTailController?.currentTranscriptPath() ?? null;
512
- const totals = transcript !== null ? aggregateUsage(transcript) : emptyUsageTotals();
513
- const plan = await ctx.planUsage();
514
- try {
515
- writer.write({
516
- type: "usage_response",
517
- v,
518
- id: message.id,
519
- inputTokens: totals.inputTokens,
520
- outputTokens: totals.outputTokens,
521
- cacheReadTokens: totals.cacheReadTokens,
522
- cacheCreationTokens: totals.cacheCreationTokens,
523
- turns: totals.turns,
524
- ...(plan?.fiveHourUtilization != null && { fiveHourUtilization: plan.fiveHourUtilization }),
525
- ...(plan?.fiveHourResetsAt != null && { fiveHourResetsAt: plan.fiveHourResetsAt }),
526
- ...(plan?.sevenDayUtilization != null && { sevenDayUtilization: plan.sevenDayUtilization }),
527
- ...(plan?.sevenDayResetsAt != null && { sevenDayResetsAt: plan.sevenDayResetsAt }),
528
- ...(plan?.sevenDayFableUtilization != null && {
529
- sevenDayFableUtilization: plan.sevenDayFableUtilization,
530
- }),
531
- ...(plan?.sevenDayFableResetsAt != null && {
532
- sevenDayFableResetsAt: plan.sevenDayFableResetsAt,
533
- }),
534
- });
535
- }
536
- catch {
537
- // 書込失敗は握り潰す(Swift 版 try? と同じ)。
538
- }
539
- break;
540
- }
541
- case "question_answer": {
542
- try {
543
- await injectQuestionAnswers(message.answers, message.session, sessionManager);
544
- }
545
- catch (error) {
546
- writeError(writer, v, message.id, "question_answer_failed", String(error));
547
- }
548
- break;
549
- }
550
- case "mode_get": {
551
- // 現在の permission mode を pane 表示から判定して返す(dialog 中は短く再試行)。
552
- try {
553
- const mode = await waitForPermissionMode(sessionManager, message.session, ctx.modeTiming.getAttempts, ctx.modeTiming.getPollMs);
554
- if (mode === null) {
555
- writeError(writer, v, message.id, "mode_unavailable", "ダイアログ表示中のためモードを判定できません");
556
- }
557
- else {
558
- writer.write({ type: "mode_set_response", v, id: message.id, mode });
559
- }
560
- }
561
- catch (error) {
562
- writeError(writer, v, message.id, "mode_get_failed", String(error));
563
- }
564
- break;
565
- }
566
- case "mode_set": {
567
- // mode_set は dialog 待ちが長くなり得るため、read loop を塞がず detached で処理する。
568
- if (state.modeSetInFlight.has(message.session)) {
569
- writeError(writer, v, message.id, "mode_set_busy", "mode_set が実行中です。");
570
- break;
571
- }
572
- state.modeSetInFlight.add(message.session);
573
- void (async () => {
574
- try {
575
- const result = await setPermissionMode(sessionManager, message.session, message.mode, ctx.modeTiming);
576
- if (result.kind === "unavailable") {
577
- writeError(writer, v, message.id, "mode_unavailable", "ダイアログ表示中のためモードを判定できません");
578
- }
579
- else if (result.mode === null) {
580
- writeError(writer, v, message.id, "mode_set_failed", "permission mode の切替に失敗しました。");
581
- }
582
- else {
583
- writer.write({ type: "mode_set_response", v, id: message.id, mode: result.mode });
584
- }
585
- }
586
- catch (error) {
587
- writeError(writer, v, message.id, "mode_set_failed", String(error));
588
- }
589
- finally {
590
- state.modeSetInFlight.delete(message.session);
591
- }
592
- })();
593
- break;
594
- }
595
- case "slash_list_request": {
596
- // セッション/プロジェクト由来の slash command 候補をベストエフォートで収集する。
597
- const commands = collectSlashCommands(ctx.homeDir, message.cwd);
598
- try {
599
- writer.write({ type: "slash_list_response", v, id: message.id, commands });
600
- }
601
- catch (error) {
602
- process.stderr.write(`[tailii-host engine] slash_list_response 書込失敗: ${String(error)}\n`);
603
- }
604
- break;
605
- }
606
- case "image_fetch_request": {
607
- // 原本オンデマンド分割配信。index 逆引きし、分割 image_fetch_response を順に書く。
608
- if (ctx.imageService === null) {
609
- // 未注入時は画像を扱わない(後方互換)。要求元が沈黙で待たないよう error を返す。
610
- writeError(writer, v, message.id, "image_not_found", "画像機能は無効です。");
611
- break;
612
- }
613
- for (const response of ctx.imageService.fetch(message.id)) {
614
- try {
615
- writer.write(response);
616
- }
617
- catch (error) {
618
- process.stderr.write(`[tailii-host engine] image_fetch_response 書込失敗: ${String(error)}\n`);
619
- break;
620
- }
621
- }
622
- break;
623
- }
624
- case "dir_list_request": {
625
- // ディレクトリ候補問い合わせ。base 外・不正・一致なしは空 entries(エラーにしない)。
626
- const entries = dirList(message.baseDir, message.partial);
627
- try {
628
- writer.write({ type: "dir_list_response", v, id: message.id, entries });
629
- }
630
- catch (error) {
631
- process.stderr.write(`[tailii-host engine] dir_list_response 書込失敗: ${String(error)}\n`);
632
- }
633
- break;
634
- }
635
- case "browse_request": {
636
- // ディレクトリ非限定ブラウズ。不存在・読取不能は空 entries(エラーにしない)。
637
- ChatTailController.diag(`browse_request id=${message.id} path=${message.path}`);
638
- const entries = dirChildren(message.path);
639
- try {
640
- writer.write({ type: "browse_response", v, id: message.id, path: message.path, entries });
641
- ChatTailController.diag(`browse_response id=${message.id} entries=${entries.length}`);
642
- }
643
- catch (error) {
644
- ChatTailController.diag(`browse_response 書込失敗 id=${message.id}: ${String(error)}`);
645
- process.stderr.write(`[tailii-host engine] browse_response 書込失敗: ${String(error)}\n`);
646
- }
647
- break;
648
- }
649
- case "claude_session_list_request": {
650
- // 会話一覧: claude(jsonl) + codex(rollout) をマージし updatedAt 降順で返す(agent-tag)。
651
- // store 未注入時はその分を空一覧扱い(後方互換 — 会話機能なし)。
652
- ChatTailController.diag(`claude_session_list_request id=${message.id}`);
653
- const claudeSessions = ctx.claudeSessionStore?.list() ?? [];
654
- const codexSessions = ctx.codexSessionStore?.list() ?? [];
655
- const sessions = [...claudeSessions, ...codexSessions].sort((lhs, rhs) => {
656
- const l = lhs.updatedAt ?? Number.MIN_SAFE_INTEGER;
657
- const r = rhs.updatedAt ?? Number.MIN_SAFE_INTEGER;
658
- if (l !== r)
659
- return r - l;
660
- return lhs.sessionId < rhs.sessionId ? -1 : lhs.sessionId > rhs.sessionId ? 1 : 0;
661
- });
662
- ChatTailController.diag(`claude_session_list_response id=${message.id} count=${sessions.length}`);
663
- try {
664
- writer.write({
665
- type: "claude_session_list_response", v, id: message.id, claudeSessions: sessions,
666
- });
667
- }
668
- catch (error) {
669
- process.stderr.write(`[tailii-host engine] claude_session_list_response 書込失敗: ${String(error)}\n`);
670
- }
671
- break;
672
- }
673
- case "session_search_request": {
674
- // Claude 会話本文検索。長時間 read loop を塞がないよう検索関数側で件数/時間/読取量を制限する。
675
- ChatTailController.diag(`session_search_request id=${message.id} query=${message.query}`);
676
- const response = ctx.claudeSessionStore !== null
677
- ? searchClaudeSessions(ctx.claudeSessionStore, message.query, { limit: message.limit })
678
- : { results: [], stats: { scannedFiles: 0, truncated: false } };
679
- ChatTailController.diag(`session_search_response id=${message.id} count=${response.results.length} scanned=${response.stats.scannedFiles} truncated=${response.stats.truncated}`);
680
- try {
681
- writer.write({ type: "session_search_response", v, id: message.id, results: response.results });
682
- }
683
- catch (error) {
684
- process.stderr.write(`[tailii-host engine] session_search_response 書込失敗: ${String(error)}\n`);
685
- }
686
- break;
687
- }
688
- case "dir_create_request": {
689
- // ディレクトリ作成。base 外/`..` 脱出は ok=false。
690
- const result = dirCreate(message.baseDir, message.relative);
691
- try {
692
- writer.write({
693
- type: "dir_create_response", v, id: message.id, path: result.path, ok: result.ok,
694
- });
695
- }
696
- catch (error) {
697
- process.stderr.write(`[tailii-host engine] dir_create_response 書込失敗: ${String(error)}\n`);
698
- }
699
- break;
700
- }
701
- default:
702
- // 承認2型・画像・chat_output などは engine 制御では処理しない(本タスク範囲外)。破棄。
703
- break;
704
- }
705
- return true;
706
- }
707
- /**
708
- * multiSelect のカーソル移動・トグル・入力の各キー送出の間に挟むウェイト(ms)。
709
- * 連続キーを一気に送ると TUI(Ink 再描画)が取りこぼすため、1 キーごとに待つ。
710
- */
711
- const KEY_STEP_MS = 150;
712
- /**
713
- * iOS の選択結果を Claude Code TUI の AskUserQuestion ダイアログへ送る。
714
- * 実 TUI の操作体系(2026-07-07 に tmux + 実 claude で再検証済み):
715
- * - 単一選択 = 数字キー(1-based)で即確定(Enter は送らない — 誤爆防止)。
716
- * - 単一選択の Other = 数字キーで行がインライン入力欄化 → literal 入力 → Enter。空なら押さない。
717
- * - multiSelect = 数字キーは無反応。カーソル(先頭 index 0 から開始)を ↓ で対象行へ移動し
718
- * Space でトグル → Right でレビュー → 最終問後に「1」Submit answers。
719
- * Other 行は Space でチェック&入力欄化 → literal 入力 → ↑ でテキスト欄を抜けてから Right。
720
- * - Other の synthetic index(= options.count)は選択肢中で最大の index になるため、
721
- * otherText があるときは最大 index を Other 行とみなす。
722
- */
723
- async function injectQuestionAnswers(answers, session, sessionManager) {
724
- const sorted = answers.slice().sort((a, b) => a.questionIndex - b.questionIndex);
725
- let needsReviewSubmit = false;
726
- for (let position = 0; position < sorted.length; position += 1) {
727
- const answer = sorted[position];
728
- const isLast = position === sorted.length - 1;
729
- const other = (answer.otherText ?? "").trim();
730
- let indexes = answer.selectedOptionIndexes.filter((i) => i >= 0).sort((a, b) => a - b);
731
- // otherText があるとき、最大 index は Other(Type something.)行。
732
- let otherIndex = null;
733
- if (other.length > 0 && indexes.length > 0) {
734
- otherIndex = indexes[indexes.length - 1];
735
- indexes = indexes.slice(0, -1);
736
- }
737
- if (answer.multiSelect) {
738
- // multiSelect は数字キーではトグルできない(実 TUI)。カーソルを ↓ で移動し Space でトグルする。
739
- // カーソルは先頭(index 0)から開始。indexes / otherIndex は昇順なので下方向のみで足りる。
740
- let cursor = 0;
741
- const moveTo = async (target) => {
742
- for (let n = cursor; n < target; n += 1) {
743
- await sessionManager.sendKeys(session, ["Down"]);
744
- await sleep(KEY_STEP_MS);
745
- }
746
- cursor = target;
747
- };
748
- for (const idx of indexes) {
749
- await moveTo(idx);
750
- await sessionManager.sendKeys(session, ["Space"]);
751
- await sleep(KEY_STEP_MS);
752
- }
753
- if (otherIndex !== null) {
754
- await moveTo(otherIndex);
755
- // Other 行を Space でチェックすると同時に入力欄がフォーカスされる。
756
- await sessionManager.sendKeys(session, ["Space"]);
757
- await sleep(KEY_STEP_MS);
758
- await sessionManager.sendKeys(session, [other], true);
759
- await sleep(KEY_STEP_MS);
760
- // テキスト欄にいると Right がタブ移動に効かないため、↑ で通常行へ退避してから進む。
761
- await sessionManager.sendKeys(session, ["Up"]);
762
- await sleep(KEY_STEP_MS);
763
- cursor = otherIndex - 1;
764
- }
765
- // レビュー(最終問)または次の質問タブへ進む。
766
- await sessionManager.sendKeys(session, ["Right"]);
767
- if (isLast)
768
- needsReviewSubmit = true;
769
- }
770
- else if (otherIndex !== null) {
771
- await sessionManager.sendKeys(session, [String(otherIndex + 1)]);
772
- await sessionManager.sendKeys(session, [other], true);
773
- await sessionManager.sendKeys(session, ["Enter"]);
774
- }
775
- else if (indexes.length > 0) {
776
- // 数字キーで即確定(Enter は送らない)。
777
- await sessionManager.sendKeys(session, [String(indexes[0] + 1)]);
778
- }
779
- // TUI の再描画/タブ送りを待つ(連続注入の取りこぼし防止)。
780
- await sleep(200);
781
- }
782
- if (needsReviewSubmit) {
783
- // レビュー画面「Ready to submit your answers?」で 1. Submit answers を確定する。
784
- await sessionManager.sendKeys(session, ["1"]);
785
- }
786
- }
787
- /** pane から mode が判定できるまで、指定回数だけ短く待つ。 */
788
- async function waitForPermissionMode(sessionManager, session, attempts, intervalMs) {
789
- for (let i = 0; i < attempts; i += 1) {
790
- const mode = parsePermissionMode(await sessionManager.capturePane(session));
791
- if (mode !== null)
792
- return mode;
793
- if (i < attempts - 1)
794
- await sleep(intervalMs);
795
- }
796
- return null;
797
- }
798
- /** mode_set の実体。dialog が閉じるのを待ち、BTab 後は実際に変化した mode だけを採用する。 */
799
- async function setPermissionMode(sessionManager, session, target, timing) {
800
- let current = null;
801
- const initialDeadline = Date.now() + timing.setInitialTimeoutMs;
802
- while (Date.now() <= initialDeadline) {
803
- current = parsePermissionMode(await sessionManager.capturePane(session));
804
- if (current !== null)
805
- break;
806
- await sleep(timing.setInitialPollMs);
807
- }
808
- if (current === null)
809
- return { kind: "unavailable" };
810
- if (current === target)
811
- return { kind: "ok", mode: target };
812
- for (let i = 0; i < 4 && current !== target; i += 1) {
813
- const before = current;
814
- await sessionManager.sendKeys(session, ["BTab"]);
815
- const changeDeadline = Date.now() + timing.setChangeTimeoutMs;
816
- let changed = false;
817
- while (Date.now() <= changeDeadline) {
818
- const next = parsePermissionMode(await sessionManager.capturePane(session));
819
- if (next === null) {
820
- current = null;
821
- break;
822
- }
823
- if (next !== before) {
824
- current = next;
825
- changed = true;
826
- break;
827
- }
828
- await sleep(timing.setChangePollMs);
829
- }
830
- if (current === null || !changed)
831
- break;
832
- }
833
- return { kind: "ok", mode: current };
834
- }
835
- /** slash_list_request 用に、ユーザー/プロジェクトの skills と commands を収集する。 */
836
- function collectSlashCommands(homeDir, cwd) {
837
- const byName = new Map();
838
- const userClaude = path.join(homeDir, ".claude");
839
- const projectClaude = cwd === undefined ? null : path.join(cwd, ".claude");
840
- scanSkillCommands(path.join(userClaude, "skills"), 2, byName);
841
- if (projectClaude !== null)
842
- scanSkillCommands(path.join(projectClaude, "skills"), 4, byName);
843
- scanMarkdownCommands(path.join(userClaude, "commands"), 1, byName);
844
- if (projectClaude !== null)
845
- scanMarkdownCommands(path.join(projectClaude, "commands"), 3, byName);
846
- return [...byName.values()]
847
- .map((entry) => entry.command)
848
- .sort((a, b) => a.name.localeCompare(b.name))
849
- .slice(0, 200);
850
- }
851
- /** ~/.claude/skills/<name>/SKILL.md 形式のコマンド候補を読む。 */
852
- function scanSkillCommands(root, priority, byName) {
853
- let entries;
854
- try {
855
- entries = fs.readdirSync(root, { withFileTypes: true });
856
- }
857
- catch {
858
- return;
859
- }
860
- for (const entry of entries) {
861
- const skillDir = path.join(root, entry.name);
862
- let isDirectory = entry.isDirectory();
863
- if (!isDirectory && entry.isSymbolicLink()) {
864
- try {
865
- isDirectory = fs.statSync(skillDir).isDirectory();
866
- }
867
- catch {
868
- isDirectory = false;
869
- }
870
- }
871
- if (!isDirectory)
872
- continue;
873
- addSlashCandidate(byName, `/${entry.name}`, path.join(skillDir, "SKILL.md"), priority);
874
- }
875
- }
876
- /** ~/.claude/commands/<name>.md 形式のコマンド候補を読む。 */
877
- function scanMarkdownCommands(root, priority, byName) {
878
- let entries;
879
- try {
880
- entries = fs.readdirSync(root, { withFileTypes: true });
881
- }
882
- catch {
883
- return;
884
- }
885
- for (const entry of entries) {
886
- if (!entry.name.endsWith(".md"))
887
- continue;
888
- const filePath = path.join(root, entry.name);
889
- let isFile = entry.isFile();
890
- if (!isFile && entry.isSymbolicLink()) {
891
- try {
892
- isFile = fs.statSync(filePath).isFile();
893
- }
894
- catch {
895
- isFile = false;
896
- }
897
- }
898
- if (!isFile)
899
- continue;
900
- addSlashCandidate(byName, `/${entry.name.slice(0, -3)}`, filePath, priority);
901
- }
902
- }
903
- /** 既存候補より優先度が高いときだけ登録する(project 優先、同 scope では skills 優先)。 */
904
- function addSlashCandidate(byName, name, filePath, priority) {
905
- const existing = byName.get(name);
906
- if (existing !== undefined && existing.priority >= priority)
907
- return;
908
- byName.set(name, { priority, command: { name, summary: readMarkdownSummary(filePath) } });
909
- }
910
- /** YAML frontmatter の description 1 行だけを素朴に抜き出す。 */
911
- function readMarkdownSummary(filePath) {
912
- let text;
913
- try {
914
- text = fs.readFileSync(filePath, "utf8");
915
- }
916
- catch {
917
- return "";
918
- }
919
- const lines = text.split(/\r?\n/);
920
- if (lines[0] !== "---")
921
- return "";
922
- for (let i = 1; i < lines.length; i += 1) {
923
- const line = lines[i];
924
- if (line === "---")
925
- break;
926
- const match = /^description:\s*(.*)$/.exec(line);
927
- if (match !== null) {
928
- return stripYamlQuotes(match[1] ?? "").slice(0, 120);
929
- }
930
- }
931
- return "";
932
- }
933
- /** description: "..." / '...' の外側だけ外す。 */
934
- function stripYamlQuotes(value) {
935
- const trimmed = value.trim();
936
- if (trimmed.length >= 2 &&
937
- ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
938
- (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
939
- return trimmed.slice(1, -1);
940
- }
941
- return trimmed;
942
- }
943
- /** session_list_response を書き出す小ヘルパー(nextCursor は null なら省略)。 */
944
- function writeSessionListResponse(writer, v, id, sessions, nextCursor) {
945
- writer.write({
946
- type: "session_list_response",
947
- v,
948
- id,
949
- sessions,
950
- ...(nextCursor !== null && { nextCursor }),
951
- });
952
- }
953
- /** error 封筒を書き出す小ヘルパー。 */
954
- function writeError(writer, v, id, code, message) {
955
- try {
956
- writer.write({ type: "error", v, ...(id !== undefined && { id }), code, message });
957
- }
958
- catch (error) {
959
- process.stderr.write(`[tailii-host engine] error 送出失敗: ${String(error)}\n`);
960
- }
961
- }
962
- //# sourceMappingURL=engine.js.map