apex-code 0.0.1-alpha.9 → 0.0.4

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 (513) hide show
  1. package/CHANGELOG.md +150 -2
  2. package/README.md +9 -8
  3. package/dist/cli/args.d.ts +25 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +43 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/mcp-command.d.ts +9 -0
  8. package/dist/cli/mcp-command.d.ts.map +1 -0
  9. package/dist/cli/mcp-command.js +53 -0
  10. package/dist/cli/mcp-command.js.map +1 -0
  11. package/dist/cli/project-trust.d.ts.map +1 -1
  12. package/dist/cli/project-trust.js +3 -1
  13. package/dist/cli/project-trust.js.map +1 -1
  14. package/dist/cli/startup-ui.d.ts.map +1 -1
  15. package/dist/cli/startup-ui.js +2 -1
  16. package/dist/cli/startup-ui.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +94 -5
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +24 -5
  22. package/dist/config.js.map +1 -1
  23. package/dist/core/agent-session-services.d.ts +3 -0
  24. package/dist/core/agent-session-services.d.ts.map +1 -1
  25. package/dist/core/agent-session-services.js +1 -0
  26. package/dist/core/agent-session-services.js.map +1 -1
  27. package/dist/core/agent-session.d.ts +157 -8
  28. package/dist/core/agent-session.d.ts.map +1 -1
  29. package/dist/core/agent-session.js +565 -22
  30. package/dist/core/agent-session.js.map +1 -1
  31. package/dist/core/bash-executor.d.ts.map +1 -1
  32. package/dist/core/bash-executor.js +7 -0
  33. package/dist/core/bash-executor.js.map +1 -1
  34. package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
  35. package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
  36. package/dist/core/checkpoints/git-checkpoints.js +251 -0
  37. package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
  38. package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
  39. package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
  40. package/dist/core/checkpoints/session-checkpoints.js +36 -0
  41. package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
  42. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  43. package/dist/core/compaction/branch-summarization.js +4 -3
  44. package/dist/core/compaction/branch-summarization.js.map +1 -1
  45. package/dist/core/compaction/compaction.d.ts +5 -0
  46. package/dist/core/compaction/compaction.d.ts.map +1 -1
  47. package/dist/core/compaction/compaction.js +19 -5
  48. package/dist/core/compaction/compaction.js.map +1 -1
  49. package/dist/core/delegation/runtime.d.ts +6 -5
  50. package/dist/core/delegation/runtime.d.ts.map +1 -1
  51. package/dist/core/delegation/runtime.js +4 -3
  52. package/dist/core/delegation/runtime.js.map +1 -1
  53. package/dist/core/exec.d.ts.map +1 -1
  54. package/dist/core/exec.js +5 -2
  55. package/dist/core/exec.js.map +1 -1
  56. package/dist/core/extensions/index.d.ts +1 -1
  57. package/dist/core/extensions/index.d.ts.map +1 -1
  58. package/dist/core/extensions/index.js.map +1 -1
  59. package/dist/core/extensions/loader.d.ts.map +1 -1
  60. package/dist/core/extensions/loader.js +8 -2
  61. package/dist/core/extensions/loader.js.map +1 -1
  62. package/dist/core/extensions/runner.d.ts +5 -0
  63. package/dist/core/extensions/runner.d.ts.map +1 -1
  64. package/dist/core/extensions/runner.js +45 -1
  65. package/dist/core/extensions/runner.js.map +1 -1
  66. package/dist/core/extensions/source-runtime.d.ts +14 -0
  67. package/dist/core/extensions/source-runtime.d.ts.map +1 -0
  68. package/dist/core/extensions/source-runtime.js +56 -0
  69. package/dist/core/extensions/source-runtime.js.map +1 -0
  70. package/dist/core/extensions/types.d.ts +22 -2
  71. package/dist/core/extensions/types.d.ts.map +1 -1
  72. package/dist/core/extensions/types.js.map +1 -1
  73. package/dist/core/formatter-lifecycle.d.ts +69 -0
  74. package/dist/core/formatter-lifecycle.d.ts.map +1 -0
  75. package/dist/core/formatter-lifecycle.js +360 -0
  76. package/dist/core/formatter-lifecycle.js.map +1 -0
  77. package/dist/core/hooks/command-handler.d.ts +19 -0
  78. package/dist/core/hooks/command-handler.d.ts.map +1 -0
  79. package/dist/core/hooks/command-handler.js +112 -0
  80. package/dist/core/hooks/command-handler.js.map +1 -0
  81. package/dist/core/hooks/http-handler.d.ts +10 -0
  82. package/dist/core/hooks/http-handler.d.ts.map +1 -0
  83. package/dist/core/hooks/http-handler.js +36 -0
  84. package/dist/core/hooks/http-handler.js.map +1 -0
  85. package/dist/core/hooks/loader.d.ts +29 -0
  86. package/dist/core/hooks/loader.d.ts.map +1 -0
  87. package/dist/core/hooks/loader.js +139 -0
  88. package/dist/core/hooks/loader.js.map +1 -0
  89. package/dist/core/hooks/runtime.d.ts +13 -0
  90. package/dist/core/hooks/runtime.d.ts.map +1 -0
  91. package/dist/core/hooks/runtime.js +63 -0
  92. package/dist/core/hooks/runtime.js.map +1 -0
  93. package/dist/core/hooks/types.d.ts +83 -0
  94. package/dist/core/hooks/types.d.ts.map +1 -0
  95. package/dist/core/hooks/types.js +24 -0
  96. package/dist/core/hooks/types.js.map +1 -0
  97. package/dist/core/mcp/config.d.ts +16 -0
  98. package/dist/core/mcp/config.d.ts.map +1 -0
  99. package/dist/core/mcp/config.js +166 -0
  100. package/dist/core/mcp/config.js.map +1 -0
  101. package/dist/core/mcp/connector.d.ts +35 -0
  102. package/dist/core/mcp/connector.d.ts.map +1 -0
  103. package/dist/core/mcp/connector.js +105 -0
  104. package/dist/core/mcp/connector.js.map +1 -0
  105. package/dist/core/mcp/contract.d.ts +19 -0
  106. package/dist/core/mcp/contract.d.ts.map +1 -0
  107. package/dist/core/mcp/contract.js +89 -0
  108. package/dist/core/mcp/contract.js.map +1 -0
  109. package/dist/core/mcp/mcp-tool.d.ts +32 -0
  110. package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
  111. package/dist/core/mcp/mcp-tool.js +171 -0
  112. package/dist/core/mcp/mcp-tool.js.map +1 -0
  113. package/dist/core/mcp/metadata-cache.d.ts +29 -0
  114. package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
  115. package/dist/core/mcp/metadata-cache.js +99 -0
  116. package/dist/core/mcp/metadata-cache.js.map +1 -0
  117. package/dist/core/mcp/oauth/authorize.d.ts +21 -0
  118. package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
  119. package/dist/core/mcp/oauth/authorize.js +32 -0
  120. package/dist/core/mcp/oauth/authorize.js.map +1 -0
  121. package/dist/core/mcp/oauth/discover.d.ts +27 -0
  122. package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
  123. package/dist/core/mcp/oauth/discover.js +116 -0
  124. package/dist/core/mcp/oauth/discover.js.map +1 -0
  125. package/dist/core/mcp/oauth/flow.d.ts +31 -0
  126. package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
  127. package/dist/core/mcp/oauth/flow.js +175 -0
  128. package/dist/core/mcp/oauth/flow.js.map +1 -0
  129. package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
  130. package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
  131. package/dist/core/mcp/oauth/mcp-token.js +129 -0
  132. package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
  133. package/dist/core/mcp/oauth/pkce.d.ts +12 -0
  134. package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
  135. package/dist/core/mcp/oauth/pkce.js +11 -0
  136. package/dist/core/mcp/oauth/pkce.js.map +1 -0
  137. package/dist/core/mcp/oauth/register.d.ts +16 -0
  138. package/dist/core/mcp/oauth/register.d.ts.map +1 -0
  139. package/dist/core/mcp/oauth/register.js +42 -0
  140. package/dist/core/mcp/oauth/register.js.map +1 -0
  141. package/dist/core/mcp/oauth/token-client.d.ts +33 -0
  142. package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
  143. package/dist/core/mcp/oauth/token-client.js +65 -0
  144. package/dist/core/mcp/oauth/token-client.js.map +1 -0
  145. package/dist/core/mcp/runtime.d.ts +20 -0
  146. package/dist/core/mcp/runtime.d.ts.map +1 -0
  147. package/dist/core/mcp/runtime.js +37 -0
  148. package/dist/core/mcp/runtime.js.map +1 -0
  149. package/dist/core/mcp/schema.d.ts +22 -0
  150. package/dist/core/mcp/schema.d.ts.map +1 -0
  151. package/dist/core/mcp/schema.js +20 -0
  152. package/dist/core/mcp/schema.js.map +1 -0
  153. package/dist/core/mcp/server-manager.d.ts +43 -0
  154. package/dist/core/mcp/server-manager.d.ts.map +1 -0
  155. package/dist/core/mcp/server-manager.js +142 -0
  156. package/dist/core/mcp/server-manager.js.map +1 -0
  157. package/dist/core/mcp/types.d.ts +100 -0
  158. package/dist/core/mcp/types.d.ts.map +1 -0
  159. package/dist/core/mcp/types.js +8 -0
  160. package/dist/core/mcp/types.js.map +1 -0
  161. package/dist/core/permissions/gate.d.ts.map +1 -1
  162. package/dist/core/permissions/gate.js +45 -4
  163. package/dist/core/permissions/gate.js.map +1 -1
  164. package/dist/core/permissions/operations.d.ts +20 -0
  165. package/dist/core/permissions/operations.d.ts.map +1 -0
  166. package/dist/core/permissions/operations.js +9 -0
  167. package/dist/core/permissions/operations.js.map +1 -0
  168. package/dist/core/permissions/policy-command.d.ts +42 -0
  169. package/dist/core/permissions/policy-command.d.ts.map +1 -0
  170. package/dist/core/permissions/policy-command.js +44 -0
  171. package/dist/core/permissions/policy-command.js.map +1 -0
  172. package/dist/core/permissions/responder.d.ts +49 -2
  173. package/dist/core/permissions/responder.d.ts.map +1 -1
  174. package/dist/core/permissions/responder.js +20 -8
  175. package/dist/core/permissions/responder.js.map +1 -1
  176. package/dist/core/permissions/rules.d.ts.map +1 -1
  177. package/dist/core/permissions/rules.js +11 -1
  178. package/dist/core/permissions/rules.js.map +1 -1
  179. package/dist/core/permissions/store.d.ts +18 -0
  180. package/dist/core/permissions/store.d.ts.map +1 -1
  181. package/dist/core/permissions/store.js +89 -8
  182. package/dist/core/permissions/store.js.map +1 -1
  183. package/dist/core/policy-executor.d.ts +47 -0
  184. package/dist/core/policy-executor.d.ts.map +1 -0
  185. package/dist/core/policy-executor.js +228 -0
  186. package/dist/core/policy-executor.js.map +1 -0
  187. package/dist/core/policy-loader.d.ts +72 -0
  188. package/dist/core/policy-loader.d.ts.map +1 -0
  189. package/dist/core/policy-loader.js +195 -0
  190. package/dist/core/policy-loader.js.map +1 -0
  191. package/dist/core/project-trust.d.ts +1 -1
  192. package/dist/core/project-trust.d.ts.map +1 -1
  193. package/dist/core/project-trust.js.map +1 -1
  194. package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
  195. package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
  196. package/dist/core/sandbox/bwrap-arguments.js +136 -0
  197. package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
  198. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  199. package/dist/core/sandbox/child-entry.js +3 -1
  200. package/dist/core/sandbox/child-entry.js.map +1 -1
  201. package/dist/core/sandbox/cli-launch.d.ts +84 -4
  202. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  203. package/dist/core/sandbox/cli-launch.js +115 -16
  204. package/dist/core/sandbox/cli-launch.js.map +1 -1
  205. package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
  206. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  207. package/dist/core/sandbox/cli-supervisor.js +50 -2
  208. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  209. package/dist/core/sandbox/full-access.d.ts +28 -0
  210. package/dist/core/sandbox/full-access.d.ts.map +1 -0
  211. package/dist/core/sandbox/full-access.js +63 -0
  212. package/dist/core/sandbox/full-access.js.map +1 -0
  213. package/dist/core/sandbox/git-identity.d.ts +57 -0
  214. package/dist/core/sandbox/git-identity.d.ts.map +1 -0
  215. package/dist/core/sandbox/git-identity.js +78 -0
  216. package/dist/core/sandbox/git-identity.js.map +1 -0
  217. package/dist/core/sandbox/host-approval.d.ts +42 -0
  218. package/dist/core/sandbox/host-approval.d.ts.map +1 -0
  219. package/dist/core/sandbox/host-approval.js +99 -0
  220. package/dist/core/sandbox/host-approval.js.map +1 -0
  221. package/dist/core/sandbox/linux-backend.d.ts +14 -0
  222. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  223. package/dist/core/sandbox/linux-backend.js +158 -80
  224. package/dist/core/sandbox/linux-backend.js.map +1 -1
  225. package/dist/core/sandbox/macos-backend.d.ts +5 -0
  226. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  227. package/dist/core/sandbox/macos-backend.js +164 -5
  228. package/dist/core/sandbox/macos-backend.js.map +1 -1
  229. package/dist/core/sandbox/network-proxy.d.ts +19 -0
  230. package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
  231. package/dist/core/sandbox/network-proxy.js +93 -19
  232. package/dist/core/sandbox/network-proxy.js.map +1 -1
  233. package/dist/core/sandbox/policy.d.ts +8 -0
  234. package/dist/core/sandbox/policy.d.ts.map +1 -1
  235. package/dist/core/sandbox/policy.js +29 -12
  236. package/dist/core/sandbox/policy.js.map +1 -1
  237. package/dist/core/sandbox/profiles.d.ts +25 -0
  238. package/dist/core/sandbox/profiles.d.ts.map +1 -0
  239. package/dist/core/sandbox/profiles.js +23 -0
  240. package/dist/core/sandbox/profiles.js.map +1 -0
  241. package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
  242. package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
  243. package/dist/core/sandbox/rpc/command-client.js +86 -0
  244. package/dist/core/sandbox/rpc/command-client.js.map +1 -0
  245. package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
  246. package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
  247. package/dist/core/sandbox/rpc/command-proxy.js +120 -0
  248. package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
  249. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
  250. package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
  251. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
  252. package/dist/core/sandbox/rpc/framing.d.ts +35 -0
  253. package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
  254. package/dist/core/sandbox/rpc/framing.js +114 -0
  255. package/dist/core/sandbox/rpc/framing.js.map +1 -0
  256. package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
  257. package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
  258. package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
  259. package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
  260. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
  261. package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
  262. package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
  263. package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
  264. package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
  265. package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
  266. package/dist/core/sandbox/supervisor-temp.js +21 -0
  267. package/dist/core/sandbox/supervisor-temp.js.map +1 -0
  268. package/dist/core/sandbox/supervisor.d.ts +12 -1
  269. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  270. package/dist/core/sandbox/supervisor.js +15 -1
  271. package/dist/core/sandbox/supervisor.js.map +1 -1
  272. package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
  273. package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
  274. package/dist/core/sandbox/terminal-handoff.js +221 -0
  275. package/dist/core/sandbox/terminal-handoff.js.map +1 -0
  276. package/dist/core/sandbox/terminal-size.d.ts +13 -0
  277. package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
  278. package/dist/core/sandbox/terminal-size.js +23 -2
  279. package/dist/core/sandbox/terminal-size.js.map +1 -1
  280. package/dist/core/sdk.d.ts +22 -0
  281. package/dist/core/sdk.d.ts.map +1 -1
  282. package/dist/core/sdk.js +53 -1
  283. package/dist/core/sdk.js.map +1 -1
  284. package/dist/core/session-manager.d.ts +13 -1
  285. package/dist/core/session-manager.d.ts.map +1 -1
  286. package/dist/core/session-manager.js +32 -5
  287. package/dist/core/session-manager.js.map +1 -1
  288. package/dist/core/settings-manager.d.ts +163 -1
  289. package/dist/core/settings-manager.d.ts.map +1 -1
  290. package/dist/core/settings-manager.js +107 -0
  291. package/dist/core/settings-manager.js.map +1 -1
  292. package/dist/core/slash-commands.d.ts.map +1 -1
  293. package/dist/core/slash-commands.js +2 -0
  294. package/dist/core/slash-commands.js.map +1 -1
  295. package/dist/core/tools/background-shell.d.ts +39 -0
  296. package/dist/core/tools/background-shell.d.ts.map +1 -0
  297. package/dist/core/tools/background-shell.js +81 -0
  298. package/dist/core/tools/background-shell.js.map +1 -0
  299. package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
  300. package/dist/core/tools/bash-command-segments.js +2 -0
  301. package/dist/core/tools/bash-command-segments.js.map +1 -1
  302. package/dist/core/tools/bash.d.ts +30 -15
  303. package/dist/core/tools/bash.d.ts.map +1 -1
  304. package/dist/core/tools/bash.js +228 -12
  305. package/dist/core/tools/bash.js.map +1 -1
  306. package/dist/core/tools/contract-snapshot.d.ts +58 -0
  307. package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
  308. package/dist/core/tools/contract-snapshot.js +51 -0
  309. package/dist/core/tools/contract-snapshot.js.map +1 -0
  310. package/dist/core/tools/contract.d.ts +53 -5
  311. package/dist/core/tools/contract.d.ts.map +1 -1
  312. package/dist/core/tools/contract.js +22 -2
  313. package/dist/core/tools/contract.js.map +1 -1
  314. package/dist/core/tools/delegate.d.ts +6 -4
  315. package/dist/core/tools/delegate.d.ts.map +1 -1
  316. package/dist/core/tools/delegate.js +21 -11
  317. package/dist/core/tools/delegate.js.map +1 -1
  318. package/dist/core/tools/edit-diff.d.ts +48 -0
  319. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  320. package/dist/core/tools/edit-diff.js +215 -3
  321. package/dist/core/tools/edit-diff.js.map +1 -1
  322. package/dist/core/tools/edit.d.ts +8 -0
  323. package/dist/core/tools/edit.d.ts.map +1 -1
  324. package/dist/core/tools/edit.js +53 -7
  325. package/dist/core/tools/edit.js.map +1 -1
  326. package/dist/core/tools/find.d.ts.map +1 -1
  327. package/dist/core/tools/find.js +5 -2
  328. package/dist/core/tools/find.js.map +1 -1
  329. package/dist/core/tools/grep.d.ts.map +1 -1
  330. package/dist/core/tools/grep.js +5 -2
  331. package/dist/core/tools/grep.js.map +1 -1
  332. package/dist/core/tools/index.d.ts +10 -1
  333. package/dist/core/tools/index.d.ts.map +1 -1
  334. package/dist/core/tools/index.js +3 -0
  335. package/dist/core/tools/index.js.map +1 -1
  336. package/dist/core/tools/ls.d.ts.map +1 -1
  337. package/dist/core/tools/ls.js +5 -2
  338. package/dist/core/tools/ls.js.map +1 -1
  339. package/dist/core/tools/lsp.d.ts +19 -1
  340. package/dist/core/tools/lsp.d.ts.map +1 -1
  341. package/dist/core/tools/lsp.js +163 -10
  342. package/dist/core/tools/lsp.js.map +1 -1
  343. package/dist/core/tools/path-permission.d.ts +3 -0
  344. package/dist/core/tools/path-permission.d.ts.map +1 -1
  345. package/dist/core/tools/path-permission.js +16 -4
  346. package/dist/core/tools/path-permission.js.map +1 -1
  347. package/dist/core/tools/path-utils.d.ts +27 -2
  348. package/dist/core/tools/path-utils.d.ts.map +1 -1
  349. package/dist/core/tools/path-utils.js +221 -4
  350. package/dist/core/tools/path-utils.js.map +1 -1
  351. package/dist/core/tools/read.d.ts.map +1 -1
  352. package/dist/core/tools/read.js +20 -11
  353. package/dist/core/tools/read.js.map +1 -1
  354. package/dist/core/tools/test.d.ts +54 -9
  355. package/dist/core/tools/test.d.ts.map +1 -1
  356. package/dist/core/tools/test.js +239 -19
  357. package/dist/core/tools/test.js.map +1 -1
  358. package/dist/core/tools/write.d.ts +7 -0
  359. package/dist/core/tools/write.d.ts.map +1 -1
  360. package/dist/core/tools/write.js +64 -9
  361. package/dist/core/tools/write.js.map +1 -1
  362. package/dist/core/verification-lifecycle.d.ts +77 -0
  363. package/dist/core/verification-lifecycle.d.ts.map +1 -0
  364. package/dist/core/verification-lifecycle.js +146 -0
  365. package/dist/core/verification-lifecycle.js.map +1 -0
  366. package/dist/core/workspace/artifacts.d.ts +45 -0
  367. package/dist/core/workspace/artifacts.d.ts.map +1 -0
  368. package/dist/core/workspace/artifacts.js +165 -0
  369. package/dist/core/workspace/artifacts.js.map +1 -0
  370. package/dist/core/workspace/comparison.d.ts +27 -0
  371. package/dist/core/workspace/comparison.d.ts.map +1 -0
  372. package/dist/core/workspace/comparison.js +94 -0
  373. package/dist/core/workspace/comparison.js.map +1 -0
  374. package/dist/core/workspace/git-observer.d.ts +48 -0
  375. package/dist/core/workspace/git-observer.d.ts.map +1 -0
  376. package/dist/core/workspace/git-observer.js +346 -0
  377. package/dist/core/workspace/git-observer.js.map +1 -0
  378. package/dist/core/workspace/projection.d.ts +3 -0
  379. package/dist/core/workspace/projection.d.ts.map +1 -0
  380. package/dist/core/workspace/projection.js +56 -0
  381. package/dist/core/workspace/projection.js.map +1 -0
  382. package/dist/core/workspace/state.d.ts +133 -0
  383. package/dist/core/workspace/state.d.ts.map +1 -0
  384. package/dist/core/workspace/state.js +96 -0
  385. package/dist/core/workspace/state.js.map +1 -0
  386. package/dist/extensions/llama/client.d.ts +6 -0
  387. package/dist/extensions/llama/client.d.ts.map +1 -1
  388. package/dist/extensions/llama/client.js +7 -0
  389. package/dist/extensions/llama/client.js.map +1 -1
  390. package/dist/extensions/llama/provider.d.ts +3 -1
  391. package/dist/extensions/llama/provider.d.ts.map +1 -1
  392. package/dist/extensions/llama/provider.js +27 -6
  393. package/dist/extensions/llama/provider.js.map +1 -1
  394. package/dist/index.d.ts +4 -1
  395. package/dist/index.d.ts.map +1 -1
  396. package/dist/index.js +3 -0
  397. package/dist/index.js.map +1 -1
  398. package/dist/main.d.ts +3 -1
  399. package/dist/main.d.ts.map +1 -1
  400. package/dist/main.js +41 -9
  401. package/dist/main.js.map +1 -1
  402. package/dist/modes/acp/server.d.ts +55 -0
  403. package/dist/modes/acp/server.d.ts.map +1 -0
  404. package/dist/modes/acp/server.js +244 -0
  405. package/dist/modes/acp/server.js.map +1 -0
  406. package/dist/modes/acp/translate.d.ts +37 -0
  407. package/dist/modes/acp/translate.d.ts.map +1 -0
  408. package/dist/modes/acp/translate.js +50 -0
  409. package/dist/modes/acp/translate.js.map +1 -0
  410. package/dist/modes/index.d.ts +1 -0
  411. package/dist/modes/index.d.ts.map +1 -1
  412. package/dist/modes/index.js +1 -0
  413. package/dist/modes/index.js.map +1 -1
  414. package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
  415. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  416. package/dist/modes/interactive/components/assistant-message.js +182 -23
  417. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  418. package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
  419. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  420. package/dist/modes/interactive/components/bash-execution.js +20 -0
  421. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  422. package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
  423. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  424. package/dist/modes/interactive/components/custom-editor.js +20 -9
  425. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  426. package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
  427. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  428. package/dist/modes/interactive/components/extension-selector.js +7 -3
  429. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  430. package/dist/modes/interactive/components/footer.d.ts +19 -0
  431. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  432. package/dist/modes/interactive/components/footer.js +70 -10
  433. package/dist/modes/interactive/components/footer.js.map +1 -1
  434. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  435. package/dist/modes/interactive/components/model-selector.js +6 -6
  436. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  437. package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
  438. package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
  439. package/dist/modes/interactive/components/permission-preview.js +33 -0
  440. package/dist/modes/interactive/components/permission-preview.js.map +1 -0
  441. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  442. package/dist/modes/interactive/components/session-selector.js +2 -2
  443. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  444. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  445. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  446. package/dist/modes/interactive/components/settings-selector.js +10 -0
  447. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  448. package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
  449. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  450. package/dist/modes/interactive/components/tool-execution.js +64 -10
  451. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  452. package/dist/modes/interactive/interactive-mode.d.ts +34 -0
  453. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  454. package/dist/modes/interactive/interactive-mode.js +231 -26
  455. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  456. package/dist/modes/interactive/theme/theme.d.ts +12 -0
  457. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  458. package/dist/modes/interactive/theme/theme.js +15 -5
  459. package/dist/modes/interactive/theme/theme.js.map +1 -1
  460. package/dist/modes/print-mode.d.ts.map +1 -1
  461. package/dist/modes/print-mode.js +27 -12
  462. package/dist/modes/print-mode.js.map +1 -1
  463. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  464. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  465. package/dist/modes/rpc/rpc-client.js +7 -0
  466. package/dist/modes/rpc/rpc-client.js.map +1 -1
  467. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  468. package/dist/modes/rpc/rpc-mode.js +3 -0
  469. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  470. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  471. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  472. package/dist/modes/rpc/rpc-types.js.map +1 -1
  473. package/dist/package-manager-cli.d.ts.map +1 -1
  474. package/dist/package-manager-cli.js +0 -6
  475. package/dist/package-manager-cli.js.map +1 -1
  476. package/dist/utils/shell.d.ts.map +1 -1
  477. package/dist/utils/shell.js +6 -4
  478. package/dist/utils/shell.js.map +1 -1
  479. package/dist/utils/version-check.d.ts.map +1 -1
  480. package/dist/utils/version-check.js +6 -4
  481. package/dist/utils/version-check.js.map +1 -1
  482. package/docs/compaction.md +4 -0
  483. package/docs/custom-provider.md +7 -7
  484. package/docs/environment-variables.md +1 -1
  485. package/docs/extensions.md +18 -0
  486. package/docs/keybindings.md +1 -1
  487. package/docs/quickstart.md +1 -1
  488. package/docs/rpc.md +23 -0
  489. package/docs/sdk.md +25 -0
  490. package/docs/settings.md +12 -4
  491. package/docs/terminal-setup.md +12 -0
  492. package/docs/usage.md +4 -3
  493. package/examples/extensions/built-in-tool-renderer.ts +8 -4
  494. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  495. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  496. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  497. package/examples/extensions/git-checkpoint.ts +24 -36
  498. package/examples/extensions/gondolin/package-lock.json +2 -2
  499. package/examples/extensions/gondolin/package.json +1 -1
  500. package/examples/extensions/minimal-mode.ts +2 -2
  501. package/examples/extensions/permission-gate.ts +1 -1
  502. package/examples/extensions/plan-mode/index.ts +1 -1
  503. package/examples/extensions/rpc-demo.ts +1 -1
  504. package/examples/extensions/sandbox/package-lock.json +2 -2
  505. package/examples/extensions/sandbox/package.json +1 -1
  506. package/examples/extensions/with-deps/package-lock.json +2 -2
  507. package/examples/extensions/with-deps/package.json +1 -1
  508. package/npm-shrinkwrap.json +194 -46
  509. package/package.json +14 -12
  510. package/dist/server/create-harness.d.ts +0 -26
  511. package/dist/server/create-harness.d.ts.map +0 -1
  512. package/dist/server/create-harness.js +0 -107
  513. package/dist/server/create-harness.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAqGrB","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { finished } from \"node:stream/promises\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `apex-code-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst closeTempFile = async () => {\n\t\tif (!tempFileStream) return;\n\t\ttempFileStream.end();\n\t\tawait finished(tempFileStream);\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tawait closeTempFile();\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tawait closeTempFile();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFile();\n\n\t\tthrow err;\n\t}\n}\n"]}
