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,502 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { promises as fsp } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { sanitizeToolInputWithMetadata, sanitizeToolResultText, } from './conversationProjectionSafety.js';
5
+ const MAX_SESSION_BYTES = 64 * 1024 * 1024;
6
+ const MAX_TEXT_BYTES = 240 * 1024;
7
+ const ENTRY_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
8
+ function record(value) {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
10
+ ? value : null;
11
+ }
12
+ /** Alias Pi's command tools onto the shared Codex command presentation contract. */
13
+ export function normalizePiCommandTool(name, input) {
14
+ if (name !== 'bash' && name !== 'shell')
15
+ return { name, input };
16
+ const args = record(input);
17
+ if (!args)
18
+ return { name, input };
19
+ const command = typeof args.command === 'string' ? args.command
20
+ : name === 'shell' && typeof args.cmd === 'string' ? args.cmd : undefined;
21
+ if (command === undefined)
22
+ return { name, input };
23
+ const { command: _command, ...rest } = args;
24
+ return { name: 'exec_command', input: { ...rest, cmd: command } };
25
+ }
26
+ function time(entry, message) {
27
+ if (typeof message?.timestamp === 'number' && Number.isFinite(message.timestamp))
28
+ return message.timestamp;
29
+ if (typeof entry.timestamp !== 'string')
30
+ return undefined;
31
+ const parsed = Date.parse(entry.timestamp);
32
+ return Number.isFinite(parsed) ? parsed : undefined;
33
+ }
34
+ function cappedText(value, maxBytes = MAX_TEXT_BYTES) {
35
+ if (typeof value !== 'string' || value.length === 0)
36
+ return null;
37
+ const originalBytes = Buffer.byteLength(value);
38
+ if (originalBytes <= maxBytes)
39
+ return { text: value, truncated: false, originalBytes };
40
+ if (maxBytes <= 0)
41
+ return { text: '', truncated: true, originalBytes };
42
+ let text = Buffer.from(value).subarray(0, maxBytes).toString('utf8');
43
+ while (Buffer.byteLength(text) > maxBytes)
44
+ text = text.slice(0, -1);
45
+ return { text, truncated: true, originalBytes };
46
+ }
47
+ function content(value, sanitizeToolResult = false) {
48
+ const raw = typeof value === 'string' ? [{ type: 'text', text: value }]
49
+ : Array.isArray(value) ? value : [];
50
+ const blocks = [];
51
+ let imageCount = 0;
52
+ let truncated = false;
53
+ let redacted = false;
54
+ let originalBytes = 0;
55
+ let remainingBytes = MAX_TEXT_BYTES;
56
+ for (const candidate of raw) {
57
+ const block = record(candidate);
58
+ if (block?.type === 'image') {
59
+ imageCount += 1;
60
+ continue;
61
+ }
62
+ if (block?.type !== 'text')
63
+ continue;
64
+ const rawText = typeof block.text === 'string' ? block.text : null;
65
+ const safe = rawText !== null && sanitizeToolResult ? sanitizeToolResultText(rawText) : null;
66
+ const normalized = cappedText(safe?.text ?? block.text, remainingBytes);
67
+ if (!normalized)
68
+ continue;
69
+ if (normalized.text) {
70
+ blocks.push({ type: 'text', text: normalized.text });
71
+ remainingBytes -= Buffer.byteLength(normalized.text);
72
+ }
73
+ truncated ||= normalized.truncated || safe?.truncated === true;
74
+ redacted ||= safe?.redacted === true;
75
+ originalBytes += safe?.originalBytes ?? normalized.originalBytes;
76
+ }
77
+ return { blocks, imageCount, truncated, redacted, originalBytes };
78
+ }
79
+ function base(entry, sessionId, id = `pi:${entry.id}`, message) {
80
+ const createdAt = time(entry, message);
81
+ return {
82
+ id,
83
+ sessionId,
84
+ status: 'complete',
85
+ ...(createdAt === undefined ? {} : { sourceCreatedAt: createdAt }),
86
+ extensions: { 'pi.entryId': entry.id },
87
+ };
88
+ }
89
+ function textStatus(value) {
90
+ return value.truncated ? {
91
+ status: 'truncated',
92
+ truncation: { reason: 'size_limit', originalBytes: value.originalBytes },
93
+ } : {};
94
+ }
95
+ function imageNotice(entry, sessionId, count, suffix) {
96
+ return count > 0 ? {
97
+ ...base(entry, sessionId, `pi:${entry.id}:${suffix}`),
98
+ kind: 'notice', level: 'info', code: 'image_unavailable',
99
+ message: count === 1 ? 'Image attachment is available in the Pi terminal'
100
+ : `${count} image attachments are available in the Pi terminal`,
101
+ } : null;
102
+ }
103
+ function projectMessage(entry, sessionId) {
104
+ const message = record(entry.message);
105
+ if (!message || typeof message.role !== 'string')
106
+ return [];
107
+ if (message.role === 'user') {
108
+ const projected = content(message.content);
109
+ const items = projected.blocks.length ? [{
110
+ ...base(entry, sessionId, undefined, message), ...textStatus(projected),
111
+ kind: 'message', role: 'user', content: projected.blocks,
112
+ }] : [];
113
+ const notice = imageNotice(entry, sessionId, projected.imageCount, 'images');
114
+ if (notice)
115
+ items.push(notice);
116
+ return items;
117
+ }
118
+ if (message.role === 'assistant') {
119
+ const raw = Array.isArray(message.content) ? message.content : [];
120
+ const textBlocks = content(raw);
121
+ const items = [];
122
+ if (textBlocks.blocks.length) {
123
+ const failed = message.stopReason === 'error' || message.stopReason === 'aborted';
124
+ const errorMessage = message.stopReason === 'aborted'
125
+ ? 'Pi generation was interrupted' : 'Pi generation failed';
126
+ items.push({
127
+ ...base(entry, sessionId, undefined, message),
128
+ ...(failed ? {
129
+ status: 'error',
130
+ error: { code: message.stopReason === 'aborted' ? 'interrupted' : 'provider_error', message: errorMessage },
131
+ } : textStatus(textBlocks)),
132
+ kind: 'message', role: 'assistant', content: textBlocks.blocks,
133
+ });
134
+ }
135
+ else if (message.stopReason === 'error' || message.stopReason === 'aborted') {
136
+ items.push({
137
+ ...base(entry, sessionId, undefined, message),
138
+ status: 'error',
139
+ error: {
140
+ code: message.stopReason === 'aborted' ? 'interrupted' : 'provider_error',
141
+ message: message.stopReason === 'aborted'
142
+ ? 'Pi generation was interrupted' : 'Pi generation failed',
143
+ },
144
+ kind: 'notice', level: 'error', code: 'generation_failed',
145
+ message: message.stopReason === 'aborted'
146
+ ? 'Pi generation was interrupted' : 'Pi generation failed',
147
+ });
148
+ }
149
+ let tool = 0;
150
+ for (const candidate of raw) {
151
+ const block = record(candidate);
152
+ // Pi persists raw thinking blocks without a user-visible summary marker. They are deliberately
153
+ // omitted rather than relabelled as reasoning_summary.
154
+ if (block?.type === 'toolCall' && typeof block.id === 'string'
155
+ && ENTRY_ID_RE.test(block.id) && typeof block.name === 'string' && block.name) {
156
+ const normalizedTool = normalizePiCommandTool(block.name, block.arguments);
157
+ const inputProjection = sanitizeToolInputWithMetadata(normalizedTool.input);
158
+ const input = inputProjection.value;
159
+ items.push({
160
+ ...base(entry, sessionId, `pi:${entry.id}:tool-${tool++}`, message),
161
+ kind: 'tool_call', callId: `pi:${block.id}`, name: normalizedTool.name,
162
+ ...(input === undefined ? {} : { input }),
163
+ ...(inputProjection.truncated ? {
164
+ status: 'truncated',
165
+ truncation: { reason: 'size_limit', originalBytes: inputProjection.originalBytes },
166
+ } : {}),
167
+ });
168
+ }
169
+ }
170
+ const notice = imageNotice(entry, sessionId, textBlocks.imageCount, 'images');
171
+ if (notice)
172
+ items.push(notice);
173
+ return items;
174
+ }
175
+ if (message.role === 'toolResult' && typeof message.toolCallId === 'string') {
176
+ const projected = content(message.content, true);
177
+ const items = [{
178
+ ...base(entry, sessionId, undefined, message), ...textStatus(projected),
179
+ kind: 'tool_result', callId: `pi:${message.toolCallId}`, content: projected.blocks,
180
+ ...(message.isError === true ? { isError: true } : {}),
181
+ }];
182
+ const notice = imageNotice(entry, sessionId, projected.imageCount, 'images');
183
+ if (notice)
184
+ items.push(notice);
185
+ return items;
186
+ }
187
+ if (message.role === 'bashExecution') {
188
+ const callId = `pi:${entry.id}:bash`;
189
+ const safeOutput = sanitizeToolResultText(String(message.output ?? ''));
190
+ const output = safeOutput.text ? safeOutput
191
+ : {
192
+ text: '', truncated: safeOutput.truncated, redacted: safeOutput.redacted,
193
+ originalBytes: safeOutput.originalBytes,
194
+ };
195
+ const normalizedTool = normalizePiCommandTool('bash', { command: String(message.command ?? '') });
196
+ const inputProjection = sanitizeToolInputWithMetadata(normalizedTool.input);
197
+ const input = inputProjection.value;
198
+ return [{
199
+ ...base(entry, sessionId, `pi:${entry.id}:bash-call`, message),
200
+ kind: 'tool_call', callId, name: normalizedTool.name,
201
+ ...(input === undefined ? {} : { input }),
202
+ ...(inputProjection.truncated ? {
203
+ status: 'truncated',
204
+ truncation: { reason: 'size_limit', originalBytes: inputProjection.originalBytes },
205
+ } : {}),
206
+ }, {
207
+ ...base(entry, sessionId, `pi:${entry.id}:bash-result`, message), ...textStatus(output),
208
+ kind: 'tool_result', callId, content: output.text ? [{ type: 'text', text: output.text }] : [],
209
+ ...((typeof message.exitCode === 'number' && message.exitCode !== 0) || message.cancelled === true
210
+ ? { isError: true } : {}),
211
+ }];
212
+ }
213
+ if (message.role === 'custom' && message.display === true) {
214
+ const projected = content(message.content);
215
+ const text = projected.blocks.filter((block) => block.type === 'text').map((block) => block.text).join('\n');
216
+ return text ? [{
217
+ ...base(entry, sessionId, undefined, message), ...textStatus(projected),
218
+ kind: 'notice', level: 'info', code: typeof message.customType === 'string'
219
+ && message.customType.length <= 256 ? message.customType : 'custom_message', message: text,
220
+ }] : [];
221
+ }
222
+ if (message.role === 'branchSummary' || message.role === 'compactionSummary') {
223
+ const summary = cappedText(message.summary);
224
+ return summary ? [{
225
+ ...base(entry, sessionId, undefined, message), ...textStatus(summary),
226
+ kind: 'compaction', summary: summary.text,
227
+ }] : [];
228
+ }
229
+ return [];
230
+ }
231
+ function projectEntry(entry, sessionId) {
232
+ if (entry.type === 'message')
233
+ return projectMessage(entry, sessionId);
234
+ if (entry.type === 'compaction' || entry.type === 'branch_summary') {
235
+ const summary = cappedText(entry.summary);
236
+ return [{
237
+ ...base(entry, sessionId), ...(summary ? textStatus(summary) : {}),
238
+ kind: 'compaction', ...(summary ? { summary: summary.text } : {}),
239
+ }];
240
+ }
241
+ if (entry.type === 'custom_message' && entry.display === true) {
242
+ return projectMessage({ ...entry, type: 'message', message: {
243
+ role: 'custom', customType: entry.customType, content: entry.content,
244
+ display: true, timestamp: Date.parse(entry.timestamp ?? ''),
245
+ } }, sessionId);
246
+ }
247
+ if (entry.type === 'model_change' && typeof entry.provider === 'string'
248
+ && typeof entry.modelId === 'string') {
249
+ return [{
250
+ ...base(entry, sessionId), kind: 'notice', level: 'info', code: 'model_changed',
251
+ message: `Model changed to ${entry.provider}/${entry.modelId}`,
252
+ }];
253
+ }
254
+ if (entry.type === 'thinking_level_change' && typeof entry.thinkingLevel === 'string') {
255
+ return [{
256
+ ...base(entry, sessionId), kind: 'notice', level: 'info', code: 'thinking_level_changed',
257
+ message: `Thinking level changed to ${entry.thinkingLevel}`,
258
+ }];
259
+ }
260
+ return [];
261
+ }
262
+ function visibleText(item) {
263
+ if (item.kind !== 'message' || item.role !== 'user')
264
+ return undefined;
265
+ const value = item.content.flatMap((block) => block.type === 'text' ? [block.text] : []).join('\n');
266
+ return value || undefined;
267
+ }
268
+ function mergeLiveItems(durable, live) {
269
+ const ids = new Set(durable.map((item) => item.id));
270
+ const merged = [...durable];
271
+ const unmatchedDurableUsers = new Set(durable.flatMap((item, index) => (item.kind === 'message' && item.role === 'user' ? [index] : [])));
272
+ for (const item of live) {
273
+ if (ids.has(item.id))
274
+ continue;
275
+ const pendingRequest = item.extensions?.['pi.pendingClientRequestId'];
276
+ const pendingNativeText = item.extensions?.['pi.pendingNativeText'];
277
+ const duplicateIndex = typeof pendingRequest === 'string' && item.sourceCreatedAt !== undefined
278
+ ? [...unmatchedDurableUsers].find((index) => {
279
+ const candidate = durable[index];
280
+ return candidate !== undefined
281
+ && candidate.kind === 'message' && candidate.role === 'user'
282
+ && candidate.sourceCreatedAt === item.sourceCreatedAt
283
+ && visibleText(candidate) === (typeof pendingNativeText === 'string'
284
+ ? pendingNativeText : visibleText(item));
285
+ }) : undefined;
286
+ if (duplicateIndex !== undefined) {
287
+ unmatchedDurableUsers.delete(duplicateIndex);
288
+ continue;
289
+ }
290
+ ids.add(item.id);
291
+ merged.push(structuredClone(item));
292
+ }
293
+ return merged;
294
+ }
295
+ export function projectPiLiveItems(values, sessionId) {
296
+ const seen = new Set();
297
+ return values.flatMap((candidate) => {
298
+ const value = record(candidate);
299
+ if (!value || typeof value.type !== 'string' || typeof value.id !== 'string'
300
+ || !ENTRY_ID_RE.test(value.id) || seen.has(value.id))
301
+ return [];
302
+ const parentId = value.parentId;
303
+ if (parentId !== null && (typeof parentId !== 'string' || !ENTRY_ID_RE.test(parentId)))
304
+ return [];
305
+ seen.add(value.id);
306
+ return projectEntry({ ...value, type: value.type, id: value.id, parentId }, sessionId);
307
+ });
308
+ }
309
+ export async function resolvePiSessionFile(root, sessionId) {
310
+ const suffix = `_${sessionId}.jsonl`;
311
+ let found = null;
312
+ async function scan(directory, depth) {
313
+ if (found || depth > 2)
314
+ return;
315
+ let entries;
316
+ try {
317
+ entries = await fsp.readdir(directory, { withFileTypes: true });
318
+ }
319
+ catch {
320
+ return;
321
+ }
322
+ for (const entry of entries) {
323
+ if (found)
324
+ return;
325
+ const candidate = path.join(directory, entry.name);
326
+ if (entry.isDirectory())
327
+ await scan(candidate, depth + 1);
328
+ else if (entry.isFile() && entry.name.endsWith(suffix))
329
+ found = candidate;
330
+ }
331
+ }
332
+ await scan(root, 0);
333
+ return found;
334
+ }
335
+ async function loadBranch(file, sessionId, requestedLeaf) {
336
+ const stat = await fsp.stat(file);
337
+ if (!stat.isFile() || stat.size > MAX_SESSION_BYTES)
338
+ throw new Error('Pi session file is invalid or too large');
339
+ const lines = (await fsp.readFile(file, 'utf8')).split('\n').filter(Boolean);
340
+ const header = record(JSON.parse(lines.shift() ?? 'null'));
341
+ if (!header || header.type !== 'session' || header.id !== sessionId
342
+ || !Number.isSafeInteger(header.version) || header.version < 1 || header.version > 3) {
343
+ throw new Error('Pi session header is invalid');
344
+ }
345
+ const entries = [];
346
+ const byId = new Map();
347
+ let previousId = null;
348
+ for (const line of lines) {
349
+ const value = record(JSON.parse(line));
350
+ if (!value || typeof value.type !== 'string' || typeof value.id !== 'string'
351
+ || !ENTRY_ID_RE.test(value.id) || byId.has(value.id))
352
+ throw new Error('Pi session entry is invalid');
353
+ const parentId = header.version === 1 ? previousId : value.parentId;
354
+ if (parentId !== null && (typeof parentId !== 'string' || !ENTRY_ID_RE.test(parentId))) {
355
+ throw new Error('Pi session parent is invalid');
356
+ }
357
+ const entry = { ...value, type: value.type, id: value.id, parentId };
358
+ entries.push(entry);
359
+ byId.set(entry.id, entry);
360
+ previousId = entry.id;
361
+ }
362
+ if (requestedLeaf && requestedLeaf !== 'root' && !byId.has(requestedLeaf)) {
363
+ throw new Error('Pi active leaf is not readable yet');
364
+ }
365
+ const leafId = requestedLeaf ?? entries.at(-1)?.id ?? 'root';
366
+ if (leafId === 'root')
367
+ return { leafId, entries: [] };
368
+ const branch = [];
369
+ const visited = new Set();
370
+ let current = byId.get(leafId);
371
+ while (current) {
372
+ if (visited.has(current.id))
373
+ throw new Error('Pi session tree contains a cycle');
374
+ visited.add(current.id);
375
+ branch.push(current);
376
+ if (current.parentId === null)
377
+ break;
378
+ current = byId.get(current.parentId);
379
+ if (!current)
380
+ throw new Error('Pi session tree contains a missing parent');
381
+ }
382
+ branch.reverse();
383
+ return { leafId, entries: branch };
384
+ }
385
+ export class PiConversationHistory {
386
+ #sessionsRoot;
387
+ #resolveFile;
388
+ #activeLeaves = new Map();
389
+ #activeViews = new Map();
390
+ #liveSnapshots = new Map();
391
+ constructor({ sessionsRoot, resolveFile = resolvePiSessionFile }) {
392
+ this.#sessionsRoot = sessionsRoot;
393
+ this.#resolveFile = resolveFile;
394
+ }
395
+ setActiveLeaf(sessionId, leafId) {
396
+ if (!ENTRY_ID_RE.test(leafId))
397
+ throw new Error('Invalid Pi active leaf');
398
+ this.#activeLeaves.set(sessionId, leafId);
399
+ this.#activeViews.delete(sessionId);
400
+ }
401
+ setActiveView(sessionId, leafId, sourceViewId) {
402
+ if (!ENTRY_ID_RE.test(leafId) || typeof sourceViewId !== 'string'
403
+ || sourceViewId.length === 0 || sourceViewId.length > 256) {
404
+ throw new Error('Invalid Pi active view');
405
+ }
406
+ this.#activeLeaves.set(sessionId, leafId);
407
+ this.#activeViews.set(sessionId, sourceViewId);
408
+ }
409
+ beginLive(sessionId, runId) {
410
+ const current = this.#liveSnapshots.get(sessionId);
411
+ if (current?.runId === runId)
412
+ return;
413
+ const sourceViewId = `pi-live:${runId}`;
414
+ this.#liveSnapshots.set(sessionId, {
415
+ runId, sourceViewId, items: [],
416
+ });
417
+ this.#activeLeaves.delete(sessionId);
418
+ this.#activeViews.delete(sessionId);
419
+ }
420
+ setLiveSnapshot(sessionId, runId, leafId, snapshot, sourceViewId) {
421
+ const current = this.#liveSnapshots.get(sessionId);
422
+ if (!current || current.runId !== runId || !ENTRY_ID_RE.test(leafId)
423
+ || (sourceViewId !== undefined && (sourceViewId.length === 0 || sourceViewId.length > 256))
424
+ || (snapshot.sessionFile !== undefined && (!path.isAbsolute(snapshot.sessionFile)
425
+ || snapshot.sessionFile.length > 4096))
426
+ || (snapshot.implementationVersion !== undefined
427
+ && (!Number.isSafeInteger(snapshot.implementationVersion)
428
+ || snapshot.implementationVersion <= 0))) {
429
+ throw new Error('Invalid Pi live snapshot');
430
+ }
431
+ this.#liveSnapshots.set(sessionId, {
432
+ runId,
433
+ leafId,
434
+ sourceViewId: sourceViewId ?? current.sourceViewId,
435
+ ...(snapshot.sessionFile === undefined ? {} : { sessionFile: snapshot.sessionFile }),
436
+ ...(snapshot.implementationVersion === undefined
437
+ ? (current.implementationVersion === undefined
438
+ ? {} : { implementationVersion: current.implementationVersion })
439
+ : { implementationVersion: snapshot.implementationVersion }),
440
+ items: snapshot.items ? structuredClone(snapshot.items) : current.items,
441
+ });
442
+ }
443
+ async discover(sessionId) {
444
+ const snapshot = await this.#snapshot(sessionId);
445
+ if (!snapshot)
446
+ return null;
447
+ const live = this.#liveSnapshots.get(sessionId);
448
+ return {
449
+ sourceViewId: this.#sourceViewId(sessionId, snapshot.leafId),
450
+ ...(live?.implementationVersion === undefined
451
+ ? {} : { implementationVersion: live.implementationVersion }),
452
+ };
453
+ }
454
+ async readPage(session, request) {
455
+ const snapshot = await this.#snapshot(session.sessionId);
456
+ if (!snapshot)
457
+ throw new Error('Pi session is unavailable');
458
+ const all = snapshot.items;
459
+ let end = all.length;
460
+ if (request.beforeSourceCursor !== undefined) {
461
+ if (!/^\d+$/.test(request.beforeSourceCursor))
462
+ throw new Error('Invalid Pi source cursor');
463
+ const cursor = Number(request.beforeSourceCursor);
464
+ if (!Number.isSafeInteger(cursor))
465
+ throw new Error('Invalid Pi source cursor');
466
+ end = Math.min(cursor, all.length);
467
+ }
468
+ const start = Math.max(0, end - request.limit);
469
+ return {
470
+ sessionId: session.sessionId,
471
+ sourceViewId: this.#sourceViewId(session.sessionId, snapshot.leafId),
472
+ sourceHistoryToken: createHash('sha256').update(JSON.stringify(all)).digest('hex'),
473
+ items: all.slice(start, end),
474
+ ...(start > 0 ? { previousSourceCursor: String(start) } : {}),
475
+ hasMore: start > 0,
476
+ };
477
+ }
478
+ async #snapshot(sessionId) {
479
+ const live = this.#liveSnapshots.get(sessionId);
480
+ const file = live?.sessionFile ?? await this.#resolveFile(this.#sessionsRoot, sessionId);
481
+ if (file) {
482
+ try {
483
+ const branch = await loadBranch(file, sessionId, live?.leafId ?? this.#activeLeaves.get(sessionId));
484
+ return {
485
+ leafId: branch.leafId,
486
+ items: mergeLiveItems(branch.entries.flatMap((entry) => projectEntry(entry, sessionId)), live?.items ?? []),
487
+ };
488
+ }
489
+ catch (error) {
490
+ const code = error !== null && typeof error === 'object'
491
+ ? error.code : undefined;
492
+ if (!live || code !== 'ENOENT')
493
+ throw error;
494
+ }
495
+ }
496
+ return live ? { leafId: live.leafId ?? 'root', items: structuredClone(live.items) } : null;
497
+ }
498
+ #sourceViewId(sessionId, leafId) {
499
+ return this.#liveSnapshots.get(sessionId)?.sourceViewId
500
+ ?? this.#activeViews.get(sessionId) ?? `pi-leaf:${leafId}`;
501
+ }
502
+ }