@cursor/july 0.1.1 → 0.1.3

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 (298) hide show
  1. package/AGENTS.md +40 -7
  2. package/README.md +33 -24
  3. package/dist/bin/agent-serve.d.ts +3 -1
  4. package/dist/bin/agent-serve.d.ts.map +1 -1
  5. package/dist/bin/agent-serve.js +466 -140
  6. package/dist/channels/github/api.d.ts.map +1 -1
  7. package/dist/channels/github/api.js +31 -14
  8. package/dist/channels/github/cursor-account.d.ts +43 -0
  9. package/dist/channels/github/cursor-account.d.ts.map +1 -0
  10. package/dist/channels/github/cursor-account.js +95 -0
  11. package/dist/channels/github/github-channel.d.ts.map +1 -1
  12. package/dist/channels/github/github-channel.js +46 -9
  13. package/dist/channels/github/index.d.ts +2 -2
  14. package/dist/channels/github/index.js +2 -2
  15. package/dist/channels/github/types.d.ts +17 -0
  16. package/dist/channels/github/types.d.ts.map +1 -1
  17. package/dist/channels/slack/slack-channel.d.ts +8 -2
  18. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  19. package/dist/channels/slack/slack-channel.js +8 -0
  20. package/dist/channels/slack/types.d.ts +24 -3
  21. package/dist/channels/slack/types.d.ts.map +1 -1
  22. package/dist/channels/slack/types.js +15 -1
  23. package/dist/docs/404.html +2 -2
  24. package/dist/docs/ab.html +8 -8
  25. package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
  26. package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
  27. package/dist/docs/assets/{app.DqfFEmJd.js → app.BR0RbIdx.js} +1 -1
  28. package/dist/docs/assets/chunks/@localSearchIndexroot.DtIOQzGj.js +1 -0
  29. package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.qjsVTneI.js} +1 -1
  30. package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.2Kg8jIp_.js} +2 -2
  31. package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
  32. package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
  33. package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
  34. package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
  35. package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
  36. package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
  37. package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
  38. package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
  39. package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
  40. package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
  41. package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
  42. package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
  43. package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
  44. package/dist/docs/assets/quickstart.md.BU6Iwi_9.js +204 -0
  45. package/dist/docs/assets/quickstart.md.BU6Iwi_9.lean.js +1 -0
  46. package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
  47. package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
  48. package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
  49. package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
  50. package/dist/docs/assets/{reference_cli.md.DA730zCu.js → reference_cli.md.Bv6pOxcF.js} +11 -6
  51. package/dist/docs/assets/{reference_cli.md.DA730zCu.lean.js → reference_cli.md.Bv6pOxcF.lean.js} +1 -1
  52. package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
  53. package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
  54. package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
  55. package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
  56. package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
  57. package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
  58. package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
  59. package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
  60. package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
  61. package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
  62. package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
  63. package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
  64. package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
  65. package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
  66. package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
  67. package/dist/docs/building-with-agents.html +4 -4
  68. package/dist/docs/concepts.html +4 -4
  69. package/dist/docs/deployment.html +7 -7
  70. package/dist/docs/evals.html +7 -7
  71. package/dist/docs/guides/agent-to-agent.html +6 -6
  72. package/dist/docs/guides/cloud-runtime.html +5 -5
  73. package/dist/docs/guides/github.html +14 -7
  74. package/dist/docs/guides/human-in-the-loop.html +6 -6
  75. package/dist/docs/guides/slack.html +8 -8
  76. package/dist/docs/guides/webhooks.html +6 -6
  77. package/dist/docs/hashmap.json +1 -1
  78. package/dist/docs/hillclimbing.html +5 -5
  79. package/dist/docs/index.html +7 -7
  80. package/dist/docs/quickstart.html +195 -26
  81. package/dist/docs/reference/agent-config.html +7 -7
  82. package/dist/docs/reference/channels.html +8 -8
  83. package/dist/docs/reference/cli.html +14 -9
  84. package/dist/docs/reference/connections.html +5 -5
  85. package/dist/docs/reference/hooks.html +5 -5
  86. package/dist/docs/reference/http-api.html +6 -6
  87. package/dist/docs/reference/instructions.html +13 -11
  88. package/dist/docs/reference/playground.html +4 -4
  89. package/dist/docs/reference/project-layout.html +4 -4
  90. package/dist/docs/reference/schedules.html +7 -7
  91. package/dist/docs/reference/sessions.html +4 -4
  92. package/dist/docs/reference/skills.html +6 -6
  93. package/dist/docs/reference/subagents.html +6 -6
  94. package/dist/docs/reference/tools.html +7 -7
  95. package/dist/docs/scaffolding-agents.html +5 -5
  96. package/dist/docs/storage.html +41 -0
  97. package/dist/docs/troubleshooting.html +4 -4
  98. package/dist/index.d.ts +2 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1 -0
  101. package/dist/internal/chat-client.d.ts +29 -3
  102. package/dist/internal/chat-client.d.ts.map +1 -1
  103. package/dist/internal/chat-client.js +180 -27
  104. package/dist/internal/cli-ax.d.ts +75 -2
  105. package/dist/internal/cli-ax.d.ts.map +1 -1
  106. package/dist/internal/cli-ax.js +600 -52
  107. package/dist/internal/cli-cursor.d.ts.map +1 -1
  108. package/dist/internal/cli-cursor.js +8 -36
  109. package/dist/internal/cli-deploy.d.ts +108 -0
  110. package/dist/internal/cli-deploy.d.ts.map +1 -0
  111. package/dist/internal/cli-deploy.js +1009 -0
  112. package/dist/internal/cursor/backend-client.d.ts +10 -1
  113. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  114. package/dist/internal/cursor/backend-client.js +79 -1
  115. package/dist/internal/cursor/credentials.d.ts +9 -0
  116. package/dist/internal/cursor/credentials.d.ts.map +1 -1
  117. package/dist/internal/cursor/credentials.js +12 -0
  118. package/dist/internal/cursor/github-credentials.d.ts +44 -0
  119. package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
  120. package/dist/internal/cursor/github-credentials.js +195 -0
  121. package/dist/internal/deploy-client.d.ts +184 -0
  122. package/dist/internal/deploy-client.d.ts.map +1 -0
  123. package/dist/internal/deploy-client.js +395 -0
  124. package/dist/internal/deploy-source.d.ts +35 -0
  125. package/dist/internal/deploy-source.d.ts.map +1 -0
  126. package/dist/internal/deploy-source.js +118 -0
  127. package/dist/internal/discovery.d.ts +11 -0
  128. package/dist/internal/discovery.d.ts.map +1 -1
  129. package/dist/internal/discovery.js +116 -19
  130. package/dist/internal/distribution.d.ts.map +1 -1
  131. package/dist/internal/distribution.js +1 -0
  132. package/dist/internal/eval-run-store.d.ts +22 -3
  133. package/dist/internal/eval-run-store.d.ts.map +1 -1
  134. package/dist/internal/eval-run-store.js +39 -20
  135. package/dist/internal/eval-runner.d.ts +2 -0
  136. package/dist/internal/eval-runner.d.ts.map +1 -1
  137. package/dist/internal/eval-runner.js +2 -0
  138. package/dist/internal/event-mapper.d.ts +54 -1
  139. package/dist/internal/event-mapper.d.ts.map +1 -1
  140. package/dist/internal/event-mapper.js +151 -41
  141. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  142. package/dist/internal/handleAgentServeTrigger.js +10 -12
  143. package/dist/internal/host-platforms.d.ts +7 -2
  144. package/dist/internal/host-platforms.d.ts.map +1 -1
  145. package/dist/internal/host-platforms.js +15 -10
  146. package/dist/internal/hosting.d.ts +37 -0
  147. package/dist/internal/hosting.d.ts.map +1 -0
  148. package/dist/internal/hosting.js +67 -0
  149. package/dist/internal/init-project.d.ts +88 -1
  150. package/dist/internal/init-project.d.ts.map +1 -1
  151. package/dist/internal/init-project.js +221 -31
  152. package/dist/internal/install-cursor-skills.d.ts +64 -0
  153. package/dist/internal/install-cursor-skills.d.ts.map +1 -0
  154. package/dist/internal/install-cursor-skills.js +274 -0
  155. package/dist/internal/logs-client.d.ts +60 -0
  156. package/dist/internal/logs-client.d.ts.map +1 -0
  157. package/dist/internal/logs-client.js +311 -0
  158. package/dist/internal/open-browser.d.ts +5 -0
  159. package/dist/internal/open-browser.d.ts.map +1 -0
  160. package/dist/internal/open-browser.js +34 -0
  161. package/dist/internal/playground/toolchain.d.ts.map +1 -1
  162. package/dist/internal/playground/toolchain.js +16 -11
  163. package/dist/internal/playground-proxy.d.ts +69 -0
  164. package/dist/internal/playground-proxy.d.ts.map +1 -0
  165. package/dist/internal/playground-proxy.js +468 -0
  166. package/dist/internal/prompt-context.d.ts +35 -0
  167. package/dist/internal/prompt-context.d.ts.map +1 -0
  168. package/dist/internal/prompt-context.js +71 -0
  169. package/dist/internal/reminder-runner.d.ts +7 -0
  170. package/dist/internal/reminder-runner.d.ts.map +1 -1
  171. package/dist/internal/reminder-runner.js +50 -6
  172. package/dist/internal/reminder-store.d.ts +2 -0
  173. package/dist/internal/reminder-store.d.ts.map +1 -1
  174. package/dist/internal/reminder-store.js +18 -0
  175. package/dist/internal/request-headers.d.ts +11 -0
  176. package/dist/internal/request-headers.d.ts.map +1 -0
  177. package/dist/internal/request-headers.js +14 -0
  178. package/dist/internal/resolve-prod-target.d.ts +42 -0
  179. package/dist/internal/resolve-prod-target.d.ts.map +1 -0
  180. package/dist/internal/resolve-prod-target.js +111 -0
  181. package/dist/internal/run-client.d.ts +2 -2
  182. package/dist/internal/run-client.d.ts.map +1 -1
  183. package/dist/internal/run-client.js +38 -23
  184. package/dist/internal/server.d.ts.map +1 -1
  185. package/dist/internal/server.js +182 -45
  186. package/dist/internal/session-engine.d.ts +50 -1
  187. package/dist/internal/session-engine.d.ts.map +1 -1
  188. package/dist/internal/session-engine.js +267 -23
  189. package/dist/internal/sessions-client.d.ts +21 -0
  190. package/dist/internal/sessions-client.d.ts.map +1 -0
  191. package/dist/internal/sessions-client.js +168 -0
  192. package/dist/internal/storage-coordinator.d.ts +139 -0
  193. package/dist/internal/storage-coordinator.d.ts.map +1 -0
  194. package/dist/internal/storage-coordinator.js +499 -0
  195. package/dist/internal/stream-progress.d.ts.map +1 -1
  196. package/dist/internal/stream-progress.js +31 -3
  197. package/dist/internal/terminal-style.d.ts +22 -0
  198. package/dist/internal/terminal-style.d.ts.map +1 -0
  199. package/dist/internal/terminal-style.js +49 -0
  200. package/dist/internal/trajectory.d.ts +10 -5
  201. package/dist/internal/trajectory.d.ts.map +1 -1
  202. package/dist/internal/trajectory.js +82 -10
  203. package/dist/internal/workspace.d.ts +11 -0
  204. package/dist/internal/workspace.d.ts.map +1 -1
  205. package/dist/internal/workspace.js +38 -4
  206. package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
  207. package/dist/playground/assets/index-D-vo2lV_.css +1 -0
  208. package/dist/playground/assets/index-x60b9q2j.js +312 -0
  209. package/dist/playground/index.html +2 -2
  210. package/dist/storage.d.ts +204 -0
  211. package/dist/storage.d.ts.map +1 -0
  212. package/dist/storage.js +153 -0
  213. package/dist/types.d.ts +70 -4
  214. package/dist/types.d.ts.map +1 -1
  215. package/docs/README.md +3 -2
  216. package/docs/guides/github.md +43 -8
  217. package/docs/guides/slack.md +1 -1
  218. package/docs/quickstart.md +350 -57
  219. package/docs/reference/cli.md +51 -3
  220. package/docs/reference/instructions.md +8 -6
  221. package/docs/scaffolding-agents.md +1 -1
  222. package/docs/storage.md +98 -0
  223. package/package.json +10 -1
  224. package/skills/ab/SKILL.md +8 -8
  225. package/skills/create-agent/SKILL.md +28 -22
  226. package/skills/debug/SKILL.md +11 -11
  227. package/skills/evals/SKILL.md +16 -16
  228. package/skills/framework-map/SKILL.md +6 -6
  229. package/skills/github/SKILL.md +22 -14
  230. package/skills/hillclimb/SKILL.md +10 -10
  231. package/skills/setup-slack/SKILL.md +13 -13
  232. package/src/bin/agent-serve.ts +552 -179
  233. package/src/channels/github/api.ts +42 -23
  234. package/src/channels/github/cursor-account.ts +165 -0
  235. package/src/channels/github/github-channel.ts +66 -6
  236. package/src/channels/github/index.ts +2 -2
  237. package/src/channels/github/types.ts +19 -0
  238. package/src/channels/slack/slack-channel.ts +17 -3
  239. package/src/channels/slack/types.ts +44 -3
  240. package/src/index.ts +13 -0
  241. package/src/internal/chat-client.ts +252 -37
  242. package/src/internal/cli-ax.ts +722 -72
  243. package/src/internal/cli-cursor.ts +14 -42
  244. package/src/internal/cli-deploy.ts +1319 -0
  245. package/src/internal/cursor/backend-client.ts +103 -1
  246. package/src/internal/cursor/credentials.ts +14 -0
  247. package/src/internal/cursor/github-credentials.ts +248 -0
  248. package/src/internal/deploy-client.ts +632 -0
  249. package/src/internal/deploy-source.ts +133 -0
  250. package/src/internal/discovery.ts +141 -17
  251. package/src/internal/distribution.ts +1 -0
  252. package/src/internal/eval-run-store.ts +50 -20
  253. package/src/internal/eval-runner.ts +5 -0
  254. package/src/internal/event-mapper.ts +222 -42
  255. package/src/internal/handleAgentServeTrigger.ts +10 -12
  256. package/src/internal/host-platforms.ts +28 -11
  257. package/src/internal/hosting.ts +77 -0
  258. package/src/internal/init-project.ts +333 -51
  259. package/src/internal/install-cursor-skills.ts +327 -0
  260. package/src/internal/logs-client.ts +442 -0
  261. package/src/internal/open-browser.ts +41 -0
  262. package/src/internal/playground/toolchain.ts +15 -13
  263. package/src/internal/playground-proxy.ts +576 -0
  264. package/src/internal/prompt-context.ts +95 -0
  265. package/src/internal/reminder-runner.ts +50 -6
  266. package/src/internal/reminder-store.ts +21 -0
  267. package/src/internal/request-headers.ts +23 -0
  268. package/src/internal/resolve-prod-target.ts +150 -0
  269. package/src/internal/run-client.ts +39 -36
  270. package/src/internal/server.ts +223 -28
  271. package/src/internal/session-engine.ts +307 -10
  272. package/src/internal/sessions-client.ts +182 -0
  273. package/src/internal/storage-coordinator.ts +615 -0
  274. package/src/internal/stream-progress.ts +36 -2
  275. package/src/internal/terminal-style.ts +74 -0
  276. package/src/internal/trajectory.ts +91 -13
  277. package/src/internal/workspace.ts +42 -4
  278. package/src/storage.ts +325 -0
  279. package/src/types.ts +82 -8
  280. package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
  281. package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
  282. package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
  283. package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
  284. package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
  285. package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
  286. package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
  287. package/dist/playground/assets/index-1K-hG-7p.css +0 -1
  288. package/dist/playground/assets/index-FlWjhg3x.js +0 -79
  289. /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
  290. /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
  291. /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
  292. /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
  293. /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
  294. /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
  295. /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
  296. /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
  297. /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
  298. /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