1
+ {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CA4GrB","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { finished } from \"node:stream/promises\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `apex-code-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t// A stream 'error' event (ENOSPC, EACCES) with no listener crashes the\n\t\t// whole process mid-command; full-output capture is best-effort, so\n\t\t// drop the capture and the reported path together.\n\t\ttempFileStream.on(\"error\", () => {\n\t\t\ttempFileStream = undefined;\n\t\t\ttempFilePath = undefined;\n\t\t});\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst closeTempFile = async () => {\n\t\tif (!tempFileStream) return;\n\t\ttempFileStream.end();\n\t\tawait finished(tempFileStream);\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tawait closeTempFile();\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tawait closeTempFile();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFile();\n\n\t\tthrow err;\n\t}\n}\n"]}
@@ -34,6 +34,13 @@ export async function executeBashWithOperations(command, cwd, operations, option
34
34
  const id = randomBytes(8).toString("hex");
35
35
  tempFilePath = join(tmpdir(), `apex-code-bash-${id}.log`);
36
36
  tempFileStream = createWriteStream(tempFilePath);
37
+ // A stream 'error' event (ENOSPC, EACCES) with no listener crashes the
38
+ // whole process mid-command; full-output capture is best-effort, so
39
+ // drop the capture and the reported path together.
40
+ tempFileStream.on("error", () => {
41
+ tempFileStream = undefined;
42
+ tempFilePath = undefined;
43
+ });
37
44
  for (const chunk of outputChunks) {
38
45
  tempFileStream.write(chunk);
39
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { finished } from \"node:stream/promises\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `apex-code-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst closeTempFile = async () => {\n\t\tif (!tempFileStream) return;\n\t\ttempFileStream.end();\n\t\tawait finished(tempFileStream);\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tawait closeTempFile();\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tawait closeTempFile();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFile();\n\n\t\tthrow err;\n\t}\n}\n"]}
1
+ {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { finished } from \"node:stream/promises\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `apex-code-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t// A stream 'error' event (ENOSPC, EACCES) with no listener crashes the\n\t\t// whole process mid-command; full-output capture is best-effort, so\n\t\t// drop the capture and the reported path together.\n\t\ttempFileStream.on(\"error\", () => {\n\t\t\ttempFileStream = undefined;\n\t\t\ttempFilePath = undefined;\n\t\t});\n\t\tfor (const chunk of outputChunks) {\n\t\t\ttempFileStream.write(chunk);\n\t\t}\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\t// Keep rolling buffer\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunks.length > 1) {\n\t\t\tconst removed = outputChunks.shift()!;\n\t\t\toutputBytes -= removed.length;\n\t\t}\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst closeTempFile = async () => {\n\t\tif (!tempFileStream) return;\n\t\ttempFileStream.end();\n\t\tawait finished(tempFileStream);\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tconst fullOutput = outputChunks.join(\"\");\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tawait closeTempFile();\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst fullOutput = outputChunks.join(\"\");\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tawait closeTempFile();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFile();\n\n\t\tthrow err;\n\t}\n}\n"]}
@@ -0,0 +1,39 @@
1
+ /** One captured worktree state, reachable from a ref so garbage collection cannot reap it. */
2
+ export interface GitCheckpoint {
3
+ readonly entryId: string;
4
+ readonly commit: string;
5
+ }
6
+ export interface GitCheckpointsOptions {
7
+ /** Oldest refs beyond this count are deleted on capture. */
8
+ readonly maxPerSession?: number;
9
+ /** A capture that exceeds this is abandoned rather than allowed to stall a turn. */
10
+ readonly timeoutMilliseconds?: number;
11
+ }
12
+ export interface GitCheckpoints {
13
+ capture(entryId: string): Promise<GitCheckpoint | undefined>;
14
+ lookup(entryId: string): Promise<GitCheckpoint | undefined>;
15
+ list(): Promise<readonly GitCheckpoint[]>;
16
+ /** Returns a checkpoint of the pre-restore state, which restores back through this same call. */
17
+ restore(checkpoint: GitCheckpoint): Promise<GitCheckpoint | undefined>;
18
+ /**
19
+ * True when the current worktree (tracked and untracked, ignored excluded)
20
+ * is tree-identical to the checkpoint; false when it moved; undefined when
21
+ * the comparison could not be performed.
22
+ */
23
+ matchesWorktree(checkpoint: GitCheckpoint): Promise<boolean | undefined>;
24
+ /** Deletes every ref this session owns, leaving other sessions' refs alone. */
25
+ prune(): Promise<void>;
26
+ }
27
+ export declare function createGitCheckpoints(workspace: string, sessionId: string, options?: GitCheckpointsOptions): Promise<GitCheckpoints | undefined>;
28
+ /**
29
+ * Opt-in configuration for the engine above.
30
+ *
31
+ * Absent by default, and an absent key constructs no engine, so an unconfigured session
32
+ * runs no `git` subprocess and writes no ref.
33
+ */
34
+ export interface CheckpointSettings {
35
+ enabled?: boolean;
36
+ /** Oldest refs beyond this count are deleted on capture. Defaults to 50. */
37
+ maxPerSession?: number;
38
+ }
39
+ //# sourceMappingURL=git-checkpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-checkpoints.d.ts","sourceRoot":"","sources":["../../../src/core/checkpoints/git-checkpoints.ts"],"names":[],"mappings":"AAKA,8FAA8F;AAC9F,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oFAAoF;IACpF,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IAC7D,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IAC5D,IAAI,IAAI,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAC;IAC1C,iGAAiG;IACjG,OAAO,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IACvE;;;;OAIG;IACH,eAAe,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzE,+EAA+E;IAC/E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAsFD,wBAAsB,oBAAoB,CACzC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAkLrC;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** One captured worktree state, reachable from a ref so garbage collection cannot reap it. */\nexport interface GitCheckpoint {\n\treadonly entryId: string;\n\treadonly commit: string;\n}\n\nexport interface GitCheckpointsOptions {\n\t/** Oldest refs beyond this count are deleted on capture. */\n\treadonly maxPerSession?: number;\n\t/** A capture that exceeds this is abandoned rather than allowed to stall a turn. */\n\treadonly timeoutMilliseconds?: number;\n}\n\nexport interface GitCheckpoints {\n\tcapture(entryId: string): Promise<GitCheckpoint | undefined>;\n\tlookup(entryId: string): Promise<GitCheckpoint | undefined>;\n\tlist(): Promise<readonly GitCheckpoint[]>;\n\t/** Returns a checkpoint of the pre-restore state, which restores back through this same call. */\n\trestore(checkpoint: GitCheckpoint): Promise<GitCheckpoint | undefined>;\n\t/**\n\t * True when the current worktree (tracked and untracked, ignored excluded)\n\t * is tree-identical to the checkpoint; false when it moved; undefined when\n\t * the comparison could not be performed.\n\t */\n\tmatchesWorktree(checkpoint: GitCheckpoint): Promise<boolean | undefined>;\n\t/** Deletes every ref this session owns, leaving other sessions' refs alone. */\n\tprune(): Promise<void>;\n}\n\nconst DEFAULT_MAX_PER_SESSION = 50;\nconst DEFAULT_TIMEOUT_MILLISECONDS = 15_000;\n\n/**\n * A fixed identity, never the user's.\n *\n * A checkpoint is harness bookkeeping rather than authorship, and `commit-tree` fails\n * outright when no identity resolves. Depending on the user's config would break exactly\n * where it matters most: a fresh clone, and a CI checkout, both of which configure none.\n */\nconst CHECKPOINT_IDENTITY = {\n\tGIT_AUTHOR_NAME: \"apex-code\",\n\tGIT_AUTHOR_EMAIL: \"checkpoints@apex-code.local\",\n\tGIT_COMMITTER_NAME: \"apex-code\",\n\tGIT_COMMITTER_EMAIL: \"checkpoints@apex-code.local\",\n} as const;\n\nconst SUBJECT_PREFIX = \"apex-code checkpoint\";\n\n/**\n * Applied to every invocation below, which is why it lives here rather than on the two\n * commands that read and write worktree content.\n *\n * `core.autocrlf` is a property of the machine, not of the repository, and it is `true` by\n * default on Windows. Left alone it decides what a checkpoint restores: capture and restore\n * both run content through git's clean and smudge filters, so a file the agent wrote with\n * LF endings comes back with CRLF and a restore silently rewrites line endings across the\n * worktree. Pinning it off makes the round trip byte-exact.\n *\n * `.gitattributes` is deliberately *not* overridden. That is the repository's own declared\n * policy, git applies it to every ordinary checkout, and a restore that ignored it would\n * disagree with `git checkout` on the same files.\n */\nconst IGNORE_MACHINE_EOL_PREFERENCE = [\"-c\", \"core.autocrlf=false\"];\n\ninterface GitResult {\n\treadonly stdout: string;\n\treadonly ok: boolean;\n}\n\nasync function runGit(\n\tcwd: string,\n\targs: string[],\n\tenvironment?: NodeJS.ProcessEnv,\n\ttimeout?: number,\n): Promise<GitResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", [...IGNORE_MACHINE_EOL_PREFERENCE, ...args], {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tenv: { ...process.env, ...environment },\n\t\t});\n\t\tlet stdout = \"\";\n\t\tlet settled = false;\n\t\tconst finish = (ok: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ stdout: stdout.trim(), ok });\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tchild.kill(\"SIGKILL\");\n\t\t\tfinish(false);\n\t\t}, timeout ?? DEFAULT_TIMEOUT_MILLISECONDS);\n\n\t\tchild.stdout?.on(\"data\", (chunk) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr?.resume();\n\t\tchild.on(\"error\", () => finish(false));\n\t\tchild.on(\"close\", (code) => finish(code === 0));\n\t});\n}\n\n/**\n * An entry id reaches this engine from a session file on disk, so it decides a ref name\n * that must not be able to escape the engine's own namespace. An allowlist is the check\n * that stays correct as git's own ref rules grow; a denylist of `..` and friends does not.\n */\nfunction isSafeRefComponent(value: string): boolean {\n\treturn /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes(\"..\") && !value.endsWith(\".lock\");\n}\n\nexport async function createGitCheckpoints(\n\tworkspace: string,\n\tsessionId: string,\n\toptions?: GitCheckpointsOptions,\n): Promise<GitCheckpoints | undefined> {\n\tif (!isSafeRefComponent(sessionId)) return undefined;\n\tif (!(await runGit(workspace, [\"rev-parse\", \"--git-dir\"])).ok) return undefined;\n\n\tconst maxPerSession = Math.max(1, options?.maxPerSession ?? DEFAULT_MAX_PER_SESSION);\n\tconst timeout = options?.timeoutMilliseconds ?? DEFAULT_TIMEOUT_MILLISECONDS;\n\tconst checkpointRef = (entryId: string) => `refs/apex-code/checkpoints/${sessionId}/${entryId}`;\n\tconst preRestoreNamespace = `refs/apex-code/pre-restore/${sessionId}`;\n\n\t/**\n\t * Write the worktree to a commit without touching anything the user can see.\n\t *\n\t * A private index is what makes that true: `add -A` against it stages tracked\n\t * modifications and untracked files, honours `.gitignore`, and leaves the real index\n\t * and the worktree byte-identical. `commit-tree` and `update-ref` are plumbing, so no\n\t * hook runs and the stash is never involved.\n\t */\n\tasync function snapshot(subject: string): Promise<string | undefined> {\n\t\tconst indexDirectory = await mkdtemp(join(tmpdir(), \"apex-checkpoint-index-\"));\n\t\tconst indexFile = join(indexDirectory, \"index\");\n\t\tconst environment = { GIT_INDEX_FILE: indexFile };\n\t\ttry {\n\t\t\t// Fails before the first commit, which is a supported state rather than an error.\n\t\t\tawait runGit(workspace, [\"read-tree\", \"HEAD\"], environment, timeout);\n\t\t\tif (!(await runGit(workspace, [\"add\", \"-A\"], environment, timeout)).ok) return undefined;\n\n\t\t\tconst tree = await runGit(workspace, [\"write-tree\"], environment, timeout);\n\t\t\tif (!tree.ok) return undefined;\n\n\t\t\tconst head = await runGit(workspace, [\"rev-parse\", \"HEAD\"]);\n\t\t\tconst parent = head.ok ? [\"-p\", head.stdout] : [];\n\t\t\tconst commit = await runGit(\n\t\t\t\tworkspace,\n\t\t\t\t[\"commit-tree\", tree.stdout, ...parent, \"-m\", subject],\n\t\t\t\t{ ...CHECKPOINT_IDENTITY, ...environment },\n\t\t\t\ttimeout,\n\t\t\t);\n\t\t\treturn commit.ok ? commit.stdout : undefined;\n\t\t} finally {\n\t\t\tawait rm(indexDirectory, { force: true, recursive: true });\n\t\t}\n\t}\n\n\tasync function refsUnder(namespace: string): Promise<Array<{ ref: string; commit: string; ordinal: number }>> {\n\t\tconst listed = await runGit(workspace, [\n\t\t\t\"for-each-ref\",\n\t\t\t\"--format=%(refname) %(objectname) %(contents:subject)\",\n\t\t\tnamespace,\n\t\t]);\n\t\tif (!listed.ok || listed.stdout === \"\") return [];\n\t\treturn listed.stdout\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => {\n\t\t\t\tconst [ref, commit, ...subject] = line.split(\" \");\n\t\t\t\treturn { ref: ref ?? \"\", commit: commit ?? \"\", ordinal: Number(subject.at(-1)) };\n\t\t\t})\n\t\t\t.filter((entry) => entry.ref !== \"\" && Number.isFinite(entry.ordinal))\n\t\t\t.sort((left, right) => left.ordinal - right.ordinal);\n\t}\n\n\t/**\n\t * Refs carry no creation order of their own, and `commit-tree` records a committer date\n\t * only to the second, so two captures inside one second would be indistinguishable.\n\t * The ordinal in the subject is the ordering, and it is derived from what is already on\n\t * disk so it survives a restart the same way the refs do.\n\t */\n\tasync function nextOrdinal(): Promise<number> {\n\t\tconst existing = [\n\t\t\t...(await refsUnder(`refs/apex-code/checkpoints/${sessionId}`)),\n\t\t\t...(await refsUnder(preRestoreNamespace)),\n\t\t];\n\t\t// The maximum across both namespaces, not the tail of the pair. `refsUnder` sorts\n\t\t// each list on its own, and two sorted lists concatenated are not sorted, so a\n\t\t// restore followed by further captures would otherwise reuse an ordinal already in\n\t\t// the other namespace and `bound` would prune by a broken order.\n\t\treturn existing.reduce((highest, entry) => Math.max(highest, entry.ordinal), 0) + 1;\n\t}\n\n\tasync function bound(namespace: string): Promise<void> {\n\t\tconst existing = await refsUnder(namespace);\n\t\tfor (const stale of existing.slice(0, Math.max(0, existing.length - maxPerSession))) {\n\t\t\tawait runGit(workspace, [\"update-ref\", \"-d\", stale.ref]);\n\t\t}\n\t}\n\n\tasync function restoreCommit(commit: string): Promise<boolean> {\n\t\tconst indexDirectory = await mkdtemp(join(tmpdir(), \"apex-checkpoint-restore-\"));\n\t\tconst indexFile = join(indexDirectory, \"index\");\n\t\tconst environment = { GIT_INDEX_FILE: indexFile };\n\t\ttry {\n\t\t\t// Seeding the private index from the *current* worktree is what lets\n\t\t\t// `read-tree -u` remove files created after the checkpoint. Seeded from HEAD\n\t\t\t// instead, those files would be untracked and would survive the restore, which\n\t\t\t// is the merge-not-restore defect this replaces.\n\t\t\tawait runGit(workspace, [\"read-tree\", \"HEAD\"], environment, timeout);\n\t\t\tawait runGit(workspace, [\"add\", \"-A\"], environment, timeout);\n\t\t\tif (!(await runGit(workspace, [\"read-tree\", \"-u\", \"--reset\", commit], environment, timeout)).ok) return false;\n\n\t\t\t// The real index goes back to HEAD without touching the worktree, so the\n\t\t\t// checkpoint's changes read as unstaged, which is the shape they had at capture.\n\t\t\tawait runGit(workspace, [\"read-tree\", \"HEAD\"], undefined, timeout);\n\t\t\treturn true;\n\t\t} finally {\n\t\t\tawait rm(indexDirectory, { force: true, recursive: true });\n\t\t}\n\t}\n\n\treturn {\n\t\tasync capture(entryId) {\n\t\t\tif (!isSafeRefComponent(entryId)) return undefined;\n\t\t\tconst ordinal = await nextOrdinal();\n\t\t\tconst commit = await snapshot(`${SUBJECT_PREFIX} ${ordinal}`);\n\t\t\tif (!commit) return undefined;\n\t\t\tif (!(await runGit(workspace, [\"update-ref\", checkpointRef(entryId), commit])).ok) return undefined;\n\t\t\tawait bound(`refs/apex-code/checkpoints/${sessionId}`);\n\t\t\treturn { entryId, commit };\n\t\t},\n\n\t\tasync lookup(entryId) {\n\t\t\tif (!isSafeRefComponent(entryId)) return undefined;\n\t\t\tconst resolved = await runGit(workspace, [\n\t\t\t\t\"rev-parse\",\n\t\t\t\t\"--verify\",\n\t\t\t\t\"--quiet\",\n\t\t\t\t`${checkpointRef(entryId)}^{commit}`,\n\t\t\t]);\n\t\t\treturn resolved.ok && resolved.stdout !== \"\" ? { entryId, commit: resolved.stdout } : undefined;\n\t\t},\n\n\t\tasync list() {\n\t\t\tconst prefix = `refs/apex-code/checkpoints/${sessionId}/`;\n\t\t\treturn (await refsUnder(`refs/apex-code/checkpoints/${sessionId}`)).map((entry) => ({\n\t\t\t\tentryId: entry.ref.slice(prefix.length),\n\t\t\t\tcommit: entry.commit,\n\t\t\t}));\n\t\t},\n\n\t\tasync matchesWorktree(checkpoint) {\n\t\t\tconst indexDirectory = await mkdtemp(join(tmpdir(), \"apex-checkpoint-compare-\"));\n\t\t\tconst indexFile = join(indexDirectory, \"index\");\n\t\t\tconst environment = { GIT_INDEX_FILE: indexFile };\n\t\t\ttry {\n\t\t\t\tconst commitTree = await runGit(workspace, [\"rev-parse\", `${checkpoint.commit}^{tree}`]);\n\t\t\t\tif (!commitTree.ok) return undefined;\n\t\t\t\tif (!(await runGit(workspace, [\"read-tree\", checkpoint.commit], environment, timeout)).ok) return undefined;\n\t\t\t\tif (!(await runGit(workspace, [\"add\", \"-A\"], environment, timeout)).ok) return undefined;\n\t\t\t\tconst worktreeTree = await runGit(workspace, [\"write-tree\"], environment, timeout);\n\t\t\t\tif (!worktreeTree.ok) return undefined;\n\t\t\t\treturn worktreeTree.stdout.trim() === commitTree.stdout.trim();\n\t\t\t} finally {\n\t\t\t\tawait rm(indexDirectory, { recursive: true, force: true }).catch(() => {});\n\t\t\t}\n\t\t},\n\n\t\tasync restore(checkpoint) {\n\t\t\tconst ordinal = await nextOrdinal();\n\t\t\tconst previous = await snapshot(`${SUBJECT_PREFIX} ${ordinal}`);\n\t\t\tif (!previous) return undefined;\n\t\t\t// Pinned before the restore runs, so an interrupted restore still leaves the\n\t\t\t// pre-restore state reachable rather than orphaned. The write is checked because\n\t\t\t// an unpinned snapshot is unreachable and collectable: proceeding past a failure\n\t\t\t// here would overwrite the worktree with no way back to what it held.\n\t\t\tif (!(await runGit(workspace, [\"update-ref\", `${preRestoreNamespace}/${ordinal}`, previous])).ok) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tawait bound(preRestoreNamespace);\n\t\t\tif (!(await restoreCommit(checkpoint.commit))) return undefined;\n\t\t\treturn { entryId: \"pre-restore\", commit: previous };\n\t\t},\n\n\t\tasync prune() {\n\t\t\tfor (const namespace of [`refs/apex-code/checkpoints/${sessionId}`, preRestoreNamespace]) {\n\t\t\t\tfor (const entry of await refsUnder(namespace)) {\n\t\t\t\t\tawait runGit(workspace, [\"update-ref\", \"-d\", entry.ref]);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Opt-in configuration for the engine above.\n *\n * Absent by default, and an absent key constructs no engine, so an unconfigured session\n * runs no `git` subprocess and writes no ref.\n */\nexport interface CheckpointSettings {\n\tenabled?: boolean;\n\t/** Oldest refs beyond this count are deleted on capture. Defaults to 50. */\n\tmaxPerSession?: number;\n}\n"]}
@@ -0,0 +1,251 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ const DEFAULT_MAX_PER_SESSION = 50;
6
+ const DEFAULT_TIMEOUT_MILLISECONDS = 15_000;
7
+ /**
8
+ * A fixed identity, never the user's.
9
+ *
10
+ * A checkpoint is harness bookkeeping rather than authorship, and `commit-tree` fails
11
+ * outright when no identity resolves. Depending on the user's config would break exactly
12
+ * where it matters most: a fresh clone, and a CI checkout, both of which configure none.
13
+ */
14
+ const CHECKPOINT_IDENTITY = {
15
+ GIT_AUTHOR_NAME: "apex-code",
16
+ GIT_AUTHOR_EMAIL: "checkpoints@apex-code.local",
17
+ GIT_COMMITTER_NAME: "apex-code",
18
+ GIT_COMMITTER_EMAIL: "checkpoints@apex-code.local",
19
+ };
20
+ const SUBJECT_PREFIX = "apex-code checkpoint";
21
+ /**
22
+ * Applied to every invocation below, which is why it lives here rather than on the two
23
+ * commands that read and write worktree content.
24
+ *
25
+ * `core.autocrlf` is a property of the machine, not of the repository, and it is `true` by
26
+ * default on Windows. Left alone it decides what a checkpoint restores: capture and restore
27
+ * both run content through git's clean and smudge filters, so a file the agent wrote with
28
+ * LF endings comes back with CRLF and a restore silently rewrites line endings across the
29
+ * worktree. Pinning it off makes the round trip byte-exact.
30
+ *
31
+ * `.gitattributes` is deliberately *not* overridden. That is the repository's own declared
32
+ * policy, git applies it to every ordinary checkout, and a restore that ignored it would
33
+ * disagree with `git checkout` on the same files.
34
+ */
35
+ const IGNORE_MACHINE_EOL_PREFERENCE = ["-c", "core.autocrlf=false"];
36
+ async function runGit(cwd, args, environment, timeout) {
37
+ return new Promise((resolve) => {
38
+ const child = spawn("git", [...IGNORE_MACHINE_EOL_PREFERENCE, ...args], {
39
+ cwd,
40
+ shell: false,
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ env: { ...process.env, ...environment },
43
+ });
44
+ let stdout = "";
45
+ let settled = false;
46
+ const finish = (ok) => {
47
+ if (settled)
48
+ return;
49
+ settled = true;
50
+ clearTimeout(timer);
51
+ resolve({ stdout: stdout.trim(), ok });
52
+ };
53
+ const timer = setTimeout(() => {
54
+ child.kill("SIGKILL");
55
+ finish(false);
56
+ }, timeout ?? DEFAULT_TIMEOUT_MILLISECONDS);
57
+ child.stdout?.on("data", (chunk) => {
58
+ stdout += chunk;
59
+ });
60
+ child.stderr?.resume();
61
+ child.on("error", () => finish(false));
62
+ child.on("close", (code) => finish(code === 0));
63
+ });
64
+ }
65
+ /**
66
+ * An entry id reaches this engine from a session file on disk, so it decides a ref name
67
+ * that must not be able to escape the engine's own namespace. An allowlist is the check
68
+ * that stays correct as git's own ref rules grow; a denylist of `..` and friends does not.
69
+ */
70
+ function isSafeRefComponent(value) {
71
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes("..") && !value.endsWith(".lock");
72
+ }
73
+ export async function createGitCheckpoints(workspace, sessionId, options) {
74
+ if (!isSafeRefComponent(sessionId))
75
+ return undefined;
76
+ if (!(await runGit(workspace, ["rev-parse", "--git-dir"])).ok)
77
+ return undefined;
78
+ const maxPerSession = Math.max(1, options?.maxPerSession ?? DEFAULT_MAX_PER_SESSION);
79
+ const timeout = options?.timeoutMilliseconds ?? DEFAULT_TIMEOUT_MILLISECONDS;
80
+ const checkpointRef = (entryId) => `refs/apex-code/checkpoints/${sessionId}/${entryId}`;
81
+ const preRestoreNamespace = `refs/apex-code/pre-restore/${sessionId}`;
82
+ /**
83
+ * Write the worktree to a commit without touching anything the user can see.
84
+ *
85
+ * A private index is what makes that true: `add -A` against it stages tracked
86
+ * modifications and untracked files, honours `.gitignore`, and leaves the real index
87
+ * and the worktree byte-identical. `commit-tree` and `update-ref` are plumbing, so no
88
+ * hook runs and the stash is never involved.
89
+ */
90
+ async function snapshot(subject) {
91
+ const indexDirectory = await mkdtemp(join(tmpdir(), "apex-checkpoint-index-"));
92
+ const indexFile = join(indexDirectory, "index");
93
+ const environment = { GIT_INDEX_FILE: indexFile };
94
+ try {
95
+ // Fails before the first commit, which is a supported state rather than an error.
96
+ await runGit(workspace, ["read-tree", "HEAD"], environment, timeout);
97
+ if (!(await runGit(workspace, ["add", "-A"], environment, timeout)).ok)
98
+ return undefined;
99
+ const tree = await runGit(workspace, ["write-tree"], environment, timeout);
100
+ if (!tree.ok)
101
+ return undefined;
102
+ const head = await runGit(workspace, ["rev-parse", "HEAD"]);
103
+ const parent = head.ok ? ["-p", head.stdout] : [];
104
+ const commit = await runGit(workspace, ["commit-tree", tree.stdout, ...parent, "-m", subject], { ...CHECKPOINT_IDENTITY, ...environment }, timeout);
105
+ return commit.ok ? commit.stdout : undefined;
106
+ }
107
+ finally {
108
+ await rm(indexDirectory, { force: true, recursive: true });
109
+ }
110
+ }
111
+ async function refsUnder(namespace) {
112
+ const listed = await runGit(workspace, [
113
+ "for-each-ref",
114
+ "--format=%(refname) %(objectname) %(contents:subject)",
115
+ namespace,
116
+ ]);
117
+ if (!listed.ok || listed.stdout === "")
118
+ return [];
119
+ return listed.stdout
120
+ .split("\n")
121
+ .map((line) => {
122
+ const [ref, commit, ...subject] = line.split(" ");
123
+ return { ref: ref ?? "", commit: commit ?? "", ordinal: Number(subject.at(-1)) };
124
+ })
125
+ .filter((entry) => entry.ref !== "" && Number.isFinite(entry.ordinal))
126
+ .sort((left, right) => left.ordinal - right.ordinal);
127
+ }
128
+ /**
129
+ * Refs carry no creation order of their own, and `commit-tree` records a committer date
130
+ * only to the second, so two captures inside one second would be indistinguishable.
131
+ * The ordinal in the subject is the ordering, and it is derived from what is already on
132
+ * disk so it survives a restart the same way the refs do.
133
+ */
134
+ async function nextOrdinal() {
135
+ const existing = [
136
+ ...(await refsUnder(`refs/apex-code/checkpoints/${sessionId}`)),
137
+ ...(await refsUnder(preRestoreNamespace)),
138
+ ];
139
+ // The maximum across both namespaces, not the tail of the pair. `refsUnder` sorts
140
+ // each list on its own, and two sorted lists concatenated are not sorted, so a
141
+ // restore followed by further captures would otherwise reuse an ordinal already in
142
+ // the other namespace and `bound` would prune by a broken order.
143
+ return existing.reduce((highest, entry) => Math.max(highest, entry.ordinal), 0) + 1;
144
+ }
145
+ async function bound(namespace) {
146
+ const existing = await refsUnder(namespace);
147
+ for (const stale of existing.slice(0, Math.max(0, existing.length - maxPerSession))) {
148
+ await runGit(workspace, ["update-ref", "-d", stale.ref]);
149
+ }
150
+ }
151
+ async function restoreCommit(commit) {
152
+ const indexDirectory = await mkdtemp(join(tmpdir(), "apex-checkpoint-restore-"));
153
+ const indexFile = join(indexDirectory, "index");
154
+ const environment = { GIT_INDEX_FILE: indexFile };
155
+ try {
156
+ // Seeding the private index from the *current* worktree is what lets
157
+ // `read-tree -u` remove files created after the checkpoint. Seeded from HEAD
158
+ // instead, those files would be untracked and would survive the restore, which
159
+ // is the merge-not-restore defect this replaces.
160
+ await runGit(workspace, ["read-tree", "HEAD"], environment, timeout);
161
+ await runGit(workspace, ["add", "-A"], environment, timeout);
162
+ if (!(await runGit(workspace, ["read-tree", "-u", "--reset", commit], environment, timeout)).ok)
163
+ return false;
164
+ // The real index goes back to HEAD without touching the worktree, so the
165
+ // checkpoint's changes read as unstaged, which is the shape they had at capture.
166
+ await runGit(workspace, ["read-tree", "HEAD"], undefined, timeout);
167
+ return true;
168
+ }
169
+ finally {
170
+ await rm(indexDirectory, { force: true, recursive: true });
171
+ }
172
+ }
173
+ return {
174
+ async capture(entryId) {
175
+ if (!isSafeRefComponent(entryId))
176
+ return undefined;
177
+ const ordinal = await nextOrdinal();
178
+ const commit = await snapshot(`${SUBJECT_PREFIX} ${ordinal}`);
179
+ if (!commit)
180
+ return undefined;
181
+ if (!(await runGit(workspace, ["update-ref", checkpointRef(entryId), commit])).ok)
182
+ return undefined;
183
+ await bound(`refs/apex-code/checkpoints/${sessionId}`);
184
+ return { entryId, commit };
185
+ },
186
+ async lookup(entryId) {
187
+ if (!isSafeRefComponent(entryId))
188
+ return undefined;
189
+ const resolved = await runGit(workspace, [
190
+ "rev-parse",
191
+ "--verify",
192
+ "--quiet",
193
+ `${checkpointRef(entryId)}^{commit}`,
194
+ ]);
195
+ return resolved.ok && resolved.stdout !== "" ? { entryId, commit: resolved.stdout } : undefined;
196
+ },
197
+ async list() {
198
+ const prefix = `refs/apex-code/checkpoints/${sessionId}/`;
199
+ return (await refsUnder(`refs/apex-code/checkpoints/${sessionId}`)).map((entry) => ({
200
+ entryId: entry.ref.slice(prefix.length),
201
+ commit: entry.commit,
202
+ }));
203
+ },
204
+ async matchesWorktree(checkpoint) {
205
+ const indexDirectory = await mkdtemp(join(tmpdir(), "apex-checkpoint-compare-"));
206
+ const indexFile = join(indexDirectory, "index");
207
+ const environment = { GIT_INDEX_FILE: indexFile };
208
+ try {
209
+ const commitTree = await runGit(workspace, ["rev-parse", `${checkpoint.commit}^{tree}`]);
210
+ if (!commitTree.ok)
211
+ return undefined;
212
+ if (!(await runGit(workspace, ["read-tree", checkpoint.commit], environment, timeout)).ok)
213
+ return undefined;
214
+ if (!(await runGit(workspace, ["add", "-A"], environment, timeout)).ok)
215
+ return undefined;
216
+ const worktreeTree = await runGit(workspace, ["write-tree"], environment, timeout);
217
+ if (!worktreeTree.ok)
218
+ return undefined;
219
+ return worktreeTree.stdout.trim() === commitTree.stdout.trim();
220
+ }
221
+ finally {
222
+ await rm(indexDirectory, { recursive: true, force: true }).catch(() => { });
223
+ }
224
+ },
225
+ async restore(checkpoint) {
226
+ const ordinal = await nextOrdinal();
227
+ const previous = await snapshot(`${SUBJECT_PREFIX} ${ordinal}`);
228
+ if (!previous)
229
+ return undefined;
230
+ // Pinned before the restore runs, so an interrupted restore still leaves the
231
+ // pre-restore state reachable rather than orphaned. The write is checked because
232
+ // an unpinned snapshot is unreachable and collectable: proceeding past a failure
233
+ // here would overwrite the worktree with no way back to what it held.
234
+ if (!(await runGit(workspace, ["update-ref", `${preRestoreNamespace}/${ordinal}`, previous])).ok) {
235
+ return undefined;
236
+ }
237
+ await bound(preRestoreNamespace);
238
+ if (!(await restoreCommit(checkpoint.commit)))
239
+ return undefined;
240
+ return { entryId: "pre-restore", commit: previous };
241
+ },
242
+ async prune() {
243
+ for (const namespace of [`refs/apex-code/checkpoints/${sessionId}`, preRestoreNamespace]) {
244
+ for (const entry of await refsUnder(namespace)) {
245
+ await runGit(workspace, ["update-ref", "-d", entry.ref]);
246
+ }
247
+ }
248
+ },
249
+ };
250
+ }
251
+ //# sourceMappingURL=git-checkpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-checkpoints.js","sourceRoot":"","sources":["../../../src/core/checkpoints/git-checkpoints.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** One captured worktree state, reachable from a ref so garbage collection cannot reap it. */\nexport interface GitCheckpoint {\n\treadonly entryId: string;\n\treadonly commit: string;\n}\n\nexport interface GitCheckpointsOptions {\n\t/** Oldest refs beyond this count are deleted on capture. */\n\treadonly maxPerSession?: number;\n\t/** A capture that exceeds this is abandoned rather than allowed to stall a turn. */\n\treadonly timeoutMilliseconds?: number;\n}\n\nexport interface GitCheckpoints {\n\tcapture(entryId: string): Promise<GitCheckpoint | undefined>;\n\tlookup(entryId: string): Promise<GitCheckpoint | undefined>;\n\tlist(): Promise<readonly GitCheckpoint[]>;\n\t/** Returns a checkpoint of the pre-restore state, which restores back through this same call. */\n\trestore(checkpoint: GitCheckpoint): Promise<GitCheckpoint | undefined>;\n\t/**\n\t * True when the current worktree (tracked and untracked, ignored excluded)\n\t * is tree-identical to the checkpoint; false when it moved; undefined when\n\t * the comparison could not be performed.\n\t */\n\tmatchesWorktree(checkpoint: GitCheckpoint): Promise<boolean | undefined>;\n\t/** Deletes every ref this session owns, leaving other sessions' refs alone. */\n\tprune(): Promise<void>;\n}\n\nconst DEFAULT_MAX_PER_SESSION = 50;\nconst DEFAULT_TIMEOUT_MILLISECONDS = 15_000;\n\n/**\n * A fixed identity, never the user's.\n *\n * A checkpoint is harness bookkeeping rather than authorship, and `commit-tree` fails\n * outright when no identity resolves. Depending on the user's config would break exactly\n * where it matters most: a fresh clone, and a CI checkout, both of which configure none.\n */\nconst CHECKPOINT_IDENTITY = {\n\tGIT_AUTHOR_NAME: \"apex-code\",\n\tGIT_AUTHOR_EMAIL: \"checkpoints@apex-code.local\",\n\tGIT_COMMITTER_NAME: \"apex-code\",\n\tGIT_COMMITTER_EMAIL: \"checkpoints@apex-code.local\",\n} as const;\n\nconst SUBJECT_PREFIX = \"apex-code checkpoint\";\n\n/**\n * Applied to every invocation below, which is why it lives here rather than on the two\n * commands that read and write worktree content.\n *\n * `core.autocrlf` is a property of the machine, not of the repository, and it is `true` by\n * default on Windows. Left alone it decides what a checkpoint restores: capture and restore\n * both run content through git's clean and smudge filters, so a file the agent wrote with\n * LF endings comes back with CRLF and a restore silently rewrites line endings across the\n * worktree. Pinning it off makes the round trip byte-exact.\n *\n * `.gitattributes` is deliberately *not* overridden. That is the repository's own declared\n * policy, git applies it to every ordinary checkout, and a restore that ignored it would\n * disagree with `git checkout` on the same files.\n */\nconst IGNORE_MACHINE_EOL_PREFERENCE = [\"-c\", \"core.autocrlf=false\"];\n\ninterface GitResult {\n\treadonly stdout: string;\n\treadonly ok: boolean;\n}\n\nasync function runGit(\n\tcwd: string,\n\targs: string[],\n\tenvironment?: NodeJS.ProcessEnv,\n\ttimeout?: number,\n): Promise<GitResult> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", [...IGNORE_MACHINE_EOL_PREFERENCE, ...args], {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tenv: { ...process.env, ...environment },\n\t\t});\n\t\tlet stdout = \"\";\n\t\tlet settled = false;\n\t\tconst finish = (ok: boolean) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tclearTimeout(timer);\n\t\t\tresolve({ stdout: stdout.trim(), ok });\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tchild.kill(\"SIGKILL\");\n\t\t\tfinish(false);\n\t\t}, timeout ?? DEFAULT_TIMEOUT_MILLISECONDS);\n\n\t\tchild.stdout?.on(\"data\", (chunk) => {\n\t\t\tstdout += chunk;\n\t\t});\n\t\tchild.stderr?.resume();\n\t\tchild.on(\"error\", () => finish(false));\n\t\tchild.on(\"close\", (code) => finish(code === 0));\n\t});\n}\n\n/**\n * An entry id reaches this engine from a session file on disk, so it decides a ref name\n * that must not be able to escape the engine's own namespace. An allowlist is the check\n * that stays correct as git's own ref rules grow; a denylist of `..` and friends does not.\n */\nfunction isSafeRefComponent(value: string): boolean {\n\treturn /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes(\"..\") && !value.endsWith(\".lock\");\n}\n\nexport async function createGitCheckpoints(\n\tworkspace: string,\n\tsessionId: string,\n\toptions?: GitCheckpointsOptions,\n): Promise<GitCheckpoints | undefined> {\n\tif (!isSafeRefComponent(sessionId)) return undefined;\n\tif (!(await runGit(workspace, [\"rev-parse\", \"--git-dir\"])).ok) return undefined;\n\n\tconst maxPerSession = Math.max(1, options?.maxPerSession ?? DEFAULT_MAX_PER_SESSION);\n\tconst timeout = options?.timeoutMilliseconds ?? DEFAULT_TIMEOUT_MILLISECONDS;\n\tconst checkpointRef = (entryId: string) => `refs/apex-code/checkpoints/${sessionId}/${entryId}`;\n\tconst preRestoreNamespace = `refs/apex-code/pre-restore/${sessionId}`;\n\n\t/**\n\t * Write the worktree to a commit without touching anything the user can see.\n\t *\n\t * A private index is what makes that true: `add -A` against it stages tracked\n\t * modifications and untracked files, honours `.gitignore`, and leaves the real index\n\t * and the worktree byte-identical. `commit-tree` and `update-ref` are plumbing, so no\n\t * hook runs and the stash is never involved.\n\t */\n\tasync function snapshot(subject: string): Promise<string | undefined> {\n\t\tconst indexDirectory = await mkdtemp(join(tmpdir(), \"apex-checkpoint-index-\"));\n\t\tconst indexFile = join(indexDirectory, \"index\");\n\t\tconst environment = { GIT_INDEX_FILE: indexFile };\n\t\ttry {\n\t\t\t// Fails before the first commit, which is a supported state rather than an error.\n\t\t\tawait runGit(workspace, [\"read-tree\", \"HEAD\"], environment, timeout);\n\t\t\tif (!(await runGit(workspace, [\"add\", \"-A\"], environment, timeout)).ok) return undefined;\n\n\t\t\tconst tree = await runGit(workspace, [\"write-tree\"], environment, timeout);\n\t\t\tif (!tree.ok) return undefined;\n\n\t\t\tconst head = await runGit(workspace, [\"rev-parse\", \"HEAD\"]);\n\t\t\tconst parent = head.ok ? [\"-p\", head.stdout] : [];\n\t\t\tconst commit = await runGit(\n\t\t\t\tworkspace,\n\t\t\t\t[\"commit-tree\", tree.stdout, ...parent, \"-m\", subject],\n\t\t\t\t{ ...CHECKPOINT_IDENTITY, ...environment },\n\t\t\t\ttimeout,\n\t\t\t);\n\t\t\treturn commit.ok ? commit.stdout : undefined;\n\t\t} finally {\n\t\t\tawait rm(indexDirectory, { force: true, recursive: true });\n\t\t}\n\t}\n\n\tasync function refsUnder(namespace: string): Promise<Array<{ ref: string; commit: string; ordinal: number }>> {\n\t\tconst listed = await runGit(workspace, [\n\t\t\t\"for-each-ref\",\n\t\t\t\"--format=%(refname) %(objectname) %(contents:subject)\",\n\t\t\tnamespace,\n\t\t]);\n\t\tif (!listed.ok || listed.stdout === \"\") return [];\n\t\treturn listed.stdout\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => {\n\t\t\t\tconst [ref, commit, ...subject] = line.split(\" \");\n\t\t\t\treturn { ref: ref ?? \"\", commit: commit ?? \"\", ordinal: Number(subject.at(-1)) };\n\t\t\t})\n\t\t\t.filter((entry) => entry.ref !== \"\" && Number.isFinite(entry.ordinal))\n\t\t\t.sort((left, right) => left.ordinal - right.ordinal);\n\t}\n\n\t/**\n\t * Refs carry no creation order of their own, and `commit-tree` records a committer date\n\t * only to the second, so two captures inside one second would be indistinguishable.\n\t * The ordinal in the subject is the ordering, and it is derived from what is already on\n\t * disk so it survives a restart the same way the refs do.\n\t */\n\tasync function nextOrdinal(): Promise<number> {\n\t\tconst existing = [\n\t\t\t...(await refsUnder(`refs/apex-code/checkpoints/${sessionId}`)),\n\t\t\t...(await refsUnder(preRestoreNamespace)),\n\t\t];\n\t\t// The maximum across both namespaces, not the tail of the pair. `refsUnder` sorts\n\t\t// each list on its own, and two sorted lists concatenated are not sorted, so a\n\t\t// restore followed by further captures would otherwise reuse an ordinal already in\n\t\t// the other namespace and `bound` would prune by a broken order.\n\t\treturn existing.reduce((highest, entry) => Math.max(highest, entry.ordinal), 0) + 1;\n\t}\n\n\tasync function bound(namespace: string): Promise<void> {\n\t\tconst existing = await refsUnder(namespace);\n\t\tfor (const stale of existing.slice(0, Math.max(0, existing.length - maxPerSession))) {\n\t\t\tawait runGit(workspace, [\"update-ref\", \"-d\", stale.ref]);\n\t\t}\n\t}\n\n\tasync function restoreCommit(commit: string): Promise<boolean> {\n\t\tconst indexDirectory = await mkdtemp(join(tmpdir(), \"apex-checkpoint-restore-\"));\n\t\tconst indexFile = join(indexDirectory, \"index\");\n\t\tconst environment = { GIT_INDEX_FILE: indexFile };\n\t\ttry {\n\t\t\t// Seeding the private index from the *current* worktree is what lets\n\t\t\t// `read-tree -u` remove files created after the checkpoint. Seeded from HEAD\n\t\t\t// instead, those files would be untracked and would survive the restore, which\n\t\t\t// is the merge-not-restore defect this replaces.\n\t\t\tawait runGit(workspace, [\"read-tree\", \"HEAD\"], environment, timeout);\n\t\t\tawait runGit(workspace, [\"add\", \"-A\"], environment, timeout);\n\t\t\tif (!(await runGit(workspace, [\"read-tree\", \"-u\", \"--reset\", commit], environment, timeout)).ok) return false;\n\n\t\t\t// The real index goes back to HEAD without touching the worktree, so the\n\t\t\t// checkpoint's changes read as unstaged, which is the shape they had at capture.\n\t\t\tawait runGit(workspace, [\"read-tree\", \"HEAD\"], undefined, timeout);\n\t\t\treturn true;\n\t\t} finally {\n\t\t\tawait rm(indexDirectory, { force: true, recursive: true });\n\t\t}\n\t}\n\n\treturn {\n\t\tasync capture(entryId) {\n\t\t\tif (!isSafeRefComponent(entryId)) return undefined;\n\t\t\tconst ordinal = await nextOrdinal();\n\t\t\tconst commit = await snapshot(`${SUBJECT_PREFIX} ${ordinal}`);\n\t\t\tif (!commit) return undefined;\n\t\t\tif (!(await runGit(workspace, [\"update-ref\", checkpointRef(entryId), commit])).ok) return undefined;\n\t\t\tawait bound(`refs/apex-code/checkpoints/${sessionId}`);\n\t\t\treturn { entryId, commit };\n\t\t},\n\n\t\tasync lookup(entryId) {\n\t\t\tif (!isSafeRefComponent(entryId)) return undefined;\n\t\t\tconst resolved = await runGit(workspace, [\n\t\t\t\t\"rev-parse\",\n\t\t\t\t\"--verify\",\n\t\t\t\t\"--quiet\",\n\t\t\t\t`${checkpointRef(entryId)}^{commit}`,\n\t\t\t]);\n\t\t\treturn resolved.ok && resolved.stdout !== \"\" ? { entryId, commit: resolved.stdout } : undefined;\n\t\t},\n\n\t\tasync list() {\n\t\t\tconst prefix = `refs/apex-code/checkpoints/${sessionId}/`;\n\t\t\treturn (await refsUnder(`refs/apex-code/checkpoints/${sessionId}`)).map((entry) => ({\n\t\t\t\tentryId: entry.ref.slice(prefix.length),\n\t\t\t\tcommit: entry.commit,\n\t\t\t}));\n\t\t},\n\n\t\tasync matchesWorktree(checkpoint) {\n\t\t\tconst indexDirectory = await mkdtemp(join(tmpdir(), \"apex-checkpoint-compare-\"));\n\t\t\tconst indexFile = join(indexDirectory, \"index\");\n\t\t\tconst environment = { GIT_INDEX_FILE: indexFile };\n\t\t\ttry {\n\t\t\t\tconst commitTree = await runGit(workspace, [\"rev-parse\", `${checkpoint.commit}^{tree}`]);\n\t\t\t\tif (!commitTree.ok) return undefined;\n\t\t\t\tif (!(await runGit(workspace, [\"read-tree\", checkpoint.commit], environment, timeout)).ok) return undefined;\n\t\t\t\tif (!(await runGit(workspace, [\"add\", \"-A\"], environment, timeout)).ok) return undefined;\n\t\t\t\tconst worktreeTree = await runGit(workspace, [\"write-tree\"], environment, timeout);\n\t\t\t\tif (!worktreeTree.ok) return undefined;\n\t\t\t\treturn worktreeTree.stdout.trim() === commitTree.stdout.trim();\n\t\t\t} finally {\n\t\t\t\tawait rm(indexDirectory, { recursive: true, force: true }).catch(() => {});\n\t\t\t}\n\t\t},\n\n\t\tasync restore(checkpoint) {\n\t\t\tconst ordinal = await nextOrdinal();\n\t\t\tconst previous = await snapshot(`${SUBJECT_PREFIX} ${ordinal}`);\n\t\t\tif (!previous) return undefined;\n\t\t\t// Pinned before the restore runs, so an interrupted restore still leaves the\n\t\t\t// pre-restore state reachable rather than orphaned. The write is checked because\n\t\t\t// an unpinned snapshot is unreachable and collectable: proceeding past a failure\n\t\t\t// here would overwrite the worktree with no way back to what it held.\n\t\t\tif (!(await runGit(workspace, [\"update-ref\", `${preRestoreNamespace}/${ordinal}`, previous])).ok) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tawait bound(preRestoreNamespace);\n\t\t\tif (!(await restoreCommit(checkpoint.commit))) return undefined;\n\t\t\treturn { entryId: \"pre-restore\", commit: previous };\n\t\t},\n\n\t\tasync prune() {\n\t\t\tfor (const namespace of [`refs/apex-code/checkpoints/${sessionId}`, preRestoreNamespace]) {\n\t\t\t\tfor (const entry of await refsUnder(namespace)) {\n\t\t\t\t\tawait runGit(workspace, [\"update-ref\", \"-d\", entry.ref]);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Opt-in configuration for the engine above.\n *\n * Absent by default, and an absent key constructs no engine, so an unconfigured session\n * runs no `git` subprocess and writes no ref.\n */\nexport interface CheckpointSettings {\n\tenabled?: boolean;\n\t/** Oldest refs beyond this count are deleted on capture. Defaults to 50. */\n\tmaxPerSession?: number;\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import { type CheckpointSettings, type GitCheckpoint, type GitCheckpoints } from "./git-checkpoints.ts";
2
+ export interface SessionCheckpointsOptions {
3
+ readonly workspace: string;
4
+ readonly sessionId: string;
5
+ readonly settings: CheckpointSettings | undefined;
6
+ }
7
+ export interface SessionCheckpoints {
8
+ /** Snapshot the worktree against one session entry. Undefined whenever checkpoints are off or unavailable. */
9
+ capture(entryId: string): Promise<GitCheckpoint | undefined>;
10
+ /** The engine, once resolved. Undefined until the first capture, and whenever checkpoints are off. */
11
+ engine(): Promise<GitCheckpoints | undefined>;
12
+ }
13
+ /**
14
+ * The session's view of the checkpoint engine.
15
+ *
16
+ * Resolution is lazy and happens at most once. Two things force that. The engine is
17
+ * async because it shells out to `git rev-parse` to decide whether a repository is even
18
+ * there, and session construction is not; and the workspace may not be a repository at
19
+ * all, which is a supported state rather than an error, so nothing may throw out of here
20
+ * into a turn.
21
+ */
22
+ export declare function createSessionCheckpoints(options: SessionCheckpointsOptions): SessionCheckpoints;
23
+ //# sourceMappingURL=session-checkpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-checkpoints.d.ts","sourceRoot":"","sources":["../../../src/core/checkpoints/session-checkpoints.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,kBAAkB,EAEvB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,GAAG,SAAS,CAAC;CAClD;AAED,MAAM,WAAW,kBAAkB;IAClC,8GAA8G;IAC9G,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IAC7D,sGAAsG;IACtG,MAAM,IAAI,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;CAC9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAyB/F","sourcesContent":["import {\n\ttype CheckpointSettings,\n\tcreateGitCheckpoints,\n\ttype GitCheckpoint,\n\ttype GitCheckpoints,\n} from \"./git-checkpoints.ts\";\n\nexport interface SessionCheckpointsOptions {\n\treadonly workspace: string;\n\treadonly sessionId: string;\n\treadonly settings: CheckpointSettings | undefined;\n}\n\nexport interface SessionCheckpoints {\n\t/** Snapshot the worktree against one session entry. Undefined whenever checkpoints are off or unavailable. */\n\tcapture(entryId: string): Promise<GitCheckpoint | undefined>;\n\t/** The engine, once resolved. Undefined until the first capture, and whenever checkpoints are off. */\n\tengine(): Promise<GitCheckpoints | undefined>;\n}\n\n/**\n * The session's view of the checkpoint engine.\n *\n * Resolution is lazy and happens at most once. Two things force that. The engine is\n * async because it shells out to `git rev-parse` to decide whether a repository is even\n * there, and session construction is not; and the workspace may not be a repository at\n * all, which is a supported state rather than an error, so nothing may throw out of here\n * into a turn.\n */\nexport function createSessionCheckpoints(options: SessionCheckpointsOptions): SessionCheckpoints {\n\t/**\n\t * Absent settings mean ON (spec 2026-09-01-checkpoints-default-on.md): the\n\t * rewind safety net must not require discovering a settings key. The explicit\n\t * opt-out is `{ enabled: false }`. Outside a git repository the engine\n\t * resolves to undefined -- one swallowed `git rev-parse` at the first capture\n\t * attempt -- so a non-repo workspace stays a supported no-subsystem state.\n\t */\n\tconst enabled = options.settings?.enabled !== false;\n\tlet resolved: Promise<GitCheckpoints | undefined> | undefined;\n\n\tconst engine = (): Promise<GitCheckpoints | undefined> => {\n\t\tif (!enabled) return Promise.resolve(undefined);\n\t\tresolved ??= createGitCheckpoints(options.workspace, options.sessionId, {\n\t\t\tmaxPerSession: options.settings?.maxPerSession,\n\t\t}).catch(() => undefined);\n\t\treturn resolved;\n\t};\n\n\treturn {\n\t\tengine,\n\t\tasync capture(entryId) {\n\t\t\treturn (await engine())?.capture(entryId);\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { createGitCheckpoints, } from "./git-checkpoints.js";
2
+ /**
3
+ * The session's view of the checkpoint engine.
4
+ *
5
+ * Resolution is lazy and happens at most once. Two things force that. The engine is
6
+ * async because it shells out to `git rev-parse` to decide whether a repository is even
7
+ * there, and session construction is not; and the workspace may not be a repository at
8
+ * all, which is a supported state rather than an error, so nothing may throw out of here
9
+ * into a turn.
10
+ */
11
+ export function createSessionCheckpoints(options) {
12
+ /**
13
+ * Absent settings mean ON (spec 2026-09-01-checkpoints-default-on.md): the
14
+ * rewind safety net must not require discovering a settings key. The explicit
15
+ * opt-out is `{ enabled: false }`. Outside a git repository the engine
16
+ * resolves to undefined -- one swallowed `git rev-parse` at the first capture
17
+ * attempt -- so a non-repo workspace stays a supported no-subsystem state.
18
+ */
19
+ const enabled = options.settings?.enabled !== false;
20
+ let resolved;
21
+ const engine = () => {
22
+ if (!enabled)
23
+ return Promise.resolve(undefined);
24
+ resolved ??= createGitCheckpoints(options.workspace, options.sessionId, {
25
+ maxPerSession: options.settings?.maxPerSession,
26
+ }).catch(() => undefined);
27
+ return resolved;
28
+ };
29
+ return {
30
+ engine,
31
+ async capture(entryId) {
32
+ return (await engine())?.capture(entryId);
33
+ },
34
+ };
35
+ }
36
+ //# sourceMappingURL=session-checkpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-checkpoints.js","sourceRoot":"","sources":["../../../src/core/checkpoints/session-checkpoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,oBAAoB,GAGpB,MAAM,sBAAsB,CAAC;AAe9B;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAkC,EAAsB;IAChG;;;;;;OAMG;IACH,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC;IACpD,IAAI,QAAyD,CAAC;IAE9D,MAAM,MAAM,GAAG,GAAwC,EAAE,CAAC;QACzD,IAAI,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChD,QAAQ,KAAK,oBAAoB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE;YACvE,aAAa,EAAE,OAAO,CAAC,QAAQ,EAAE,aAAa;SAC9C,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC1B,OAAO,QAAQ,CAAC;IAAA,CAChB,CAAC;IAEF,OAAO;QACN,MAAM;QACN,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE;YACtB,OAAO,CAAC,MAAM,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAAA,CAC1C;KACD,CAAC;AAAA,CACF","sourcesContent":["import {\n\ttype CheckpointSettings,\n\tcreateGitCheckpoints,\n\ttype GitCheckpoint,\n\ttype GitCheckpoints,\n} from \"./git-checkpoints.ts\";\n\nexport interface SessionCheckpointsOptions {\n\treadonly workspace: string;\n\treadonly sessionId: string;\n\treadonly settings: CheckpointSettings | undefined;\n}\n\nexport interface SessionCheckpoints {\n\t/** Snapshot the worktree against one session entry. Undefined whenever checkpoints are off or unavailable. */\n\tcapture(entryId: string): Promise<GitCheckpoint | undefined>;\n\t/** The engine, once resolved. Undefined until the first capture, and whenever checkpoints are off. */\n\tengine(): Promise<GitCheckpoints | undefined>;\n}\n\n/**\n * The session's view of the checkpoint engine.\n *\n * Resolution is lazy and happens at most once. Two things force that. The engine is\n * async because it shells out to `git rev-parse` to decide whether a repository is even\n * there, and session construction is not; and the workspace may not be a repository at\n * all, which is a supported state rather than an error, so nothing may throw out of here\n * into a turn.\n */\nexport function createSessionCheckpoints(options: SessionCheckpointsOptions): SessionCheckpoints {\n\t/**\n\t * Absent settings mean ON (spec 2026-09-01-checkpoints-default-on.md): the\n\t * rewind safety net must not require discovering a settings key. The explicit\n\t * opt-out is `{ enabled: false }`. Outside a git repository the engine\n\t * resolves to undefined -- one swallowed `git rev-parse` at the first capture\n\t * attempt -- so a non-repo workspace stays a supported no-subsystem state.\n\t */\n\tconst enabled = options.settings?.enabled !== false;\n\tlet resolved: Promise<GitCheckpoints | undefined> | undefined;\n\n\tconst engine = (): Promise<GitCheckpoints | undefined> => {\n\t\tif (!enabled) return Promise.resolve(undefined);\n\t\tresolved ??= createGitCheckpoints(options.workspace, options.sessionId, {\n\t\t\tmaxPerSession: options.settings?.maxPerSession,\n\t\t}).catch(() => undefined);\n\t\treturn resolved;\n\t};\n\n\treturn {\n\t\tengine,\n\t\tasync capture(entryId) {\n\t\t\treturn (await engine())?.capture(entryId);\n\t\t},\n\t};\n}\n"]}