handmux 0.23.0 → 0.25.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 (327) hide show
  1. package/README.md +31 -8
  2. package/README.zh-CN.md +31 -8
  3. package/bin/handmux.js +14 -688
  4. package/bin/node-version.js +20 -0
  5. package/dist/bin/handmux.js +1015 -0
  6. package/dist/connectors/bridgeClient.js +385 -0
  7. package/dist/connectors/claude/index.js +476 -0
  8. package/dist/connectors/pi/bridgeClient.js +8 -0
  9. package/dist/connectors/pi/index.js +851 -0
  10. package/dist/hooks/handmux-notify.sh +64 -0
  11. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  12. package/dist/hooks/handmux-write.cjs +264 -0
  13. package/dist/package.json +127 -0
  14. package/dist/public/assets/index-CTrqTqtT.css +32 -0
  15. package/dist/public/assets/index-DmjeMSQA.js +346 -0
  16. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  17. package/{public → dist/public}/index.html +5 -4
  18. package/dist/src/agent-runtime/adapter.js +204 -0
  19. package/dist/src/agent-runtime/bridge.js +1006 -0
  20. package/dist/src/agent-runtime/bridgeStore.js +25 -0
  21. package/dist/src/agent-runtime/bridgeTransport.js +619 -0
  22. package/dist/src/agent-runtime/bridgeTypes.js +13 -0
  23. package/dist/src/agent-runtime/builtinRuntime.js +193 -0
  24. package/dist/src/agent-runtime/conversation.js +2179 -0
  25. package/dist/src/agent-runtime/conversationActivation.js +75 -0
  26. package/dist/src/agent-runtime/conversationActivity.js +69 -0
  27. package/dist/src/agent-runtime/conversationControls.js +206 -0
  28. package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
  29. package/dist/src/agent-runtime/conversationStore.js +352 -0
  30. package/dist/src/agent-runtime/conversationTypes.js +1 -0
  31. package/dist/src/agent-runtime/conversationValidation.js +339 -0
  32. package/dist/src/agent-runtime/inbox.js +973 -0
  33. package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
  34. package/dist/src/agent-runtime/inboxStore.js +25 -0
  35. package/dist/src/agent-runtime/inboxTypes.js +1 -0
  36. package/dist/src/agent-runtime/interaction.js +829 -0
  37. package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
  38. package/dist/src/agent-runtime/interactionStore.js +18 -0
  39. package/dist/src/agent-runtime/interactionTypes.js +1 -0
  40. package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
  41. package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
  42. package/dist/src/agent-runtime/resourceTypes.js +1 -0
  43. package/dist/src/agent-runtime/resources.js +245 -0
  44. package/dist/src/agent-runtime/run.js +328 -0
  45. package/dist/src/agent-runtime/runtime.js +793 -0
  46. package/dist/src/agent-runtime/sessionControl.js +164 -0
  47. package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
  48. package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
  49. package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
  50. package/dist/src/agents/claude.js +203 -0
  51. package/dist/src/agents/claudeConversation.js +362 -0
  52. package/dist/src/agents/claudeInteraction.js +134 -0
  53. package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
  54. package/dist/src/agents/codex.js +262 -0
  55. package/dist/src/agents/codexConversation.js +1103 -0
  56. package/dist/src/agents/codexConversationActivation.js +121 -0
  57. package/dist/src/agents/codexConversationControls.js +256 -0
  58. package/dist/src/agents/codexInteraction.js +306 -0
  59. package/dist/src/agents/codexSessionControl.js +118 -0
  60. package/dist/src/agents/codexSubscriptionUsage.js +313 -0
  61. package/dist/src/agents/codexTerminalControl.js +23 -0
  62. package/dist/src/agents/conversationProjectionSafety.js +416 -0
  63. package/dist/src/agents/index.js +39 -0
  64. package/dist/src/agents/nativeInbox.js +264 -0
  65. package/dist/src/agents/nativeInboxSources.js +115 -0
  66. package/dist/src/agents/pi.js +28 -0
  67. package/dist/src/agents/piConversation.js +523 -0
  68. package/dist/src/agents/piConversationHistory.js +502 -0
  69. package/dist/src/agents/piInboxBridge.js +368 -0
  70. package/dist/src/agents/piSessionControl.js +32 -0
  71. package/dist/src/agents/processIdentity.js +94 -0
  72. package/dist/src/agents/scanUtils.js +262 -0
  73. package/dist/src/apiAccountProviders.js +194 -0
  74. package/dist/src/apiAccounts.js +592 -0
  75. package/dist/src/apiErrors.js +86 -0
  76. package/{src → dist/src}/appName.js +12 -13
  77. package/dist/src/asr/iflyConfig.js +7 -0
  78. package/dist/src/asr/iflySign.js +14 -0
  79. package/dist/src/auth.js +27 -0
  80. package/dist/src/browser/bootstrap.js +72 -0
  81. package/dist/src/browser/cookieProfiles.js +413 -0
  82. package/dist/src/browser/coordinator.js +51 -0
  83. package/dist/src/browser/hammerheadCompat.js +104 -0
  84. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  85. package/dist/src/browser/manager.js +560 -0
  86. package/dist/src/browser/originLabel.js +15 -0
  87. package/dist/src/browser/profilePersistence.js +235 -0
  88. package/dist/src/browser/publicProxy.js +176 -0
  89. package/dist/src/browser/routes.js +159 -0
  90. package/dist/src/browser/sessionStore.js +72 -0
  91. package/dist/src/browser/siteVersion.js +121 -0
  92. package/dist/src/browser/targetPolicy.js +163 -0
  93. package/{src → dist/src}/browser/worker.js +14 -18
  94. package/dist/src/browser/workerClient.js +286 -0
  95. package/dist/src/browser/workerServer.js +91 -0
  96. package/dist/src/captureBackground.js +95 -0
  97. package/dist/src/claudeEvents.js +525 -0
  98. package/dist/src/cli/agentIntegration.js +131 -0
  99. package/dist/src/cli/agentLauncher.js +26 -0
  100. package/dist/src/cli/claudeHooks.js +290 -0
  101. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  102. package/dist/src/cli/cloudflared.js +106 -0
  103. package/dist/src/cli/codexManaged.js +80 -0
  104. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  105. package/dist/src/cli/drivers.js +98 -0
  106. package/dist/src/cli/hookScaffold.js +39 -0
  107. package/dist/src/cli/i18n/en.js +392 -0
  108. package/{src → dist/src}/cli/i18n/index.js +27 -25
  109. package/dist/src/cli/i18n/zh.js +391 -0
  110. package/dist/src/cli/legacyCodexHooks.js +52 -0
  111. package/{src → dist/src}/cli/natappUrl.js +2 -3
  112. package/dist/src/cli/options.js +245 -0
  113. package/dist/src/cli/piExtension.js +155 -0
  114. package/dist/src/cli/probe.js +16 -0
  115. package/{src → dist/src}/cli/prompt.js +4 -6
  116. package/dist/src/cli/pushCmd.js +102 -0
  117. package/{src → dist/src}/cli/qr.js +19 -19
  118. package/dist/src/cli/service.js +154 -0
  119. package/dist/src/cli/setupModel.js +194 -0
  120. package/dist/src/cli/setupWizard.js +647 -0
  121. package/dist/src/cli/shortcutEditor.js +351 -0
  122. package/dist/src/cli/sshTunnel.js +18 -0
  123. package/dist/src/cli/state.js +117 -0
  124. package/dist/src/cli/statusLine.js +115 -0
  125. package/dist/src/cli/supervisor.js +278 -0
  126. package/dist/src/cli/supervisorHealth.js +21 -0
  127. package/dist/src/cli/supervisorLaunch.js +148 -0
  128. package/dist/src/cli/supervisorProcesses.js +62 -0
  129. package/dist/src/cli/supervisorState.js +62 -0
  130. package/dist/src/cli/tmuxVersion.js +53 -0
  131. package/{src → dist/src}/cli/tunlite.js +7 -10
  132. package/dist/src/cli/tunnelClients.js +72 -0
  133. package/{src → dist/src}/cli/updateCheck.js +98 -69
  134. package/{src → dist/src}/cli/urlHost.js +8 -4
  135. package/dist/src/cli/workspaceCmd.js +347 -0
  136. package/dist/src/codexAppServer.js +3436 -0
  137. package/dist/src/codexConversationProjection.js +199 -0
  138. package/dist/src/codexDiff.js +118 -0
  139. package/dist/src/codexMessageIdentity.js +18 -0
  140. package/dist/src/codexPlan.js +30 -0
  141. package/dist/src/codexQueueProtocol.js +137 -0
  142. package/dist/src/codexStreamProtocol.js +207 -0
  143. package/dist/src/codexToolProtocol.js +68 -0
  144. package/dist/src/codexTranscriptParse.js +835 -0
  145. package/dist/src/codexUsageSnapshot.js +228 -0
  146. package/dist/src/config.js +8 -0
  147. package/{src → dist/src}/docPath.js +22 -24
  148. package/dist/src/docs.js +337 -0
  149. package/dist/src/git.js +284 -0
  150. package/dist/src/healthProtocol.js +97 -0
  151. package/dist/src/httpApi.js +96 -0
  152. package/dist/src/jsonStore.js +15 -0
  153. package/{src → dist/src}/keyNames.js +3 -3
  154. package/dist/src/notifications.js +111 -0
  155. package/dist/src/orphans.js +167 -0
  156. package/dist/src/paneInput.js +42 -0
  157. package/dist/src/pendingPrompt.js +194 -0
  158. package/dist/src/previewServer.js +202 -0
  159. package/dist/src/previews.js +179 -0
  160. package/dist/src/privateStateStore.js +124 -0
  161. package/dist/src/projectTask/backup.js +224 -0
  162. package/dist/src/projectTask/lock.js +108 -0
  163. package/dist/src/projectTask/migrations.js +90 -0
  164. package/dist/src/projectTask/routes.js +155 -0
  165. package/dist/src/projectTask/runtime.js +182 -0
  166. package/dist/src/projectTask/schema.js +56 -0
  167. package/dist/src/projectTask/store.js +457 -0
  168. package/dist/src/push.js +224 -0
  169. package/dist/src/routes/agents.js +631 -0
  170. package/dist/src/routes/apiAccounts.js +82 -0
  171. package/dist/src/routes/files.js +230 -0
  172. package/dist/src/routes/git.js +89 -0
  173. package/dist/src/routes/health.js +16 -0
  174. package/dist/src/routes/notifications.js +16 -0
  175. package/dist/src/routes/previews.js +43 -0
  176. package/dist/src/routes/push.js +145 -0
  177. package/dist/src/routes/sessions.js +244 -0
  178. package/dist/src/routes/system.js +220 -0
  179. package/dist/src/routes/terminal.js +244 -0
  180. package/dist/src/routes/workspace.js +267 -0
  181. package/dist/src/server.js +315 -0
  182. package/dist/src/shortcutConfig.js +83 -0
  183. package/{src → dist/src}/staticCache.js +5 -3
  184. package/{src → dist/src}/staticCompression.js +5 -6
  185. package/dist/src/terminalStream.js +441 -0
  186. package/{src → dist/src}/tmux/commands.js +185 -172
  187. package/dist/src/tmux/format.js +47 -0
  188. package/dist/src/transcriptParse.js +261 -0
  189. package/dist/src/transcriptReader.js +209 -0
  190. package/{src → dist/src}/trimCapture.js +11 -11
  191. package/dist/src/uploadTypes.js +31 -0
  192. package/dist/src/urlPolicy.js +18 -0
  193. package/dist/src/usagePaths.js +15 -0
  194. package/dist/src/workspace/agentRunner.js +301 -0
  195. package/dist/src/workspace/atomicJson.js +31 -0
  196. package/dist/src/workspace/capture.js +139 -0
  197. package/dist/src/workspace/checkpointer.js +228 -0
  198. package/dist/src/workspace/environment.js +71 -0
  199. package/dist/src/workspace/lock.js +137 -0
  200. package/dist/src/workspace/mapping.js +128 -0
  201. package/dist/src/workspace/operations.js +271 -0
  202. package/dist/src/workspace/paths.js +15 -0
  203. package/dist/src/workspace/planner.js +292 -0
  204. package/dist/src/workspace/restore.js +363 -0
  205. package/dist/src/workspace/runtime.js +318 -0
  206. package/dist/src/workspace/schema.js +156 -0
  207. package/dist/src/workspace/store.js +464 -0
  208. package/dist/src/workspace/tmuxAdapter.js +704 -0
  209. package/dist/src/workspaceProtocol.js +158 -0
  210. package/package.json +30 -17
  211. package/hooks/handmux-notify.sh +0 -22
  212. package/hooks/handmux-write.cjs +0 -78
  213. package/public/assets/index-C3N7OkyP.css +0 -32
  214. package/public/assets/index-D1DWcC5u.js +0 -337
  215. package/src/.gitkeep +0 -0
  216. package/src/agents/claude.js +0 -159
  217. package/src/agents/codex.js +0 -168
  218. package/src/agents/index.js +0 -21
  219. package/src/agents/processIdentity.js +0 -72
  220. package/src/agents/scanUtils.js +0 -225
  221. package/src/asr/iflyConfig.js +0 -10
  222. package/src/asr/iflySign.js +0 -16
  223. package/src/auth.js +0 -30
  224. package/src/browser/bootstrap.js +0 -68
  225. package/src/browser/cookieProfiles.js +0 -403
  226. package/src/browser/coordinator.js +0 -47
  227. package/src/browser/hammerheadCompat.js +0 -86
  228. package/src/browser/hammerheadRedirectCompat.js +0 -38
  229. package/src/browser/manager.js +0 -556
  230. package/src/browser/originLabel.js +0 -17
  231. package/src/browser/profilePersistence.js +0 -241
  232. package/src/browser/publicProxy.js +0 -175
  233. package/src/browser/routes.js +0 -154
  234. package/src/browser/sessionStore.js +0 -78
  235. package/src/browser/siteVersion.js +0 -124
  236. package/src/browser/targetPolicy.js +0 -137
  237. package/src/browser/workerClient.js +0 -253
  238. package/src/browser/workerServer.js +0 -98
  239. package/src/captureBackground.js +0 -93
  240. package/src/claudeEvents.js +0 -399
  241. package/src/cli/claudeHooks.js +0 -206
  242. package/src/cli/cloudflared.js +0 -99
  243. package/src/cli/codexManaged.js +0 -77
  244. package/src/cli/drivers.js +0 -97
  245. package/src/cli/hookScaffold.js +0 -33
  246. package/src/cli/i18n/en.js +0 -385
  247. package/src/cli/i18n/zh.js +0 -384
  248. package/src/cli/legacyCodexHooks.js +0 -49
  249. package/src/cli/options.js +0 -211
  250. package/src/cli/probe.js +0 -16
  251. package/src/cli/pushCmd.js +0 -60
  252. package/src/cli/service.js +0 -126
  253. package/src/cli/setupModel.js +0 -154
  254. package/src/cli/setupWizard.js +0 -506
  255. package/src/cli/shortcutEditor.js +0 -288
  256. package/src/cli/sshTunnel.js +0 -12
  257. package/src/cli/state.js +0 -90
  258. package/src/cli/statusLine.js +0 -90
  259. package/src/cli/supervisor.js +0 -183
  260. package/src/cli/supervisorProcesses.js +0 -57
  261. package/src/cli/tmuxVersion.js +0 -49
  262. package/src/cli/tunnelClients.js +0 -62
  263. package/src/cli/workspaceCmd.js +0 -300
  264. package/src/codexAppServer.js +0 -1620
  265. package/src/codexDiff.js +0 -108
  266. package/src/codexTranscriptParse.js +0 -413
  267. package/src/codexUsageSnapshot.cjs +0 -129
  268. package/src/config.js +0 -9
  269. package/src/docs.js +0 -228
  270. package/src/git.js +0 -208
  271. package/src/httpApi.js +0 -72
  272. package/src/jsonStore.js +0 -24
  273. package/src/notifications.js +0 -68
  274. package/src/orphans.js +0 -152
  275. package/src/pendingPrompt.js +0 -163
  276. package/src/previewServer.js +0 -168
  277. package/src/previews.js +0 -116
  278. package/src/push.js +0 -183
  279. package/src/routes/codex.js +0 -473
  280. package/src/routes/files.js +0 -172
  281. package/src/routes/git.js +0 -57
  282. package/src/routes/notifications.js +0 -19
  283. package/src/routes/previews.js +0 -35
  284. package/src/routes/push.js +0 -119
  285. package/src/routes/sessions.js +0 -168
  286. package/src/routes/system.js +0 -132
  287. package/src/routes/terminal.js +0 -210
  288. package/src/routes/transcript.js +0 -171
  289. package/src/routes/workspace.js +0 -267
  290. package/src/server.js +0 -186
  291. package/src/shortcutConfig.js +0 -75
  292. package/src/terminalStream.js +0 -391
  293. package/src/tmux/format.js +0 -45
  294. package/src/transcriptParse.js +0 -180
  295. package/src/transcriptReader.js +0 -101
  296. package/src/uploadTypes.js +0 -30
  297. package/src/urlPolicy.js +0 -14
  298. package/src/usage.js +0 -260
  299. package/src/workspace/agentRunner.js +0 -188
  300. package/src/workspace/atomicJson.js +0 -32
  301. package/src/workspace/capture.js +0 -139
  302. package/src/workspace/checkpointer.js +0 -187
  303. package/src/workspace/environment.js +0 -75
  304. package/src/workspace/lock.js +0 -128
  305. package/src/workspace/mapping.js +0 -124
  306. package/src/workspace/operations.js +0 -252
  307. package/src/workspace/paths.js +0 -16
  308. package/src/workspace/planner.js +0 -247
  309. package/src/workspace/restore.js +0 -279
  310. package/src/workspace/runtime.js +0 -314
  311. package/src/workspace/schema.js +0 -136
  312. package/src/workspace/store.js +0 -382
  313. package/src/workspace/tmuxAdapter.js +0 -554
  314. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  315. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  316. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  317. /package/{public → dist/public}/icons/badge-96.png +0 -0
  318. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  319. /package/{public → dist/public}/icons/icon-192.png +0 -0
  320. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  321. /package/{public → dist/public}/icons/icon-512.png +0 -0
  322. /package/{public → dist/public}/icons/logo.svg +0 -0
  323. /package/{public → dist/public}/manifest.webmanifest +0 -0
  324. /package/{public → dist/public}/offline.html +0 -0
  325. /package/{public → dist/public}/sw.js +0 -0
  326. /package/{src → dist/src}/browser/protocol.js +0 -0
  327. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -0,0 +1,476 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { classifyClaude } from '../../src/agents/claude.js';