@@ -7,8 +7,8 @@
7
7
  * event handlers and hooks.
8
8
  */
9
9
 
10
- import { mkdir, rm } from "node:fs/promises";
11
- import { join } from "node:path";
10
+ import { mkdir, rm, writeFile } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
12
  import type { SDKCustomTool } from "@cursor/sdk";
13
13
  import {
14
14
  type ABDefinition,
@@ -54,6 +54,7 @@ import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
54
54
  import { ApprovalGate } from "./approval-gate.js";
55
55
  import { samePrincipal } from "./auth.js";
56
56
  import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
57
+ import { isDevMode } from "./dev-mode.js";
57
58
  import { EventLogStore } from "./event-log.js";
58
59
  import { TurnEventMapper } from "./event-mapper.js";
59
60
  import { createHostPlatforms, type HostPlatforms } from "./host-platforms.js";
@@ -65,9 +66,15 @@ import {
65
66
  } from "./ids.js";
66
67
  import { validateToolInput } from "./json-schema.js";
67
68
  import { McpHost } from "./mcp-host.js";
69
+ import {
70
+ buildIdentitySection,
71
+ buildSessionContextBlock,
72
+ SCHEDULE_CHANNEL_PREFIX,
73
+ } from "./prompt-context.js";
68
74
  import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
69
75
  import type { AgentRunner } from "./sdk-runner.js";
70
76
  import { SessionStore } from "./session-store.js";
77
+ import { StorageCoordinator } from "./storage-coordinator.js";
71
78
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
72
79
  import {
73
80
  buildAgentToolsCatalog,
@@ -76,7 +83,7 @@ import {
76
83
  } from "./workspace.js";
77
84
 
78
85
  export const HTTP_CHANNEL_ID = "http";
79
- export const SCHEDULE_CHANNEL_PREFIX = "schedule:";
86
+ export { SCHEDULE_CHANNEL_PREFIX } from "./prompt-context.js";
80
87
 
81
88
  export class SessionBusyError extends Error {
82
89
  constructor(sessionId: string) {
@@ -157,6 +164,7 @@ export interface SessionEngineOptions {
157
164
  stateRoot: string;
158
165
  runner: AgentRunner;
159
166
  logger?: (line: string) => void;
167
+ platforms?: HostPlatforms;
160
168
  }
161
169
 
162
170
  /**
@@ -192,6 +200,21 @@ export class SessionEngine {
192
200
  readonly project: AgentProject;
193
201
  readonly stateRoot: string;
194
202
  readonly sessions: SessionStore;
203
+ /**
204
+ * Runtime for `agent/storage.ts` (`defineStorage`), when
205
+ * authored. All durable state changes funnel through it (session
206
+ * records, event chunks, A/B samples/snapshots); the eval run store
207
+ * attaches via {@link StorageCoordinator.asEvalRunPersistence} at
208
+ * serve start.
209
+ */
210
+ readonly storage: StorageCoordinator | undefined;
211
+ /** In-flight lazy restores, deduped per channel + continuation key. */
212
+ private readonly restoreInFlight = new Map<
213
+ string,
214
+ Promise<SessionRecord | undefined>
215
+ >();
216
+ /** Once-per-process cache for the A/B snapshot backfill (see abSnapshot). */
217
+ private restoredAbSnapshot: Promise<ABSnapshot | undefined> | undefined;
195
218
 
196
219
  private readonly runner: AgentRunner;
197
220
  private readonly logger: (line: string) => void;
@@ -229,15 +252,26 @@ export class SessionEngine {
229
252
  `[agent-serve] failed to persist events for ${sessionId}: ${describeError(error)}`
230
253
  )
231
254
  );
255
+ this.storage =
256
+ options.project.storage === undefined
257
+ ? undefined
258
+ : new StorageCoordinator({
259
+ definition: options.project.storage,
260
+ agentName: options.project.name,
261
+ projectRoot: options.project.rootDir,
262
+ logger: this.logger,
263
+ });
264
+ // Domain-specific hooks win; the defineStorage sink is the fallback.
265
+ const persistSamples =
266
+ options.project.abConfig?.persistSamples ??
267
+ this.storage?.asABSamplePersistence();
232
268
  this.abCollector = new ABCollector(
233
269
  options.project.abs,
234
270
  this.logger,
235
271
  async (sessionId) => (await this.logs.get(sessionId)).snapshot(0),
236
272
  {
237
273
  projectRoot: options.project.rootDir,
238
- ...(options.project.abConfig?.persistSamples === undefined
239
- ? {}
240
- : { persistSamples: options.project.abConfig.persistSamples }),
274
+ persistSamples,
241
275
  }
242
276
  );
243
277
  // Peer and Cursor-account transports are symbolic until the serve host
@@ -248,7 +282,7 @@ export class SessionEngine {
248
282
  (connection) => !isSymbolicConnectionTransport(connection.transport)
249
283
  )
250
284
  );
251
- this.platforms = createHostPlatforms();
285
+ this.platforms = options.platforms ?? createHostPlatforms();
252
286
  for (const channel of options.project.channels) {
253
287
  this.channelsById.set(channel.id, channel.definition);
254
288
  }
@@ -305,13 +339,21 @@ export class SessionEngine {
305
339
  options: EngineSendOptions = {}
306
340
  ): Promise<ChannelSession> {
307
341
  const auth = options.auth ?? null;
308
- const existing =
342
+ let existing =
309
343
  options.continuationToken === undefined
310
344
  ? undefined
311
345
  : await this.sessions.findByContinuation(
312
346
  channelId,
313
347
  options.continuationToken
314
348
  );
349
+ if (existing === undefined && options.continuationToken !== undefined) {
350
+ // Local miss: the session may live only in the storage sink (it
351
+ // fell outside the startup restore window, or restore is lazy-only).
352
+ existing = await this.restoreSessionByContinuation(
353
+ channelId,
354
+ options.continuationToken
355
+ );
356
+ }
315
357
 
316
358
  if (existing !== undefined) {
317
359
  if (!samePrincipal(existing.auth, auth)) {
@@ -361,6 +403,7 @@ export class SessionEngine {
361
403
  updatedAt: now,
362
404
  };
363
405
  await this.sessions.save(record);
406
+ this.storage?.sessionRecord(record);
364
407
  await this.appendEvent(record.sessionId, {
365
408
  type: "session.started",
366
409
  data: { channelId },
@@ -707,7 +750,10 @@ export class SessionEngine {
707
750
  // events while sibling parallel tools are still executing.
708
751
  if (
709
752
  update.type === "tool-call-started" ||
710
- update.type === "tool-call-completed"
753
+ update.type === "tool-call-completed" ||
754
+ // Nested subagent progress (newer SDKs); string check keeps
755
+ // typecheck green against older published `@cursor/sdk` pins.
756
+ (update as { type: string }).type === "tool-call-delta"
711
757
  ) {
712
758
  await yieldEventLoop();
713
759
  }
@@ -810,14 +856,24 @@ export class SessionEngine {
810
856
  })
811
857
  : undefined;
812
858
  let turnPrompt = prompt;
859
+ if (isFirstTurn) {
860
+ const contextBlock = buildSessionContextBlock({
861
+ sessionId: record.sessionId,
862
+ channelId: record.channelId,
863
+ channelKind: this.channelsById.get(record.channelId)?.meta?.kind,
864
+ environment: isDevMode() ? "local dev server" : "hosted deployment",
865
+ });
866
+ turnPrompt = `${contextBlock}\n\n${prompt}`;
867
+ }
813
868
  if (runtime === "cloud" && isFirstTurn) {
814
869
  const preamble = [
815
870
  agent.instructions?.trim() ?? "",
871
+ buildIdentitySection(agent),
816
872
  buildAgentToolsCatalog(agent.tools, { includeScripts: true }),
817
873
  ]
818
874
  .filter((part) => part !== "")
819
875
  .join("\n\n");
820
- turnPrompt = preamble === "" ? prompt : `${preamble}\n\n${prompt}`;
876
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
821
877
  }
822
878
  return {
823
879
  sessionId: record.sessionId,
@@ -1337,6 +1393,9 @@ export class SessionEngine {
1337
1393
  }
1338
1394
 
1339
1395
  private queueDispatch(sessionId: string, event: SessionEvent): void {
1396
+ // Single funnel for both append paths (appendEvent + the held-log emit),
1397
+ // so the storage sink sees every durable event exactly once.
1398
+ this.storage?.event(event);
1340
1399
  const chain = this.dispatchChains.get(sessionId) ?? Promise.resolve();
1341
1400
  const next = chain
1342
1401
  .then(() => this.dispatchEvent(sessionId, event))
@@ -1344,6 +1403,12 @@ export class SessionEngine {
1344
1403
  this.logger(
1345
1404
  `[agent-serve] event dispatch failed for ${sessionId}: ${describeError(error)}`
1346
1405
  );
1406
+ })
1407
+ .then(() => {
1408
+ // Turn-end flush happens here — after the boundary event's handlers
1409
+ // and their record updates settled — so the persisted record carries
1410
+ // the turn's final channel state and continuation token.
1411
+ this.storage?.eventDispatched(event);
1347
1412
  });
1348
1413
  this.dispatchChains.set(sessionId, next);
1349
1414
  }
@@ -1508,6 +1573,25 @@ export class SessionEngine {
1508
1573
  abConfig?.maxPlaygroundSessions
1509
1574
  );
1510
1575
  const foldedSessions = sessions.slice(0, maxPlaygroundSessions);
1576
+ // Backfill for a replacement host with nothing local to fold (e.g.
1577
+ // lazy-only restore before any session resumed): serve the persisted
1578
+ // aggregate instead of an empty surface. Gated on the host having no
1579
+ // sessions AT ALL — the persisted snapshot spans every principal, so a
1580
+ // caller who merely owns no sessions must not see it. Loaded once per
1581
+ // process (playground polls this route ~every 4s); skip the persist
1582
+ // side effect below — re-saving a restored snapshot would echo stale
1583
+ // data back into the store.
1584
+ if (
1585
+ foldedSessions.length === 0 &&
1586
+ this.storage !== undefined &&
1587
+ (await this.sessions.list()).length === 0
1588
+ ) {
1589
+ this.restoredAbSnapshot ??= this.storage.getLatestAbSnapshot();
1590
+ const restoredSnapshot = await this.restoredAbSnapshot;
1591
+ if (restoredSnapshot !== undefined) {
1592
+ return restoredSnapshot;
1593
+ }
1594
+ }
1511
1595
  const snapshot = await buildABSnapshot({
1512
1596
  experiments: this.project.abs,
1513
1597
  agentName: this.project.name,
@@ -1535,6 +1619,9 @@ export class SessionEngine {
1535
1619
  `[agent-serve] ab persistSnapshots.save threw: ${describeError(error)}`
1536
1620
  );
1537
1621
  }
1622
+ } else {
1623
+ // defineStorage fallback (throttled inside the coordinator).
1624
+ this.storage?.abSnapshot(snapshot);
1538
1625
  }
1539
1626
  return snapshot;
1540
1627
  }
@@ -1700,6 +1787,7 @@ export class SessionEngine {
1700
1787
  updatedAt: new Date().toISOString(),
1701
1788
  };
1702
1789
  await this.sessions.save(updated);
1790
+ this.storage?.sessionRecord(updated);
1703
1791
  return updated;
1704
1792
  });
1705
1793
  this.updateChains.set(
@@ -1713,6 +1801,203 @@ export class SessionEngine {
1713
1801
  // Lifecycle
1714
1802
  // ==========================================================================
1715
1803
 
1804
+ /**
1805
+ * Restore sessions from the authored storage sink. Called once at
1806
+ * serve start, before channels accept traffic: any persisted session
1807
+ * missing from the local `--state-root` is materialized there — events
1808
+ * as `events.ndjson`, record as `session.json` — after which every
1809
+ * existing read path (continuation lookup, event replay/streams, A/B
1810
+ * fold hydration) works unchanged. Local state always wins over the
1811
+ * sink, so restarts on the same host are no-ops.
1812
+ *
1813
+ * Failures are logged and skipped; a broken store (including rows that
1814
+ * violate the SessionRecord shape) never blocks serve start.
1815
+ */
1816
+ async restoreStoredSessions(): Promise<void> {
1817
+ const storage = this.storage;
1818
+ if (storage === undefined || !storage.canBulkRestore) {
1819
+ return;
1820
+ }
1821
+ const policy = storage.policy.restore;
1822
+ if (policy === "off") {
1823
+ return;
1824
+ }
1825
+ try {
1826
+ await this.runBulkRestore(storage, policy);
1827
+ } catch (error) {
1828
+ this.logger(
1829
+ `[agent-serve] session restore failed: ${describeError(error)}`
1830
+ );
1831
+ }
1832
+ }
1833
+
1834
+ private async runBulkRestore(
1835
+ storage: StorageCoordinator,
1836
+ policy: { maxSessions: number; maxAgeMs: number; maxTotalBytes: number }
1837
+ ): Promise<void> {
1838
+ const loaded = await storage.listSessions();
1839
+ // Guardrails on whatever the sink returned: drop invalid rows and
1840
+ // sessions older than the age window, then restore newest-first under
1841
+ // the count cap. Anything filtered out here stays reachable through
1842
+ // the lazy continuation-lookup path.
1843
+ const cutoff = Date.now() - policy.maxAgeMs;
1844
+ const candidates = loaded
1845
+ .map((record) => ({
1846
+ record,
1847
+ // Numeric timestamp for both the age filter and the sort; a
1848
+ // non-string updatedAt (e.g. a raw DB Date from an untyped sink)
1849
+ // parses to NaN and is dropped instead of crashing serve start.
1850
+ updatedAt:
1851
+ typeof record?.updatedAt === "string"
1852
+ ? Date.parse(record.updatedAt)
1853
+ : Number.NaN,
1854
+ }))
1855
+ .filter(
1856
+ ({ record, updatedAt }) =>
1857
+ typeof record?.sessionId === "string" &&
1858
+ record.sessionId !== "" &&
1859
+ updatedAt >= cutoff
1860
+ )
1861
+ .sort((a, b) => b.updatedAt - a.updatedAt)
1862
+ .slice(0, policy.maxSessions)
1863
+ .map(({ record }) => record);
1864
+
1865
+ let restored = 0;
1866
+ let budget = policy.maxTotalBytes;
1867
+ // Sink round trips dominate restore latency, so event streams are
1868
+ // fetched with bounded parallelism; writes stay sequential, newest
1869
+ // first, so the byte budget is enforced deterministically.
1870
+ outer: for (
1871
+ let i = 0;
1872
+ i < candidates.length;
1873
+ i += RESTORE_FETCH_CONCURRENCY
1874
+ ) {
1875
+ const chunk = candidates.slice(i, i + RESTORE_FETCH_CONCURRENCY);
1876
+ const fetched = await Promise.all(
1877
+ chunk.map(async (record) => {
1878
+ try {
1879
+ // Local state always wins over the sink.
1880
+ if ((await this.sessions.get(record.sessionId)) !== undefined) {
1881
+ return undefined;
1882
+ }
1883
+ const events = await storage.listSessionEvents(record.sessionId);
1884
+ return { record, events };
1885
+ } catch (error) {
1886
+ this.logger(
1887
+ `[agent-serve] failed to restore session ${record.sessionId}: ${describeError(error)}`
1888
+ );
1889
+ return undefined;
1890
+ }
1891
+ })
1892
+ );
1893
+ for (const item of fetched) {
1894
+ if (item === undefined) {
1895
+ continue;
1896
+ }
1897
+ const ndjson = eventsNdjson(item.events);
1898
+ // Budget is checked BEFORE anything is written: one oversized
1899
+ // session cannot blow past maxTotalBytes on disk. Skipped sessions
1900
+ // stay reachable through the lazy path.
1901
+ const bytes =
1902
+ Buffer.byteLength(JSON.stringify(item.record), "utf8") +
1903
+ (ndjson === undefined ? 0 : Buffer.byteLength(ndjson, "utf8"));
1904
+ if (bytes > budget) {
1905
+ this.logger(
1906
+ `[agent-serve] session restore stopped at the ${policy.maxTotalBytes}-byte budget after ${restored} session(s); remaining sessions restore lazily on resume`
1907
+ );
1908
+ break outer;
1909
+ }
1910
+ try {
1911
+ await this.materializeSession(item.record, ndjson);
1912
+ budget -= bytes;
1913
+ restored += 1;
1914
+ } catch (error) {
1915
+ this.logger(
1916
+ `[agent-serve] failed to restore session ${item.record.sessionId}: ${describeError(error)}`
1917
+ );
1918
+ }
1919
+ }
1920
+ }
1921
+ if (restored > 0) {
1922
+ this.logger(`[agent-serve] restored ${restored} session(s) from storage`);
1923
+ }
1924
+ }
1925
+
1926
+ /**
1927
+ * Lazy restore: a continuation lookup missed locally, so ask the sink
1928
+ * for that one session and materialize it before resuming. Disk grows
1929
+ * with sessions that are actually resumed, not with history.
1930
+ *
1931
+ * Concurrent misses for the same key (webhook redeliveries, retries)
1932
+ * share one in-flight restore — a second materialization racing the
1933
+ * first resumed turn could truncate events the turn already appended.
1934
+ * Sink errors propagate and fail the send (see
1935
+ * {@link StorageCoordinator.getSessionByContinuation}).
1936
+ */
1937
+ private restoreSessionByContinuation(
1938
+ channelId: string,
1939
+ continuationKey: string
1940
+ ): Promise<SessionRecord | undefined> {
1941
+ const storage = this.storage;
1942
+ if (storage === undefined) {
1943
+ return Promise.resolve(undefined);
1944
+ }
1945
+ const key = `${channelId}\u0000${continuationKey}`;
1946
+ let pending = this.restoreInFlight.get(key);
1947
+ if (pending === undefined) {
1948
+ pending = this.loadAndMaterializeByContinuation(
1949
+ storage,
1950
+ channelId,
1951
+ continuationKey
1952
+ ).finally(() => this.restoreInFlight.delete(key));
1953
+ this.restoreInFlight.set(key, pending);
1954
+ }
1955
+ return pending;
1956
+ }
1957
+
1958
+ private async loadAndMaterializeByContinuation(
1959
+ storage: StorageCoordinator,
1960
+ channelId: string,
1961
+ continuationKey: string
1962
+ ): Promise<SessionRecord | undefined> {
1963
+ const record = await storage.getSessionByContinuation(
1964
+ channelId,
1965
+ continuationKey
1966
+ );
1967
+ if (record === undefined) {
1968
+ // Definitive miss: the caller starts a fresh session under this token.
1969
+ return undefined;
1970
+ }
1971
+ const local = await this.sessions.get(record.sessionId);
1972
+ if (local !== undefined) {
1973
+ return local;
1974
+ }
1975
+ const events = await storage.listSessionEvents(record.sessionId);
1976
+ await this.materializeSession(record, eventsNdjson(events));
1977
+ this.logger(
1978
+ `[agent-serve] lazily restored session ${record.sessionId} from storage`
1979
+ );
1980
+ return record;
1981
+ }
1982
+
1983
+ /**
1984
+ * Write one restored session to the local state root. Events land on
1985
+ * disk before the record: a session becomes visible (and addressable via
1986
+ * its continuation key) only with its full stream in place, and the
1987
+ * lazily opened event log hydrates from this file.
1988
+ */
1989
+ private async materializeSession(
1990
+ record: SessionRecord,
1991
+ eventsNdjson: string | undefined
1992
+ ): Promise<void> {
1993
+ if (eventsNdjson !== undefined) {
1994
+ const filePath = this.sessions.eventFilePath(record.sessionId);
1995
+ await mkdir(dirname(filePath), { recursive: true });
1996
+ await writeFile(filePath, eventsNdjson, "utf8");
1997
+ }
1998
+ await this.sessions.save(record);
1999
+ }
2000
+
1716
2001
  trackBackground(promise: Promise<unknown>): void {
1717
2002
  this.backgroundWork.add(promise);
1718
2003
  void promise
@@ -1730,11 +2015,23 @@ export class SessionEngine {
1730
2015
  await Promise.allSettled([...this.dispatchChains.values()]);
1731
2016
  await Promise.allSettled([...this.updateChains.values()]);
1732
2017
  await this.logs.flushAll();
2018
+ await this.storage?.close();
1733
2019
  await this.mcpHost.close();
1734
2020
  await this.runner.dispose?.();
1735
2021
  }
1736
2022
  }
1737
2023
 
2024
+ /** Parallel event-stream loads during the startup bulk restore. */
2025
+ const RESTORE_FETCH_CONCURRENCY = 8;
2026
+
2027
+ /** NDJSON payload for a restored event stream; undefined when empty. */
2028
+ function eventsNdjson(events: SessionEvent[]): string | undefined {
2029
+ if (events.length === 0) {
2030
+ return undefined;
2031
+ }
2032
+ return `${events.map((event) => JSON.stringify(event)).join("\n")}\n`;
2033
+ }
2034
+
1738
2035
  function subagentPrompt(subagent: ResolvedAgent): string {
1739
2036
  if (subagent.instructions !== undefined && subagent.instructions !== "") {
1740
2037
  return subagent.instructions;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Session list / event fetch for CLI (`sessions`, `session`, chat reattach).
3
+ *
4
+ * Customer-facing errors never include hosting URLs or infra jargon.
5
+ */
6
+
7
+ import type { SessionEvent, SessionSummary } from "../types.js";
8
+ import { sanitizeCustomerError } from "./logs-client.js";
9
+ import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
10
+
11
+ export async function listSessions(args: {
12
+ baseUrl: string;
13
+ auth?: RequestAuth;
14
+ fetchImpl?: typeof fetch;
15
+ }): Promise<SessionSummary[]> {
16
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
17
+ const fetchImpl = args.fetchImpl ?? fetch;
18
+ let resp: Response;
19
+ try {
20
+ resp = await fetchImpl(`${baseUrl}/v1/sessions`, {
21
+ headers: mergeRequestHeaders(args.auth),
22
+ });
23
+ } catch (error) {
24
+ throw new Error(
25
+ sanitizeCustomerError(
26
+ error instanceof Error ? error.message : "Could not list sessions."
27
+ )
28
+ );
29
+ }
30
+ if (!resp.ok) {
31
+ throw new Error(`Could not list sessions (HTTP ${resp.status}).`);
32
+ }
33
+ const body = (await resp.json()) as { sessions?: unknown };
34
+ if (!Array.isArray(body.sessions)) {
35
+ return [];
36
+ }
37
+ const out: SessionSummary[] = [];
38
+ for (const row of body.sessions) {
39
+ const parsed = parseSessionSummary(row);
40
+ if (parsed !== undefined) {
41
+ out.push(parsed);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ export async function fetchSessionEvents(args: {
48
+ baseUrl: string;
49
+ sessionId: string;
50
+ auth?: RequestAuth;
51
+ fetchImpl?: typeof fetch;
52
+ }): Promise<SessionEvent[]> {
53
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
54
+ const fetchImpl = args.fetchImpl ?? fetch;
55
+ const path = `/v1/session/${encodeURIComponent(args.sessionId)}/events`;
56
+
57
+ try {
58
+ const jsonResp = await fetchImpl(`${baseUrl}${path}?format=json`, {
59
+ headers: mergeRequestHeaders(args.auth),
60
+ });
61
+ if (jsonResp.ok) {
62
+ const body = (await jsonResp.json()) as { events?: unknown };
63
+ if (Array.isArray(body.events)) {
64
+ return body.events as SessionEvent[];
65
+ }
66
+ } else if (jsonResp.status === 404) {
67
+ throw new Error(`Session ${args.sessionId} not found.`);
68
+ } else if (jsonResp.status === 403) {
69
+ throw new Error(
70
+ `Session ${args.sessionId} is not readable by this caller.`
71
+ );
72
+ }
73
+ } catch (error) {
74
+ if (error instanceof Error && error.message.startsWith("Session ")) {
75
+ throw error;
76
+ }
77
+ // Fall through to NDJSON.
78
+ }
79
+
80
+ try {
81
+ const resp = await fetchImpl(`${baseUrl}${path}`, {
82
+ headers: mergeRequestHeaders(args.auth),
83
+ });
84
+ if (resp.status === 404) {
85
+ throw new Error(`Session ${args.sessionId} not found.`);
86
+ }
87
+ if (resp.status === 403) {
88
+ throw new Error(
89
+ `Session ${args.sessionId} is not readable by this caller.`
90
+ );
91
+ }
92
+ if (!resp.ok) {
93
+ throw new Error(`Could not fetch session events (HTTP ${resp.status}).`);
94
+ }
95
+ const text = await resp.text();
96
+ const events: SessionEvent[] = [];
97
+ for (const line of text.split("\n")) {
98
+ if (line.trim() === "") {
99
+ continue;
100
+ }
101
+ try {
102
+ events.push(JSON.parse(line) as SessionEvent);
103
+ } catch {
104
+ // skip malformed lines
105
+ }
106
+ }
107
+ return events;
108
+ } catch (error) {
109
+ if (error instanceof Error && error.message.startsWith("Session ")) {
110
+ throw error;
111
+ }
112
+ if (error instanceof Error && error.message.startsWith("Could not fetch")) {
113
+ throw new Error(sanitizeCustomerError(error.message));
114
+ }
115
+ throw new Error(
116
+ sanitizeCustomerError(
117
+ error instanceof Error
118
+ ? error.message
119
+ : "Could not fetch session events."
120
+ )
121
+ );
122
+ }
123
+ }
124
+
125
+ /** Lenient parse so partial stubs / older servers still list by sessionId. */
126
+ function parseSessionSummary(value: unknown): SessionSummary | undefined {
127
+ if (typeof value !== "object" || value === null) {
128
+ return undefined;
129
+ }
130
+ const row = value as Record<string, unknown>;
131
+ if (typeof row.sessionId !== "string" || row.sessionId === "") {
132
+ return undefined;
133
+ }
134
+ const continuationToken =
135
+ typeof row.continuationToken === "string"
136
+ ? row.continuationToken
137
+ : row.continuationToken === null
138
+ ? null
139
+ : null;
140
+ return {
141
+ sessionId: row.sessionId,
142
+ channelId: typeof row.channelId === "string" ? row.channelId : "",
143
+ mode: row.mode === "task" ? "task" : "chat",
144
+ title: typeof row.title === "string" ? row.title : undefined,
145
+ turnCount: typeof row.turnCount === "number" ? row.turnCount : 0,
146
+ running: row.running === true,
147
+ createdAt: typeof row.createdAt === "string" ? row.createdAt : "",
148
+ updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "",
149
+ continuationToken,
150
+ externalUrl:
151
+ typeof row.externalUrl === "string" ? row.externalUrl : undefined,
152
+ };
153
+ }
154
+
155
+ /** Format a sessions table (id, channel, mode, turns, running, updated). */
156
+ export function formatSessionsTable(sessions: SessionSummary[]): string {
157
+ if (sessions.length === 0) {
158
+ return "No sessions.\n";
159
+ }
160
+ const header = ["SESSION", "CHANNEL", "MODE", "TURNS", "RUNNING", "UPDATED"];
161
+ const rows = sessions.map((s) => [
162
+ s.sessionId,
163
+ s.channelId,
164
+ s.mode,
165
+ String(s.turnCount),
166
+ s.running ? "yes" : "no",
167
+ s.updatedAt,
168
+ ]);
169
+ return `${renderTable(header, rows)}\n`;
170
+ }
171
+
172
+ function renderTable(header: string[], rows: string[][]): string {
173
+ const widths = header.map((h, i) =>
174
+ Math.max(h.length, ...rows.map((row) => (row[i] ?? "").length))
175
+ );
176
+ const renderRow = (row: string[]): string =>
177
+ row
178
+ .map((cell, i) => cell.padEnd(widths[i] ?? 0))
179
+ .join(" ")
180
+ .trimEnd();
181
+ return [renderRow(header), ...rows.map(renderRow)].join("\n");
182
+ }