5
+ import { LocalConnectorBridgeClient } from '../bridgeClient.js';
6
+ const EVENT_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
7
+ function isRecord(value) {
8
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ }
10
+ function bounded(value, max) {
11
+ return typeof value === 'string' && value.length > 0 && value.length <= max;
12
+ }
13
+ function parseProcessFingerprint(value) {
14
+ if (!isRecord(value) || !Number.isSafeInteger(value.pid) || Number(value.pid) <= 0
15
+ || typeof value.startedAt !== 'number' || !Number.isFinite(value.startedAt)
16
+ || value.startedAt < 0 || !bounded(value.tty, 1024))
17
+ return null;
18
+ return {
19
+ pid: Number(value.pid),
20
+ startedAt: value.startedAt,
21
+ tty: value.tty,
22
+ };
23
+ }
24
+ function normalizedTty(value) {
25
+ if (!value)
26
+ return undefined;
27
+ return value.startsWith('/dev/') ? value : `/dev/${value}`;
28
+ }
29
+ function matchesSourceProcess(source, current) {
30
+ if (!source)
31
+ return true; // additive field: accept records produced by an older installed Hook
32
+ return source.pid === current.pid
33
+ && source.startedAt === current.startedAt
34
+ && normalizedTty(source.tty) === normalizedTty(current.tty);
35
+ }
36
+ function optionalSession(payload, explicit) {
37
+ if (bounded(explicit, 1024))
38
+ return explicit;
39
+ return bounded(payload.session_id, 1024) ? payload.session_id : undefined;
40
+ }
41
+ function message(value, max = 4096) {
42
+ if (typeof value !== 'string')
43
+ return undefined;
44
+ const normalized = value.trim();
45
+ if (!normalized)
46
+ return undefined;
47
+ return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}…`;
48
+ }
49
+ function canonicalState(kind) {
50
+ if (kind === 'working' || kind === 'compacting')
51
+ return 'working';
52
+ if (kind === 'permission')
53
+ return 'waiting';
54
+ if (kind === 'done')
55
+ return 'done';
56
+ if (kind === 'error')
57
+ return 'error';
58
+ return null;
59
+ }
60
+ export function projectClaudeHookInbox({ eventId, src, sourceOccurredAt, payload, }) {
61
+ const classified = classifyClaude(src, payload);
62
+ const state = classified ? canonicalState(classified.kind) : null;
63
+ const safeEventId = eventId && EVENT_ID_RE.test(eventId) ? eventId : undefined;
64
+ const occurred = typeof sourceOccurredAt === 'number' && Number.isFinite(sourceOccurredAt)
65
+ ? sourceOccurredAt : undefined;
66
+ const base = {
67
+ ...(safeEventId === undefined ? {} : { eventId: safeEventId }),
68
+ ...(occurred === undefined ? {} : { sourceOccurredAt: occurred }),
69
+ };
70
+ if (!state) {
71
+ return {
72
+ operation: { kind: 'clear', ...base },
73
+ snapshot: { availability: 'ready' },
74
+ };
75
+ }
76
+ const text = message(classified?.msg);
77
+ const item = {
78
+ state,
79
+ ...(text === undefined ? {} : { message: text }),
80
+ ...((state === 'waiting' || state === 'done' || state === 'error') && safeEventId
81
+ ? { eventId: safeEventId } : {}),
82
+ ...(occurred === undefined ? {} : { sourceOccurredAt: occurred }),
83
+ };
84
+ return {
85
+ operation: {
86
+ kind: 'set',
87
+ ...item,
88
+ ...(state === 'working' && safeEventId ? { eventId: safeEventId } : {}),
89
+ },
90
+ snapshot: { availability: 'ready', current: item },
91
+ };
92
+ }
93
+ function parseEvent(value) {
94
+ if (!isRecord(value) || value.version !== 1 || (value.type !== 'event' && value.type !== 'gap')
95
+ || (value.agent !== undefined && value.agent !== 'claude')
96
+ || !bounded(value.eventId, 256) || !EVENT_ID_RE.test(value.eventId)
97
+ || !bounded(value.paneId, 256)
98
+ || (value.sequence !== undefined && (!Number.isSafeInteger(value.sequence) || Number(value.sequence) <= 0))
99
+ || (value.src !== undefined && !bounded(value.src, 64))
100
+ || (value.sessionId !== undefined && !bounded(value.sessionId, 1024))
101
+ || (value.sourceOccurredAt !== undefined
102
+ && (typeof value.sourceOccurredAt !== 'number' || !Number.isFinite(value.sourceOccurredAt)))
103
+ || (value.process !== undefined && parseProcessFingerprint(value.process) === null)
104
+ || (value.payload !== undefined && !isRecord(value.payload)))
105
+ return null;
106
+ return {
107
+ ...value,
108
+ ...(value.process === undefined ? {} : { process: parseProcessFingerprint(value.process) }),
109
+ };
110
+ }
111
+ function readHookState(file) {
112
+ try {
113
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
114
+ if (!isRecord(value))
115
+ return new Map();
116
+ return new Map(Object.entries(value).flatMap(([paneId, raw]) => {
117
+ const row = isRecord(raw) ? raw : null;
118
+ const payload = isRecord(row?.payload) ? row.payload : null;
119
+ if (!bounded(paneId, 256) || !row || !payload || !bounded(row.src, 64)
120
+ || (row.agent !== undefined && row.agent !== 'claude'))
121
+ return [];
122
+ const ts = typeof row.ts === 'number' && Number.isFinite(row.ts) ? row.ts : 0;
123
+ const sequence = Number.isSafeInteger(row.sequence) && Number(row.sequence) > 0
124
+ ? Number(row.sequence) : undefined;
125
+ const process = row.process === undefined ? undefined : parseProcessFingerprint(row.process);
126
+ if (process === null)
127
+ return [];
128
+ return [[paneId, {
129
+ ts,
130
+ src: row.src,
131
+ ...(row.agent === 'claude' ? { agent: 'claude' } : {}),
132
+ payload,
133
+ ...(sequence === undefined ? {} : { sequence }),
134
+ ...(process === undefined ? {} : { process }),
135
+ }]];
136
+ }));
137
+ }
138
+ catch {
139
+ return new Map();
140
+ }
141
+ }
142
+ function signature(paneId, sessionId, process) {
143
+ return JSON.stringify({ paneId, sessionId, pid: process.pid, startedAt: process.startedAt, tty: process.tty });
144
+ }
145
+ function digest(value) {
146
+ return crypto.createHash('sha256').update(value).digest('hex');
147
+ }
148
+ function gapKey(file, event) {
149
+ return JSON.stringify({
150
+ marker: path.basename(file),
151
+ paneId: event.paneId,
152
+ sessionId: event.sessionId ?? null,
153
+ process: event.process ?? null,
154
+ eventId: event.eventId,
155
+ });
156
+ }
157
+ function candidate(paneId, sessionId, process) {
158
+ const processRef = {
159
+ pid: process.pid,
160
+ ...(process.startedAt === undefined ? {} : { startedAt: process.startedAt }),
161
+ ...(process.tty === undefined ? {} : { tty: process.tty }),
162
+ };
163
+ return {
164
+ paneId,
165
+ attachmentId: `claude-hook:${digest(JSON.stringify({ paneId, process: processRef }))}`,
166
+ ...(sessionId === undefined ? {} : { sessionId }),
167
+ process: processRef,
168
+ };
169
+ }
170
+ function looksLikeClaude(pane, foreground) {
171
+ if (pane.currentCommand === 'claude')
172
+ return true;
173
+ if (!/^\d+[._]\d+[._]\d+$/.test(pane.currentCommand))
174
+ return false;
175
+ return typeof foreground.executable === 'string' && /claude/.test(foreground.executable);
176
+ }
177
+ // Bridges the existing async Claude Hooks without ever blocking Claude itself. Hook subprocesses only
178
+ // commit files; this coordinator owns socket lifetime, per-session replacement and durable acknowledgements.
179
+ export class ClaudeHookBridgeConnector {
180
+ #socketPath;
181
+ #credentialFile;
182
+ #stateDirectory;
183
+ #hookStateFile;
184
+ #eventDirectory;
185
+ #panes;
186
+ #process;
187
+ #pollMs;
188
+ #retryDelayMs;
189
+ #maxRetryDelayMs;
190
+ #logger;
191
+ #createClient;
192
+ #clients = new Map();
193
+ #confirmedGaps = new Set();
194
+ #timer;
195
+ #tail = Promise.resolve();
196
+ #started = false;
197
+ #closed = false;
198
+ constructor({ socketPath, credentialFile, stateDirectory, hookStateFile, eventDirectory, panes, process, pollMs = 250, retryDelayMs = 100, maxRetryDelayMs = 5_000, logger = () => { }, createClient = (options) => new LocalConnectorBridgeClient(options), }) {
199
+ if (![socketPath, credentialFile, stateDirectory, hookStateFile, eventDirectory].every(path.isAbsolute)
200
+ || !panes || !process || !Number.isSafeInteger(pollMs) || pollMs < 50
201
+ || !Number.isSafeInteger(retryDelayMs) || retryDelayMs <= 0
202
+ || !Number.isSafeInteger(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs) {
203
+ throw new TypeError('Claude Hook Bridge Connector requires private paths and Runtime identity sources');
204
+ }
205
+ this.#socketPath = socketPath;
206
+ this.#credentialFile = credentialFile;
207
+ this.#stateDirectory = stateDirectory;
208
+ this.#hookStateFile = hookStateFile;
209
+ this.#eventDirectory = eventDirectory;
210
+ this.#panes = panes;
211
+ this.#process = process;
212
+ this.#pollMs = pollMs;
213
+ this.#retryDelayMs = retryDelayMs;
214
+ this.#maxRetryDelayMs = maxRetryDelayMs;
215
+ this.#logger = logger;
216
+ this.#createClient = createClient;
217
+ }
218
+ start() {
219
+ if (this.#closed)
220
+ throw new Error('Claude Hook Bridge Connector is closed');
221
+ if (this.#started)
222
+ return;
223
+ this.#started = true;
224
+ this.#poll();
225
+ }
226
+ reconcile() {
227
+ const pending = this.#tail.then(() => this.#reconcile());
228
+ this.#tail = pending.catch((error) => {
229
+ this.#logger('Claude Hook Bridge reconciliation failed', error);
230
+ });
231
+ return pending;
232
+ }
233
+ async close() {
234
+ if (this.#closed)
235
+ return;
236
+ this.#closed = true;
237
+ if (this.#timer)
238
+ clearTimeout(this.#timer);
239
+ this.#timer = undefined;
240
+ for (const tracked of this.#clients.values())
241
+ tracked.client.close();
242
+ this.#clients.clear();
243
+ await this.#tail.catch(() => { });
244
+ }
245
+ #poll() {
246
+ if (this.#closed)
247
+ return;
248
+ void this.reconcile().catch(() => { }).finally(() => {
249
+ if (this.#closed)
250
+ return;
251
+ this.#timer = setTimeout(() => this.#poll(), this.#pollMs);
252
+ this.#timer.unref?.();
253
+ });
254
+ }
255
+ #eventFiles() {
256
+ try {
257
+ return fs.readdirSync(this.#eventDirectory)
258
+ .filter((name) => /^(event-\d+-\d+|gap-[a-f0-9]+)\.json$/.test(name))
259
+ .sort((first, second) => {
260
+ const firstGap = first.startsWith('gap-');
261
+ const secondGap = second.startsWith('gap-');
262
+ return firstGap === secondGap ? first.localeCompare(second) : firstGap ? -1 : 1;
263
+ })
264
+ .map((name) => path.join(this.#eventDirectory, name));
265
+ }
266
+ catch {
267
+ return [];
268
+ }
269
+ }
270
+ async #reconcile() {
271
+ if (this.#closed)
272
+ return;
273
+ let panes;
274
+ try {
275
+ panes = await this.#panes.list();
276
+ }
277
+ catch {
278
+ return;
279
+ }
280
+ const paneMap = new Map(panes.map((pane) => [pane.paneId, pane]));
281
+ const identities = new Map();
282
+ const identity = (pane) => {
283
+ let pending = identities.get(pane.paneId);
284
+ if (!pending) {
285
+ pending = this.#process.inspectForeground(pane);
286
+ identities.set(pane.paneId, pending);
287
+ }
288
+ return pending;
289
+ };
290
+ const events = [];
291
+ for (const eventFile of this.#eventFiles()) {
292
+ let event = null;
293
+ try {
294
+ const stat = fs.lstatSync(eventFile);
295
+ if (stat.isFile())
296
+ event = parseEvent(JSON.parse(fs.readFileSync(eventFile, 'utf8')));
297
+ }
298
+ catch { /* partial/removed file */ }
299
+ if (event)
300
+ events.push({ file: eventFile, event });
301
+ }
302
+ const state = readHookState(this.#hookStateFile);
303
+ const gaps = new Map();
304
+ const activeGapKeys = new Set(events.flatMap(({ file, event }) => (event.type === 'gap' ? [gapKey(file, event)] : [])));
305
+ for (const key of this.#confirmedGaps) {
306
+ if (!activeGapKeys.has(key))
307
+ this.#confirmedGaps.delete(key);
308
+ }
309
+ for (const { file: eventFile, event } of events) {
310
+ if (event.type !== 'gap')
311
+ continue;
312
+ const pane = paneMap.get(event.paneId);
313
+ const foreground = pane ? await identity(pane) : null;
314
+ if (!pane || !foreground)
315
+ continue;
316
+ if (!matchesSourceProcess(event.process, foreground)) {
317
+ this.#logger('Discarding stale Claude Hook event after pane process replacement', {
318
+ eventId: event.eventId,
319
+ paneId: event.paneId,
320
+ });
321
+ try {
322
+ fs.unlinkSync(eventFile);
323
+ }
324
+ catch { /* concurrently removed */ }
325
+ continue;
326
+ }
327
+ const latest = state.get(event.paneId);
328
+ const latestSession = latest ? optionalSession(latest.payload) : undefined;
329
+ const gapSession = optionalSession(event.payload ?? {}, event.sessionId);
330
+ if (latestSession && gapSession && latestSession !== gapSession) {
331
+ try {
332
+ fs.unlinkSync(eventFile);
333
+ }
334
+ catch { /* replaced session already retired the gap */ }
335
+ continue;
336
+ }
337
+ gaps.set(event.paneId, { event, key: gapKey(eventFile, event) });
338
+ }
339
+ const currentStatePanes = new Set();
340
+ const persistedSnapshots = new Map();
341
+ for (const [paneId, row] of state) {
342
+ const pane = paneMap.get(paneId);
343
+ const foreground = pane ? await identity(pane) : null;
344
+ if (!pane || !foreground)
345
+ continue;
346
+ if (!matchesSourceProcess(row.process, foreground))
347
+ continue;
348
+ currentStatePanes.add(paneId);
349
+ const sessionId = optionalSession(row.payload);
350
+ const eventId = row.sequence === undefined ? undefined : `claude-hook-${row.sequence}`;
351
+ const projection = projectClaudeHookInbox({
352
+ ...(eventId === undefined ? {} : { eventId }),
353
+ src: row.src,
354
+ sourceOccurredAt: row.ts,
355
+ payload: row.payload,
356
+ });
357
+ const snapshot = gaps.has(paneId) ? {
358
+ ...projection.snapshot,
359
+ availability: 'degraded',
360
+ message: 'Claude Hook event history is incomplete',
361
+ } : projection.snapshot;
362
+ persistedSnapshots.set(paneId, this.#client(paneId, sessionId, foreground).setSnapshot('inbox', snapshot));
363
+ }
364
+ const liveClaudePanes = new Set(currentStatePanes);
365
+ for (const pane of panes) {
366
+ if (currentStatePanes.has(pane.paneId))
367
+ continue;
368
+ const foreground = await identity(pane);
369
+ if (!foreground || !looksLikeClaude(pane, foreground))
370
+ continue;
371
+ liveClaudePanes.add(pane.paneId);
372
+ const gap = gaps.get(pane.paneId)?.event;
373
+ const sessionId = gap ? optionalSession(gap.payload ?? {}, gap.sessionId) : undefined;
374
+ persistedSnapshots.set(pane.paneId, this.#client(pane.paneId, sessionId, foreground).setSnapshot('inbox', gap ? {
375
+ availability: 'degraded',
376
+ message: 'Claude Hook event history is incomplete',
377
+ } : { availability: 'ready' }));
378
+ }
379
+ for (const { file: eventFile, event } of events) {
380
+ const gap = event.type === 'gap' ? gaps.get(event.paneId) : undefined;
381
+ if (event.type === 'gap' && gap?.event !== event)
382
+ continue;
383
+ const pane = paneMap.get(event.paneId);
384
+ const foreground = pane ? await identity(pane) : null;
385
+ if (!pane || !foreground)
386
+ continue;
387
+ if (!matchesSourceProcess(event.process, foreground)) {
388
+ this.#logger('Discarding stale Claude Hook event after pane process replacement', {
389
+ eventId: event.eventId,
390
+ paneId: event.paneId,
391
+ });
392
+ try {
393
+ fs.unlinkSync(eventFile);
394
+ }
395
+ catch { /* concurrently removed */ }
396
+ continue;
397
+ }
398
+ const payload = event.payload ?? {};
399
+ const sessionId = optionalSession(payload, event.sessionId);
400
+ const client = this.#client(event.paneId, sessionId, foreground);
401
+ if (event.type === 'gap' && persistedSnapshots.get(event.paneId) !== true)
402
+ continue;
403
+ if (event.type === 'gap' && gap && this.#confirmedGaps.has(gap.key))
404
+ continue;
405
+ const projection = event.type === 'gap' ? {
406
+ operation: { kind: 'gap', eventId: event.eventId },
407
+ } : projectClaudeHookInbox({
408
+ eventId: event.eventId,
409
+ src: event.src ?? '',
410
+ ...(event.sourceOccurredAt === undefined ? {} : {
411
+ sourceOccurredAt: event.sourceOccurredAt,
412
+ }),
413
+ payload,
414
+ });
415
+ let operation = projection.operation;
416
+ const latest = state.get(event.paneId);
417
+ const latestSession = latest ? optionalSession(latest.payload) : undefined;
418
+ if (event.type === 'event' && operation.kind === 'set' && operation.state === 'waiting'
419
+ && event.sequence !== undefined && latest?.sequence !== undefined
420
+ && latest.sequence > event.sequence && latestSession === sessionId) {
421
+ operation = {
422
+ kind: 'superseded',
423
+ eventId: event.eventId,
424
+ ...(event.sourceOccurredAt === undefined ? {} : { sourceOccurredAt: event.sourceOccurredAt }),
425
+ };
426
+ }
427
+ if (!client.publishDurable('inbox', event.eventId, operation)) {
428
+ this.#logger('Claude Hook event remains queued because Connector durable state could not be persisted', {
429
+ eventId: event.eventId,
430
+ paneId: event.paneId,
431
+ });
432
+ continue;
433
+ }
434
+ try {
435
+ await client.waitForDurableAck('inbox', event.eventId);
436
+ if (event.type === 'gap' && gap)
437
+ this.#confirmedGaps.add(gap.key);
438
+ else
439
+ fs.unlinkSync(eventFile);
440
+ }
441
+ catch (error) {
442
+ if (!this.#closed)
443
+ throw error;
444
+ return;
445
+ }
446
+ }
447
+ for (const [paneId, tracked] of [...this.#clients]) {
448
+ if (liveClaudePanes.has(paneId))
449
+ continue;
450
+ tracked.client.close();
451
+ this.#clients.delete(paneId);
452
+ }
453
+ }
454
+ #client(paneId, sessionId, foreground) {
455
+ const nextSignature = signature(paneId, sessionId, foreground);
456
+ const current = this.#clients.get(paneId);
457
+ if (current?.signature === nextSignature)
458
+ return current.client;
459
+ current?.client.close();
460
+ const stateFile = path.join(this.#stateDirectory, `${digest(nextSignature)}.json`);
461
+ const client = this.#createClient({
462
+ adapterId: 'claude',
463
+ socketPath: this.#socketPath,
464
+ credentialFile: this.#credentialFile,
465
+ stateFile,
466
+ candidate: candidate(paneId, sessionId, foreground),
467
+ generation: { id: `claude-session:${digest(sessionId ?? nextSignature)}`, replace: true },
468
+ retryDelayMs: this.#retryDelayMs,
469
+ maxRetryDelayMs: this.#maxRetryDelayMs,
470
+ logger: this.#logger,
471
+ });
472
+ this.#clients.set(paneId, { signature: nextSignature, client });
473
+ client.start();
474
+ return client;
475
+ }
476
+ }
@@ -0,0 +1,8 @@
1
+ import { LocalConnectorBridgeClient } from '../bridgeClient.js';
2
+ // Compatibility name for the Pi Extension. Connection, retry, durable queue and request behavior are
3
+ // implemented once by the shared Handmux-owned Connector client.
4
+ export class PiBridgeClient extends LocalConnectorBridgeClient {
5
+ constructor(options) {
6
+ super({ ...options, adapterId: 'pi' });
7
+ }
8
+ }