@creativeintelligence/abbie 0.1.4 → 0.1.6

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 (538) hide show
  1. package/dist/cli/commands/login.js +26 -0
  2. package/dist/cli/commands/start.js +2 -1
  3. package/oclif.manifest.json +1 -1
  4. package/package.json +9 -5
  5. package/dist/cli/base-command.d.ts.map +0 -1
  6. package/dist/cli/commands/agent/list.d.ts.map +0 -1
  7. package/dist/cli/commands/annotation/ack.d.ts.map +0 -1
  8. package/dist/cli/commands/annotation/create.d.ts.map +0 -1
  9. package/dist/cli/commands/annotation/events.d.ts.map +0 -1
  10. package/dist/cli/commands/annotation/export.d.ts.map +0 -1
  11. package/dist/cli/commands/annotation/import.d.ts.map +0 -1
  12. package/dist/cli/commands/annotation/ingest.d.ts.map +0 -1
  13. package/dist/cli/commands/annotation/list.d.ts.map +0 -1
  14. package/dist/cli/commands/annotation/reply.d.ts.map +0 -1
  15. package/dist/cli/commands/annotation/resolve.d.ts.map +0 -1
  16. package/dist/cli/commands/auto/index.d.ts.map +0 -1
  17. package/dist/cli/commands/backlog/add.d.ts.map +0 -1
  18. package/dist/cli/commands/backlog/claim.d.ts.map +0 -1
  19. package/dist/cli/commands/backlog/complete.d.ts.map +0 -1
  20. package/dist/cli/commands/backlog/list.d.ts.map +0 -1
  21. package/dist/cli/commands/backlog/pick.d.ts.map +0 -1
  22. package/dist/cli/commands/backlog/sync.d.ts.map +0 -1
  23. package/dist/cli/commands/bootstrap.d.ts.map +0 -1
  24. package/dist/cli/commands/bridge.d.ts.map +0 -1
  25. package/dist/cli/commands/context/inject.d.ts.map +0 -1
  26. package/dist/cli/commands/context/list.d.ts.map +0 -1
  27. package/dist/cli/commands/context/publish.d.ts.map +0 -1
  28. package/dist/cli/commands/context/read.d.ts.map +0 -1
  29. package/dist/cli/commands/daemon.d.ts.map +0 -1
  30. package/dist/cli/commands/digest/index.d.ts.map +0 -1
  31. package/dist/cli/commands/docs/lint.d.ts.map +0 -1
  32. package/dist/cli/commands/docs/sync.d.ts.map +0 -1
  33. package/dist/cli/commands/doctor.d.ts.map +0 -1
  34. package/dist/cli/commands/find/index.d.ts.map +0 -1
  35. package/dist/cli/commands/gc.d.ts.map +0 -1
  36. package/dist/cli/commands/history/index.d.ts.map +0 -1
  37. package/dist/cli/commands/hooks/guard.d.ts.map +0 -1
  38. package/dist/cli/commands/hooks/list.d.ts.map +0 -1
  39. package/dist/cli/commands/hooks/lock.d.ts.map +0 -1
  40. package/dist/cli/commands/hooks/status.d.ts.map +0 -1
  41. package/dist/cli/commands/hooks/test.d.ts.map +0 -1
  42. package/dist/cli/commands/hooks/unlock.d.ts.map +0 -1
  43. package/dist/cli/commands/index.d.ts.map +0 -1
  44. package/dist/cli/commands/list.d.ts.map +0 -1
  45. package/dist/cli/commands/list.e2e.test.d.ts +0 -1
  46. package/dist/cli/commands/list.e2e.test.js +0 -47
  47. package/dist/cli/commands/login.d.ts.map +0 -1
  48. package/dist/cli/commands/logout.d.ts.map +0 -1
  49. package/dist/cli/commands/panes/broker.d.ts.map +0 -1
  50. package/dist/cli/commands/panes/pipe-sink.d.ts.map +0 -1
  51. package/dist/cli/commands/panes/snapshot.d.ts.map +0 -1
  52. package/dist/cli/commands/plan.d.ts.map +0 -1
  53. package/dist/cli/commands/plan.e2e.test.d.ts +0 -1
  54. package/dist/cli/commands/plan.e2e.test.js +0 -74
  55. package/dist/cli/commands/preview/index.d.ts.map +0 -1
  56. package/dist/cli/commands/preview/init.d.ts.map +0 -1
  57. package/dist/cli/commands/preview/list.d.ts.map +0 -1
  58. package/dist/cli/commands/preview/status.d.ts.map +0 -1
  59. package/dist/cli/commands/preview/stop.d.ts.map +0 -1
  60. package/dist/cli/commands/preview/sync.d.ts.map +0 -1
  61. package/dist/cli/commands/preview/watch.d.ts.map +0 -1
  62. package/dist/cli/commands/project/add.d.ts.map +0 -1
  63. package/dist/cli/commands/project/list.d.ts.map +0 -1
  64. package/dist/cli/commands/project/remove.d.ts.map +0 -1
  65. package/dist/cli/commands/push.d.ts.map +0 -1
  66. package/dist/cli/commands/reference/add.d.ts.map +0 -1
  67. package/dist/cli/commands/reference/delete.d.ts.map +0 -1
  68. package/dist/cli/commands/reference/extract.d.ts.map +0 -1
  69. package/dist/cli/commands/reference/list.d.ts.map +0 -1
  70. package/dist/cli/commands/reference/normalize.d.ts.map +0 -1
  71. package/dist/cli/commands/reference/open.d.ts.map +0 -1
  72. package/dist/cli/commands/reference/save.d.ts.map +0 -1
  73. package/dist/cli/commands/reference/search.d.ts.map +0 -1
  74. package/dist/cli/commands/reference/show.d.ts.map +0 -1
  75. package/dist/cli/commands/reference/update-index.d.ts.map +0 -1
  76. package/dist/cli/commands/reference/update.d.ts.map +0 -1
  77. package/dist/cli/commands/report/blocked.d.ts.map +0 -1
  78. package/dist/cli/commands/report/complete.d.ts.map +0 -1
  79. package/dist/cli/commands/report/progress.d.ts.map +0 -1
  80. package/dist/cli/commands/report/start.d.ts.map +0 -1
  81. package/dist/cli/commands/resource/acquire.d.ts.map +0 -1
  82. package/dist/cli/commands/resource/list.d.ts.map +0 -1
  83. package/dist/cli/commands/resource/release.d.ts.map +0 -1
  84. package/dist/cli/commands/resource/wait.d.ts.map +0 -1
  85. package/dist/cli/commands/review.d.ts.map +0 -1
  86. package/dist/cli/commands/session/attach.d.ts.map +0 -1
  87. package/dist/cli/commands/session/await.d.ts.map +0 -1
  88. package/dist/cli/commands/session/complete.d.ts.map +0 -1
  89. package/dist/cli/commands/session/create.d.ts.map +0 -1
  90. package/dist/cli/commands/session/heartbeat.d.ts.map +0 -1
  91. package/dist/cli/commands/session/list.d.ts.map +0 -1
  92. package/dist/cli/commands/session/mark-done.d.ts.map +0 -1
  93. package/dist/cli/commands/session/mine.d.ts.map +0 -1
  94. package/dist/cli/commands/session/replay.d.ts.map +0 -1
  95. package/dist/cli/commands/session/run.d.ts.map +0 -1
  96. package/dist/cli/commands/session/show.d.ts.map +0 -1
  97. package/dist/cli/commands/session/start.d.ts.map +0 -1
  98. package/dist/cli/commands/session/state/cleanup.d.ts.map +0 -1
  99. package/dist/cli/commands/session/state/end.d.ts.map +0 -1
  100. package/dist/cli/commands/session/state/get.d.ts.map +0 -1
  101. package/dist/cli/commands/session/state/init.d.ts.map +0 -1
  102. package/dist/cli/commands/session/state/list.d.ts.map +0 -1
  103. package/dist/cli/commands/session/state/update.d.ts.map +0 -1
  104. package/dist/cli/commands/session/stop.d.ts.map +0 -1
  105. package/dist/cli/commands/session/view.d.ts.map +0 -1
  106. package/dist/cli/commands/start.d.ts.map +0 -1
  107. package/dist/cli/commands/state/dump.d.ts.map +0 -1
  108. package/dist/cli/commands/status.d.ts.map +0 -1
  109. package/dist/cli/commands/sync.d.ts.map +0 -1
  110. package/dist/cli/commands/trace/export.d.ts.map +0 -1
  111. package/dist/cli/commands/triage/claim.d.ts.map +0 -1
  112. package/dist/cli/commands/triage/list.d.ts.map +0 -1
  113. package/dist/cli/commands/triage/next.d.ts.map +0 -1
  114. package/dist/cli/commands/triage/pull.d.ts.map +0 -1
  115. package/dist/cli/commands/triage/stats.d.ts.map +0 -1
  116. package/dist/cli/commands/tunnel/list.d.ts.map +0 -1
  117. package/dist/cli/commands/tunnel/start.d.ts.map +0 -1
  118. package/dist/cli/commands/tunnel/stop.d.ts.map +0 -1
  119. package/dist/cli/commands/tunnel/url.d.ts.map +0 -1
  120. package/dist/cli/commands/web/start.d.ts.map +0 -1
  121. package/dist/cli/commands/windows/context.d.ts.map +0 -1
  122. package/dist/cli/commands/windows/focus.d.ts.map +0 -1
  123. package/dist/cli/commands/windows/list.d.ts.map +0 -1
  124. package/dist/cli/commands/windows/map.d.ts.map +0 -1
  125. package/dist/cli/commands/windows/read.d.ts.map +0 -1
  126. package/dist/cli/commands/windows/search.d.ts.map +0 -1
  127. package/dist/cli/commands/windows/show.d.ts.map +0 -1
  128. package/dist/cli/commands/windows/watch.d.ts.map +0 -1
  129. package/dist/lib/active-sessions.d.ts.map +0 -1
  130. package/dist/lib/agent-adapters.d.ts.map +0 -1
  131. package/dist/lib/agent-sessions.d.ts.map +0 -1
  132. package/dist/lib/agent-trace.d.ts.map +0 -1
  133. package/dist/lib/analytics.d.ts.map +0 -1
  134. package/dist/lib/annotations-convex.d.ts.map +0 -1
  135. package/dist/lib/annotations.d.ts.map +0 -1
  136. package/dist/lib/auto/discover.d.ts.map +0 -1
  137. package/dist/lib/auto/ideate.d.ts.map +0 -1
  138. package/dist/lib/auto/spawn.d.ts.map +0 -1
  139. package/dist/lib/auto/workspace.d.ts.map +0 -1
  140. package/dist/lib/backlog.d.ts.map +0 -1
  141. package/dist/lib/backlog.test.d.ts +0 -1
  142. package/dist/lib/backlog.test.js +0 -162
  143. package/dist/lib/config-loader.d.ts.map +0 -1
  144. package/dist/lib/config-sync/adapters/claude.d.ts.map +0 -1
  145. package/dist/lib/config-sync/adapters/codex.d.ts.map +0 -1
  146. package/dist/lib/config-sync/adapters/copilot.d.ts.map +0 -1
  147. package/dist/lib/config-sync/adapters/gemini.d.ts.map +0 -1
  148. package/dist/lib/config-sync/adapters/index.d.ts.map +0 -1
  149. package/dist/lib/config-sync/adapters/opencode.d.ts.map +0 -1
  150. package/dist/lib/config-sync/index.d.ts.map +0 -1
  151. package/dist/lib/config-sync/types.d.ts.map +0 -1
  152. package/dist/lib/config-sync/writer.d.ts.map +0 -1
  153. package/dist/lib/content-sync/index.d.ts.map +0 -1
  154. package/dist/lib/content-sync/types.d.ts.map +0 -1
  155. package/dist/lib/contracts.d.ts.map +0 -1
  156. package/dist/lib/convex.d.ts.map +0 -1
  157. package/dist/lib/device.d.ts.map +0 -1
  158. package/dist/lib/digest/index.d.ts.map +0 -1
  159. package/dist/lib/docs/lint.d.ts.map +0 -1
  160. package/dist/lib/docs/lint.test.d.ts +0 -1
  161. package/dist/lib/docs/lint.test.js +0 -120
  162. package/dist/lib/docs/sync.d.ts.map +0 -1
  163. package/dist/lib/doctor/index.d.ts.map +0 -1
  164. package/dist/lib/doctor/repos.d.ts.map +0 -1
  165. package/dist/lib/doctor/templates.d.ts.map +0 -1
  166. package/dist/lib/errors.d.ts.map +0 -1
  167. package/dist/lib/events.d.ts.map +0 -1
  168. package/dist/lib/heartbeat.d.ts.map +0 -1
  169. package/dist/lib/heartbeat.test.d.ts +0 -1
  170. package/dist/lib/heartbeat.test.js +0 -124
  171. package/dist/lib/hooks/adapters/claude.d.ts.map +0 -1
  172. package/dist/lib/hooks/adapters/codex.d.ts.map +0 -1
  173. package/dist/lib/hooks/adapters/copilot.d.ts.map +0 -1
  174. package/dist/lib/hooks/context.d.ts.map +0 -1
  175. package/dist/lib/hooks/index.d.ts.map +0 -1
  176. package/dist/lib/hooks/registry.d.ts.map +0 -1
  177. package/dist/lib/hooks/runner.d.ts.map +0 -1
  178. package/dist/lib/hooks/types.d.ts.map +0 -1
  179. package/dist/lib/index.test.d.ts +0 -1
  180. package/dist/lib/index.test.js +0 -334
  181. package/dist/lib/managed-session.d.ts.map +0 -1
  182. package/dist/lib/math.d.ts.map +0 -1
  183. package/dist/lib/math.test.d.ts +0 -1
  184. package/dist/lib/math.test.js +0 -238
  185. package/dist/lib/ngrok.d.ts.map +0 -1
  186. package/dist/lib/nvim/discovery.d.ts.map +0 -1
  187. package/dist/lib/nvim/discovery.test.d.ts +0 -1
  188. package/dist/lib/nvim/discovery.test.js +0 -131
  189. package/dist/lib/nvim/index.d.ts.map +0 -1
  190. package/dist/lib/nvim/remote.d.ts.map +0 -1
  191. package/dist/lib/nvim/remote.test.d.ts +0 -1
  192. package/dist/lib/nvim/remote.test.js +0 -18
  193. package/dist/lib/panes/broker.d.ts.map +0 -1
  194. package/dist/lib/panes/index.d.ts.map +0 -1
  195. package/dist/lib/panes/server.d.ts.map +0 -1
  196. package/dist/lib/preview/detect.d.ts.map +0 -1
  197. package/dist/lib/preview/index.d.ts.map +0 -1
  198. package/dist/lib/preview/manager.d.ts.map +0 -1
  199. package/dist/lib/preview/schema.d.ts.map +0 -1
  200. package/dist/lib/preview/sprite.d.ts.map +0 -1
  201. package/dist/lib/preview/watcher.d.ts.map +0 -1
  202. package/dist/lib/process/index.d.ts.map +0 -1
  203. package/dist/lib/process/snapshot.d.ts.map +0 -1
  204. package/dist/lib/process/snapshot.test.d.ts +0 -1
  205. package/dist/lib/process/snapshot.test.js +0 -127
  206. package/dist/lib/project-identity.d.ts.map +0 -1
  207. package/dist/lib/provider-auth.d.ts.map +0 -1
  208. package/dist/lib/references.d.ts.map +0 -1
  209. package/dist/lib/report.d.ts.map +0 -1
  210. package/dist/lib/resources.d.ts.map +0 -1
  211. package/dist/lib/resources.test.d.ts +0 -1
  212. package/dist/lib/resources.test.js +0 -94
  213. package/dist/lib/runner.d.ts.map +0 -1
  214. package/dist/lib/runner.test.d.ts +0 -1
  215. package/dist/lib/runner.test.js +0 -234
  216. package/dist/lib/session-artifacts.d.ts.map +0 -1
  217. package/dist/lib/session-manager.d.ts.map +0 -1
  218. package/dist/lib/session-manager.test.d.ts +0 -1
  219. package/dist/lib/session-manager.test.js +0 -310
  220. package/dist/lib/session-result.d.ts.map +0 -1
  221. package/dist/lib/session-state.d.ts.map +0 -1
  222. package/dist/lib/slack-workspace.d.ts.map +0 -1
  223. package/dist/lib/tmux/bridge.d.ts.map +0 -1
  224. package/dist/lib/tmux/context.d.ts.map +0 -1
  225. package/dist/lib/tmux/context.test.d.ts +0 -1
  226. package/dist/lib/tmux/context.test.js +0 -215
  227. package/dist/lib/tmux/index.d.ts.map +0 -1
  228. package/dist/lib/tmux/map.d.ts.map +0 -1
  229. package/dist/lib/tmux/map.test.d.ts +0 -1
  230. package/dist/lib/tmux/map.test.js +0 -80
  231. package/dist/lib/tmux/panes.d.ts.map +0 -1
  232. package/dist/lib/tmux/redaction.d.ts.map +0 -1
  233. package/dist/lib/tmux/redaction.test.d.ts +0 -1
  234. package/dist/lib/tmux/redaction.test.js +0 -183
  235. package/dist/lib/triage-llm.d.ts.map +0 -1
  236. package/dist/lib/triage-tracker.d.ts.map +0 -1
  237. package/dist/lib/triage.d.ts.map +0 -1
  238. package/dist/lib/types.d.ts.map +0 -1
  239. package/dist/lib/windows/index.d.ts.map +0 -1
  240. package/dist/lib/windows/inventory.d.ts.map +0 -1
  241. package/dist/lib/windows/inventory.test.d.ts +0 -1
  242. package/dist/lib/windows/inventory.test.js +0 -292
  243. package/dist/lib/windows/types.d.ts.map +0 -1
  244. package/dist/lib.d.ts.map +0 -1
  245. package/dist/web/app/@overlay/default.d.ts +0 -1
  246. package/dist/web/app/@overlay/default.js +0 -3
  247. package/dist/web/app/about/page.d.ts +0 -1
  248. package/dist/web/app/about/page.js +0 -6
  249. package/dist/web/app/layout.d.ts +0 -7
  250. package/dist/web/app/layout.js +0 -19
  251. package/dist/web/app/page.d.ts +0 -1
  252. package/dist/web/app/page.js +0 -38
  253. package/dist/web/app/settings/page.d.ts +0 -1
  254. package/dist/web/app/settings/page.js +0 -6
  255. package/dist/web/app/shortcuts/page.d.ts +0 -1
  256. package/dist/web/app/shortcuts/page.js +0 -6
  257. package/dist/web/atoms/sidebar.d.ts +0 -16
  258. package/dist/web/atoms/sidebar.js +0 -58
  259. package/dist/web/atoms/state.d.ts +0 -24
  260. package/dist/web/atoms/state.js +0 -43
  261. package/dist/web/components/command-palette.d.ts +0 -6
  262. package/dist/web/components/command-palette.js +0 -297
  263. package/dist/web/components/main-view.d.ts +0 -1
  264. package/dist/web/components/main-view.js +0 -93
  265. package/dist/web/components/overlay/page-overlay.d.ts +0 -16
  266. package/dist/web/components/overlay/page-overlay.js +0 -84
  267. package/dist/web/components/pane-view.d.ts +0 -5
  268. package/dist/web/components/pane-view.js +0 -168
  269. package/dist/web/components/project-files-view.d.ts +0 -6
  270. package/dist/web/components/project-files-view.js +0 -205
  271. package/dist/web/components/project-planning-view.d.ts +0 -6
  272. package/dist/web/components/project-planning-view.js +0 -146
  273. package/dist/web/components/session-view.d.ts +0 -6
  274. package/dist/web/components/session-view.js +0 -211
  275. package/dist/web/components/sessions-list-view.d.ts +0 -1
  276. package/dist/web/components/sessions-list-view.js +0 -118
  277. package/dist/web/components/sidebar/index.d.ts +0 -9
  278. package/dist/web/components/sidebar/index.js +0 -29
  279. package/dist/web/components/sidebar/kbd.d.ts +0 -17
  280. package/dist/web/components/sidebar/kbd.js +0 -47
  281. package/dist/web/components/sidebar/nav-group.d.ts +0 -22
  282. package/dist/web/components/sidebar/nav-group.js +0 -43
  283. package/dist/web/components/sidebar/nav-item.d.ts +0 -17
  284. package/dist/web/components/sidebar/nav-item.js +0 -38
  285. package/dist/web/components/sidebar/sidebar-header.d.ts +0 -9
  286. package/dist/web/components/sidebar/sidebar-header.js +0 -18
  287. package/dist/web/components/sidebar/sidebar-nav.d.ts +0 -7
  288. package/dist/web/components/sidebar/sidebar-nav.js +0 -505
  289. package/dist/web/components/sidebar/sidebar-search.d.ts +0 -7
  290. package/dist/web/components/sidebar/sidebar-search.js +0 -18
  291. package/dist/web/components/sidebar/sidebar-settings.d.ts +0 -4
  292. package/dist/web/components/sidebar/sidebar-settings.js +0 -28
  293. package/dist/web/components/sidebar/sidebar-transition.d.ts +0 -12
  294. package/dist/web/components/sidebar/sidebar-transition.js +0 -58
  295. package/dist/web/components/status-bar.d.ts +0 -1
  296. package/dist/web/components/status-bar.js +0 -53
  297. package/dist/web/components/terminal.d.ts +0 -4
  298. package/dist/web/components/terminal.js +0 -324
  299. package/dist/web/components/toast.d.ts +0 -7
  300. package/dist/web/components/toast.js +0 -72
  301. package/dist/web/hooks/use-keybindings.d.ts +0 -30
  302. package/dist/web/hooks/use-keybindings.js +0 -322
  303. package/dist/web/hooks/use-state.d.ts +0 -11
  304. package/dist/web/hooks/use-state.js +0 -84
  305. package/dist/web/lib/api.d.ts +0 -179
  306. package/dist/web/lib/api.js +0 -79
  307. package/dist/web/lib/paths.d.ts +0 -2
  308. package/dist/web/lib/paths.js +0 -26
  309. package/dist/web/lib/utils.d.ts +0 -2
  310. package/dist/web/lib/utils.js +0 -5
  311. package/dist/web/lib/ws.d.ts +0 -18
  312. package/dist/web/lib/ws.js +0 -138
  313. package/dist/web/next.config.d.ts +0 -3
  314. package/dist/web/next.config.js +0 -9
  315. package/scripts/generate-manifest.ts +0 -44
  316. package/src/cli/base-command.ts +0 -233
  317. package/src/cli/commands/__tests__/annotations.test.ts +0 -704
  318. package/src/cli/commands/__tests__/bridge.test.ts +0 -101
  319. package/src/cli/commands/__tests__/daemon.test.ts +0 -79
  320. package/src/cli/commands/agent/list.ts +0 -75
  321. package/src/cli/commands/annotation/ack.ts +0 -37
  322. package/src/cli/commands/annotation/create.ts +0 -67
  323. package/src/cli/commands/annotation/events.ts +0 -63
  324. package/src/cli/commands/annotation/export.ts +0 -67
  325. package/src/cli/commands/annotation/import.ts +0 -98
  326. package/src/cli/commands/annotation/ingest.ts +0 -112
  327. package/src/cli/commands/annotation/list.ts +0 -57
  328. package/src/cli/commands/annotation/reply.ts +0 -46
  329. package/src/cli/commands/annotation/resolve.ts +0 -37
  330. package/src/cli/commands/auto/index.ts +0 -755
  331. package/src/cli/commands/backlog/add.ts +0 -74
  332. package/src/cli/commands/backlog/claim.ts +0 -53
  333. package/src/cli/commands/backlog/complete.ts +0 -51
  334. package/src/cli/commands/backlog/list.ts +0 -107
  335. package/src/cli/commands/backlog/pick.ts +0 -50
  336. package/src/cli/commands/backlog/sync.ts +0 -131
  337. package/src/cli/commands/bootstrap.ts +0 -205
  338. package/src/cli/commands/bridge.ts +0 -233
  339. package/src/cli/commands/context/inject.ts +0 -103
  340. package/src/cli/commands/context/list.ts +0 -112
  341. package/src/cli/commands/context/publish.ts +0 -83
  342. package/src/cli/commands/context/read.ts +0 -85
  343. package/src/cli/commands/daemon.ts +0 -1809
  344. package/src/cli/commands/digest/index.ts +0 -245
  345. package/src/cli/commands/docs/lint.ts +0 -93
  346. package/src/cli/commands/docs/sync.ts +0 -90
  347. package/src/cli/commands/doctor.ts +0 -267
  348. package/src/cli/commands/find/index.ts +0 -313
  349. package/src/cli/commands/history/index.ts +0 -269
  350. package/src/cli/commands/hooks/guard.ts +0 -71
  351. package/src/cli/commands/hooks/list.ts +0 -139
  352. package/src/cli/commands/hooks/lock.ts +0 -47
  353. package/src/cli/commands/hooks/status.ts +0 -56
  354. package/src/cli/commands/hooks/test.ts +0 -190
  355. package/src/cli/commands/hooks/unlock.ts +0 -56
  356. package/src/cli/commands/list.e2e.test.ts +0 -58
  357. package/src/cli/commands/list.ts +0 -96
  358. package/src/cli/commands/login.ts +0 -236
  359. package/src/cli/commands/logout.ts +0 -45
  360. package/src/cli/commands/panes/broker.ts +0 -68
  361. package/src/cli/commands/panes/pipe-sink.ts +0 -101
  362. package/src/cli/commands/panes/snapshot.ts +0 -156
  363. package/src/cli/commands/plan.e2e.test.ts +0 -90
  364. package/src/cli/commands/plan.ts +0 -68
  365. package/src/cli/commands/preview/index.ts +0 -282
  366. package/src/cli/commands/preview/list.ts +0 -116
  367. package/src/cli/commands/preview/status.ts +0 -137
  368. package/src/cli/commands/preview/stop.ts +0 -165
  369. package/src/cli/commands/project/add.ts +0 -110
  370. package/src/cli/commands/project/list.ts +0 -136
  371. package/src/cli/commands/project/remove.ts +0 -60
  372. package/src/cli/commands/push.ts +0 -115
  373. package/src/cli/commands/reference/add.ts +0 -266
  374. package/src/cli/commands/reference/delete.ts +0 -67
  375. package/src/cli/commands/reference/extract.ts +0 -389
  376. package/src/cli/commands/reference/list.ts +0 -117
  377. package/src/cli/commands/reference/normalize.ts +0 -90
  378. package/src/cli/commands/reference/open.ts +0 -92
  379. package/src/cli/commands/reference/save.ts +0 -103
  380. package/src/cli/commands/reference/search.ts +0 -85
  381. package/src/cli/commands/reference/show.ts +0 -174
  382. package/src/cli/commands/reference/update-index.ts +0 -103
  383. package/src/cli/commands/reference/update.ts +0 -136
  384. package/src/cli/commands/report/blocked.ts +0 -165
  385. package/src/cli/commands/report/complete.ts +0 -179
  386. package/src/cli/commands/report/progress.ts +0 -142
  387. package/src/cli/commands/report/start.ts +0 -140
  388. package/src/cli/commands/resource/acquire.ts +0 -116
  389. package/src/cli/commands/resource/list.ts +0 -77
  390. package/src/cli/commands/resource/release.ts +0 -64
  391. package/src/cli/commands/resource/wait.ts +0 -93
  392. package/src/cli/commands/review.ts +0 -105
  393. package/src/cli/commands/session/attach.ts +0 -200
  394. package/src/cli/commands/session/await.ts +0 -83
  395. package/src/cli/commands/session/complete.ts +0 -100
  396. package/src/cli/commands/session/create.ts +0 -92
  397. package/src/cli/commands/session/heartbeat.ts +0 -63
  398. package/src/cli/commands/session/list.ts +0 -281
  399. package/src/cli/commands/session/mark-done.ts +0 -132
  400. package/src/cli/commands/session/mine.ts +0 -189
  401. package/src/cli/commands/session/replay.ts +0 -659
  402. package/src/cli/commands/session/run.ts +0 -44
  403. package/src/cli/commands/session/show.ts +0 -177
  404. package/src/cli/commands/session/start.ts +0 -580
  405. package/src/cli/commands/session/state/cleanup.ts +0 -61
  406. package/src/cli/commands/session/state/end.ts +0 -47
  407. package/src/cli/commands/session/state/get.ts +0 -65
  408. package/src/cli/commands/session/state/init.ts +0 -50
  409. package/src/cli/commands/session/state/list.ts +0 -68
  410. package/src/cli/commands/session/state/update.ts +0 -108
  411. package/src/cli/commands/session/stop.ts +0 -134
  412. package/src/cli/commands/session/view.ts +0 -186
  413. package/src/cli/commands/start.ts +0 -256
  414. package/src/cli/commands/state/dump.ts +0 -449
  415. package/src/cli/commands/status.ts +0 -244
  416. package/src/cli/commands/sync.ts +0 -174
  417. package/src/cli/commands/trace/export.ts +0 -255
  418. package/src/cli/commands/triage/claim.ts +0 -203
  419. package/src/cli/commands/triage/list.ts +0 -137
  420. package/src/cli/commands/triage/next.ts +0 -73
  421. package/src/cli/commands/triage/pull.ts +0 -97
  422. package/src/cli/commands/triage/stats.ts +0 -82
  423. package/src/cli/commands/tunnel/list.ts +0 -113
  424. package/src/cli/commands/tunnel/start.ts +0 -122
  425. package/src/cli/commands/tunnel/stop.ts +0 -108
  426. package/src/cli/commands/tunnel/url.ts +0 -82
  427. package/src/cli/commands/web/start.ts +0 -125
  428. package/src/cli/commands/windows/context.ts +0 -439
  429. package/src/cli/commands/windows/focus.ts +0 -130
  430. package/src/cli/commands/windows/list.ts +0 -213
  431. package/src/cli/commands/windows/map.ts +0 -223
  432. package/src/cli/commands/windows/read.ts +0 -279
  433. package/src/cli/commands/windows/search.ts +0 -219
  434. package/src/cli/commands/windows/show.ts +0 -188
  435. package/src/cli/commands/windows/watch.ts +0 -262
  436. package/src/lib/__tests__/annotations-convex.test.ts +0 -104
  437. package/src/lib/active-sessions.ts +0 -1486
  438. package/src/lib/agent-adapters.ts +0 -412
  439. package/src/lib/agent-sessions.ts +0 -671
  440. package/src/lib/agent-trace.test.ts +0 -296
  441. package/src/lib/agent-trace.ts +0 -513
  442. package/src/lib/analytics.ts +0 -178
  443. package/src/lib/annotations-convex.ts +0 -296
  444. package/src/lib/annotations.test.ts +0 -274
  445. package/src/lib/annotations.ts +0 -836
  446. package/src/lib/auto/discover.ts +0 -545
  447. package/src/lib/auto/ideate.ts +0 -412
  448. package/src/lib/auto/spawn.ts +0 -549
  449. package/src/lib/auto/workspace.ts +0 -282
  450. package/src/lib/backlog.test.ts +0 -194
  451. package/src/lib/backlog.ts +0 -428
  452. package/src/lib/config-loader.ts +0 -391
  453. package/src/lib/config-sync/adapters/claude.ts +0 -91
  454. package/src/lib/config-sync/adapters/codex.ts +0 -135
  455. package/src/lib/config-sync/adapters/copilot.ts +0 -98
  456. package/src/lib/config-sync/adapters/gemini.ts +0 -86
  457. package/src/lib/config-sync/adapters/index.ts +0 -39
  458. package/src/lib/config-sync/adapters/opencode.ts +0 -94
  459. package/src/lib/config-sync/index.ts +0 -399
  460. package/src/lib/config-sync/types.ts +0 -92
  461. package/src/lib/config-sync/writer.ts +0 -188
  462. package/src/lib/content-sync/index.ts +0 -882
  463. package/src/lib/content-sync/types.ts +0 -104
  464. package/src/lib/contracts.test.ts +0 -186
  465. package/src/lib/contracts.ts +0 -195
  466. package/src/lib/convex.ts +0 -54
  467. package/src/lib/device.ts +0 -41
  468. package/src/lib/digest/index.ts +0 -529
  469. package/src/lib/docs/lint.test.ts +0 -135
  470. package/src/lib/docs/lint.ts +0 -310
  471. package/src/lib/docs/sync.ts +0 -184
  472. package/src/lib/doctor/index.ts +0 -647
  473. package/src/lib/doctor/repos.ts +0 -381
  474. package/src/lib/doctor/templates.ts +0 -191
  475. package/src/lib/errors.ts +0 -111
  476. package/src/lib/events.ts +0 -479
  477. package/src/lib/heartbeat.test.ts +0 -164
  478. package/src/lib/heartbeat.ts +0 -326
  479. package/src/lib/hooks/adapters/claude.ts +0 -115
  480. package/src/lib/hooks/adapters/codex.ts +0 -92
  481. package/src/lib/hooks/adapters/copilot.ts +0 -141
  482. package/src/lib/hooks/context.ts +0 -174
  483. package/src/lib/hooks/index.ts +0 -39
  484. package/src/lib/hooks/registry.ts +0 -101
  485. package/src/lib/hooks/runner.ts +0 -208
  486. package/src/lib/hooks/types.ts +0 -89
  487. package/src/lib/index.test.ts +0 -426
  488. package/src/lib/managed-session.ts +0 -117
  489. package/src/lib/math.test.ts +0 -299
  490. package/src/lib/math.ts +0 -120
  491. package/src/lib/ngrok.ts +0 -441
  492. package/src/lib/nvim/discovery.test.ts +0 -157
  493. package/src/lib/nvim/discovery.ts +0 -181
  494. package/src/lib/nvim/index.ts +0 -28
  495. package/src/lib/nvim/remote.test.ts +0 -21
  496. package/src/lib/nvim/remote.ts +0 -184
  497. package/src/lib/panes/README.md +0 -20
  498. package/src/lib/panes/broker.ts +0 -261
  499. package/src/lib/panes/index.ts +0 -1
  500. package/src/lib/panes/server.ts +0 -379
  501. package/src/lib/preview/detect.ts +0 -184
  502. package/src/lib/preview/index.ts +0 -1
  503. package/src/lib/process/index.ts +0 -16
  504. package/src/lib/process/snapshot.test.ts +0 -188
  505. package/src/lib/process/snapshot.ts +0 -257
  506. package/src/lib/provider-auth.ts +0 -258
  507. package/src/lib/references.ts +0 -481
  508. package/src/lib/report.ts +0 -973
  509. package/src/lib/resources.test.ts +0 -132
  510. package/src/lib/resources.ts +0 -429
  511. package/src/lib/runner.test.ts +0 -288
  512. package/src/lib/runner.ts +0 -1223
  513. package/src/lib/session-artifacts.test.ts +0 -107
  514. package/src/lib/session-artifacts.ts +0 -193
  515. package/src/lib/session-manager.test.ts +0 -402
  516. package/src/lib/session-manager.ts +0 -150
  517. package/src/lib/session-result.test.ts +0 -98
  518. package/src/lib/session-result.ts +0 -262
  519. package/src/lib/session-state.ts +0 -470
  520. package/src/lib/slack-workspace.ts +0 -25
  521. package/src/lib/tmux/bridge.ts +0 -439
  522. package/src/lib/tmux/context.test.ts +0 -242
  523. package/src/lib/tmux/context.ts +0 -380
  524. package/src/lib/tmux/index.ts +0 -46
  525. package/src/lib/tmux/map.test.ts +0 -99
  526. package/src/lib/tmux/map.ts +0 -252
  527. package/src/lib/tmux/panes.ts +0 -170
  528. package/src/lib/tmux/redaction.test.ts +0 -231
  529. package/src/lib/tmux/redaction.ts +0 -201
  530. package/src/lib/triage-llm.ts +0 -312
  531. package/src/lib/triage-tracker.ts +0 -400
  532. package/src/lib/triage.ts +0 -655
  533. package/src/lib/types.ts +0 -61
  534. package/src/lib/windows/index.ts +0 -2
  535. package/src/lib/windows/inventory.test.ts +0 -370
  536. package/src/lib/windows/inventory.ts +0 -352
  537. package/src/lib/windows/types.ts +0 -46
  538. package/src/lib.ts +0 -260
@@ -1,1809 +0,0 @@
1
- import { type ChildProcess, execSync, spawn as spawnProcess } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
- import { homedir, hostname } from "node:os";
5
- import { join } from "node:path";
6
- import { api, closeClients, getHttpClient, isConvexConfigured, subscribe } from "@creativeintelligence/sdk/convex";
7
- import { Flags } from "@oclif/core";
8
- import type { FunctionReturnType } from "convex/server";
9
- import { type ActiveSession, getActiveSessionManager } from "../../lib/active-sessions.js";
10
- import { discoverProjects, loadConfig } from "../../lib/config-loader.js";
11
- import { getDeviceId } from "../../lib/device.js";
12
- import * as nvim from "../../lib/nvim/remote.js";
13
- import {
14
- capturePane,
15
- isRunning,
16
- isSensitiveCommand,
17
- type RedactMode,
18
- redactContent,
19
- sendKeys,
20
- } from "../../lib/tmux/index.js";
21
- import { AGENTS } from "../../lib/types.js";
22
- import * as windows from "../../lib/windows/index.js";
23
- import { ConfigSyncRunner, type SyncTickResult } from "../../lib/config-sync/index.js";
24
- import { ContentSyncRunner, type ContentSyncRunnerOptions } from "../../lib/content-sync/index.js";
25
- import type { ContentSyncTickResult } from "../../lib/content-sync/types.js";
26
- import { BaseCommand } from "../base-command.js";
27
-
28
- type DaemonTick = {
29
- at: string;
30
- tmuxRunning: boolean;
31
- windows: { synced: boolean; count: number; error?: string };
32
- panes: { enabled: boolean; synced: boolean; count: number; redacted: number; error?: string };
33
- input: { enabled: boolean; received: number; sent: number; skipped: number; error?: string };
34
- commandQueue: {
35
- enabled: boolean;
36
- processed: number;
37
- failed: number;
38
- pending: number;
39
- error?: string;
40
- };
41
- bufferContent: { enabled: boolean; synced: boolean; count: number; error?: string };
42
- sessions: { enabled: boolean; created: number; updated: number; error?: string };
43
- projects: { enabled: boolean; synced: boolean; count: number; error?: string };
44
- heartbeat: { ok: boolean; error?: string };
45
- desktopHeartbeat: { ok: boolean; error?: string };
46
- sandboxCommands: {
47
- enabled: boolean;
48
- processed: number;
49
- failed: number;
50
- activeServers: number;
51
- error?: string;
52
- };
53
- configSync?: {
54
- ok: boolean;
55
- reason: string;
56
- agents: SyncTickResult["changes"];
57
- error?: string;
58
- };
59
- contentSync?: {
60
- ok: boolean;
61
- reason: string;
62
- changes: ContentSyncTickResult["changes"];
63
- error?: string;
64
- };
65
- };
66
-
67
- type SessionSyncState = {
68
- status: ActiveSession["status"];
69
- endedAt?: number;
70
- startCommitSha?: string;
71
- cwd?: string;
72
- traceId?: string;
73
- sessionId?: string;
74
- project?: string;
75
- };
76
-
77
- type TerminalInputPayload = {
78
- deviceId: string;
79
- paneId: string;
80
- data: string;
81
- token?: string;
82
- };
83
-
84
- function sleep(ms: number, signal: AbortSignal): Promise<void> {
85
- return new Promise((resolve) => {
86
- if (signal.aborted) return resolve();
87
- const timer = setTimeout(resolve, ms);
88
- signal.addEventListener(
89
- "abort",
90
- () => {
91
- clearTimeout(timer);
92
- resolve();
93
- },
94
- { once: true },
95
- );
96
- });
97
- }
98
-
99
- function truncate(value: string, max: number): string {
100
- if (value.length <= max) return value;
101
- return `${value.slice(0, max - 1)}…`;
102
- }
103
-
104
- function isSessionNotFoundError(err: unknown, sessionId: string): boolean {
105
- const message = err instanceof Error ? err.message : String(err);
106
- return (
107
- message.includes(`Session not found: ${sessionId}`) || message.includes("Session not found")
108
- );
109
- }
110
-
111
- /** Best-effort git rev-parse HEAD in a directory. Returns undefined if not a git repo. */
112
- function getHeadSha(cwd: string): string | undefined {
113
- try {
114
- return execSync("git rev-parse HEAD", { cwd, timeout: 3000, stdio: "pipe" }).toString().trim();
115
- } catch {
116
- return undefined;
117
- }
118
- }
119
-
120
- /** Best-effort git diff --name-status between two commits. Returns parsed file changes. */
121
- function getGitDiff(
122
- cwd: string,
123
- fromSha: string,
124
- toSha?: string,
125
- ): Array<{ status: string; file: string }> {
126
- try {
127
- const to = toSha ?? "HEAD";
128
- const raw = execSync(`git diff --name-status ${fromSha}..${to}`, {
129
- cwd,
130
- timeout: 5000,
131
- stdio: "pipe",
132
- })
133
- .toString()
134
- .trim();
135
- if (!raw) return [];
136
- return raw.split("\n").map((line) => {
137
- const [status, ...fileParts] = line.split("\t");
138
- return { status: status || "M", file: fileParts.join("\t") };
139
- });
140
- } catch {
141
- return [];
142
- }
143
- }
144
-
145
- export default class DaemonCommand extends BaseCommand {
146
- static override description = "Run daemon to sync local state to Convex";
147
- static override hidden = false;
148
-
149
- static override flags = {
150
- ...BaseCommand.baseFlags,
151
- interval: Flags.integer({
152
- description: "Sync interval in seconds",
153
- default: 5,
154
- }),
155
- "tmux-session": Flags.string({
156
- description:
157
- 'Tmux session to sync (default: ABBIE_TMUX_SESSION or "abbie"). Use --all-sessions to sync everything.',
158
- }),
159
- "all-sessions": Flags.boolean({
160
- description: "Sync all tmux sessions (legacy behavior; can be noisy)",
161
- default: false,
162
- }),
163
- "windows-only": Flags.boolean({
164
- description: "Only sync windows, not sessions",
165
- default: false,
166
- }),
167
- redact: Flags.string({
168
- description: "Redaction mode for pane content published to Convex",
169
- options: ["none", "balanced", "strict"],
170
- default: "balanced",
171
- }),
172
- "enable-input": Flags.boolean({
173
- description:
174
- "Enable processing remote terminal input events from Convex (DANGEROUS without a token)",
175
- default: false,
176
- }),
177
- "input-token": Flags.string({
178
- description:
179
- "Shared secret token required to accept terminal input events (recommended; otherwise input is ignored)",
180
- }),
181
- };
182
-
183
- protected async execute(): Promise<unknown> {
184
- const { flags } = await this.parse(DaemonCommand);
185
- this.parsedFlags = flags;
186
-
187
- if (!isConvexConfigured()) {
188
- this.error(
189
- "Convex not configured. Run `abbie login` to authenticate, or set CONVEX_URL env var.",
190
- );
191
- }
192
-
193
- const intervalSec = flags.interval;
194
- if (!Number.isInteger(intervalSec) || intervalSec <= 0) {
195
- this.error("--interval must be a positive integer (seconds)");
196
- }
197
-
198
- const deviceId = getDeviceId();
199
-
200
- const tmuxSessionFilter = flags["all-sessions"]
201
- ? undefined
202
- : flags["tmux-session"] || process.env.ABBIE_TMUX_SESSION || "abbie";
203
-
204
- const stopController = new AbortController();
205
- const startedAt = new Date().toISOString();
206
-
207
- const sessionSyncState = new Map<string, SessionSyncState>();
208
- let lastWindowsHash: string | null = null;
209
- const paneHashByPaneId = new Map<string, string>();
210
- const bufferContentHashByKey = new Map<string, string>(); // "paneId:bufnr" -> hash
211
- const inventoryByPaneId = new Map<string, windows.WindowInfo>();
212
-
213
- const inputEnabled = Boolean(flags["enable-input"]);
214
- const inputToken = flags["input-token"] || process.env.AGENTS_INPUT_TOKEN || null;
215
- const inputState = {
216
- received: 0,
217
- sent: 0,
218
- skipped: 0,
219
- error: null as string | null,
220
- };
221
-
222
- const inputUnsubscribers: Array<() => void> = [];
223
- const seenInputEventIds = new Set<string>();
224
- let processingInput = false;
225
- const pendingInputEvents: Array<{ id: string; payload: TerminalInputPayload }> = [];
226
-
227
- const enqueueInput = (id: string, payload: TerminalInputPayload) => {
228
- if (seenInputEventIds.has(id)) return;
229
- seenInputEventIds.add(id);
230
- // Bound memory.
231
- if (seenInputEventIds.size > 500) {
232
- const first = seenInputEventIds.values().next().value as string | undefined;
233
- if (first) seenInputEventIds.delete(first);
234
- }
235
- pendingInputEvents.push({ id, payload });
236
- inputState.received++;
237
- };
238
-
239
- const sendToTmux = async (payload: TerminalInputPayload): Promise<void> => {
240
- const target = inventoryByPaneId.get(payload.paneId);
241
- if (!target) {
242
- inputState.skipped++;
243
- return;
244
- }
245
-
246
- // Split on newlines and send Enter for each newline boundary.
247
- const parts = payload.data.split(/\r\n|\r|\n/);
248
- for (let i = 0; i < parts.length; i++) {
249
- const part = parts[i];
250
- if (part.length > 0) {
251
- await sendKeys(target.window, target.pane, part, {
252
- session: target.session,
253
- literal: true,
254
- });
255
- }
256
- if (i < parts.length - 1) {
257
- // newline boundary
258
- await sendKeys(target.window, target.pane, "Enter", { session: target.session });
259
- }
260
- }
261
- inputState.sent++;
262
- };
263
-
264
- const processInputQueue = async (): Promise<void> => {
265
- if (processingInput) return;
266
- processingInput = true;
267
- try {
268
- while (pendingInputEvents.length > 0 && !stopController.signal.aborted) {
269
- const next = pendingInputEvents.shift();
270
- if (!next) break;
271
- await sendToTmux(next.payload);
272
- }
273
- } catch (err) {
274
- inputState.error = err instanceof Error ? err.message : String(err);
275
- } finally {
276
- processingInput = false;
277
- }
278
- };
279
-
280
- const requestStop = (signal: NodeJS.Signals) => {
281
- if (stopController.signal.aborted) return;
282
- this.logInfo(`Daemon stopping... (${signal})`);
283
- stopController.abort();
284
- };
285
-
286
- const onSigint = () => requestStop("SIGINT");
287
- const onSigterm = () => requestStop("SIGTERM");
288
-
289
- process.on("SIGINT", onSigint);
290
- process.on("SIGTERM", onSigterm);
291
-
292
- let cmdQueueWorker: Promise<void> | null = null;
293
- let sandboxCmdWorker: Promise<void> | null = null;
294
-
295
- // Sync subscription state (declared outside try for finally cleanup)
296
- let configSyncUnsub: (() => void) | null = null;
297
- let contentSyncUnsub: (() => void) | null = null;
298
-
299
- try {
300
- this.logInfo(`Daemon started (deviceId=${deviceId}, interval=${intervalSec}s)`);
301
- await this.syncDeviceHeartbeat(deviceId);
302
-
303
- if (inputEnabled) {
304
- if (!inputToken) {
305
- this.logWarn(
306
- "Remote input enabled but no token configured; will ignore terminal input events. Set --input-token or AGENTS_INPUT_TOKEN.",
307
- );
308
- } else {
309
- this.logInfo("Remote input enabled (token gated).");
310
- }
311
-
312
- // Subscribe to a small tail of recent terminal input events. This is a temporary bus that
313
- // avoids adding new Convex API surface before auth is wired up.
314
- const unsubscribe = subscribe(
315
- api.events.listRecent,
316
- { eventType: "terminal.input", limit: 50 },
317
- (events) => {
318
- // listRecent returns newest-first; process oldest-first.
319
- for (const evt of [...events].reverse()) {
320
- const id = (evt as { _id?: unknown })._id;
321
- const eventId = typeof id === "string" ? id : null;
322
- if (!eventId) continue;
323
-
324
- const payload = (evt as { payload?: unknown }).payload;
325
- if (!payload || typeof payload !== "object") continue;
326
- const p = payload as Partial<TerminalInputPayload>;
327
- if (p.deviceId !== deviceId) continue;
328
- if (typeof p.paneId !== "string") continue;
329
- if (typeof p.data !== "string") continue;
330
-
331
- // Token gate: if inputToken configured, only accept matching events.
332
- if (inputToken && p.token !== inputToken) continue;
333
- // If token is not configured, ignore everything (fail closed).
334
- if (!inputToken) continue;
335
-
336
- enqueueInput(eventId, {
337
- deviceId: p.deviceId,
338
- paneId: p.paneId,
339
- data: p.data,
340
- token: p.token,
341
- });
342
- }
343
-
344
- void processInputQueue();
345
- },
346
- );
347
- inputUnsubscribers.push(unsubscribe);
348
- }
349
-
350
- // Command queue worker - always enabled.
351
- // Process commands enqueued by the chat agent (e.g., "abbie session start").
352
- const cmdQueueState = {
353
- processed: 0,
354
- failed: 0,
355
- error: null as string | null,
356
- inFlight: false,
357
- };
358
- cmdQueueWorker = this.runCommandQueueWorker(deviceId, cmdQueueState, stopController.signal);
359
- this.logInfo("Command queue worker active");
360
-
361
- // Sandbox command worker state — processes sandbox commands from Convex.
362
- // Launched on first successful desktop heartbeat (needs connectionId).
363
- const sandboxCmdState = {
364
- processed: 0,
365
- failed: 0,
366
- error: null as string | null,
367
- };
368
-
369
- // Config sync — subscribes to Convex for MCP connections, writes agent config files.
370
- // Fires instantly on change via Convex reactive subscription.
371
- let configSyncRunner: ConfigSyncRunner | null = null;
372
- let configSyncInFlight = false;
373
- let lastConfigSyncResult: DaemonTick["configSync"] = undefined;
374
-
375
- // Content sync — subscribes to Convex for rules/skills/hooks, writes to ~/.abbie/.
376
- // Fires instantly on change via Convex reactive subscription.
377
- let contentSyncRunner: ContentSyncRunner | null = null;
378
- let contentSyncInFlight = false;
379
- let lastContentSyncResult: DaemonTick["contentSync"] = undefined;
380
-
381
- // Set up decrypt function for apiKey values (best-effort)
382
- let configSyncDecrypt: ((v: string) => Promise<string>) | undefined;
383
- try {
384
- const { decryptIfEncrypted } = await import("@lnittman/convex-encryption");
385
- configSyncDecrypt = async (v: string) => {
386
- const result = await decryptIfEncrypted(v);
387
- return result ?? v;
388
- };
389
- } catch {
390
- // Encryption package not available — pass through
391
- }
392
-
393
- while (!stopController.signal.aborted) {
394
- const tickAt = new Date().toISOString();
395
-
396
- const tick: DaemonTick = {
397
- at: tickAt,
398
- tmuxRunning: false,
399
- windows: { synced: false, count: 0 },
400
- panes: { enabled: true, synced: false, count: 0, redacted: 0 },
401
- input: {
402
- enabled: inputEnabled,
403
- received: inputState.received,
404
- sent: inputState.sent,
405
- skipped: inputState.skipped,
406
- ...(inputState.error ? { error: inputState.error } : {}),
407
- },
408
- commandQueue: {
409
- enabled: true,
410
- processed: cmdQueueState.processed,
411
- failed: cmdQueueState.failed,
412
- pending: cmdQueueState.inFlight ? 1 : 0,
413
- ...(cmdQueueState.error ? { error: cmdQueueState.error } : {}),
414
- },
415
- bufferContent: { enabled: true, synced: false, count: 0 },
416
- sessions: { enabled: !flags["windows-only"], created: 0, updated: 0 },
417
- projects: { enabled: true, synced: false, count: 0 },
418
- heartbeat: { ok: false },
419
- desktopHeartbeat: { ok: false },
420
- sandboxCommands: {
421
- enabled: this.desktopConnectionId !== null,
422
- processed: sandboxCmdState.processed,
423
- failed: sandboxCmdState.failed,
424
- activeServers: this.sandboxServerProcesses.size,
425
- ...(sandboxCmdState.error ? { error: sandboxCmdState.error } : {}),
426
- },
427
- };
428
-
429
- // Device heartbeat (best-effort)
430
- try {
431
- await this.syncDeviceHeartbeat(deviceId);
432
- tick.heartbeat.ok = true;
433
- } catch (err) {
434
- tick.heartbeat.ok = false;
435
- tick.heartbeat.error = err instanceof Error ? err.message : String(err);
436
- this.logWarn(`Device heartbeat failed: ${tick.heartbeat.error}`);
437
- }
438
-
439
- // tmux windows
440
- try {
441
- tick.tmuxRunning = await isRunning();
442
- } catch (err) {
443
- tick.tmuxRunning = false;
444
- tick.windows.error = err instanceof Error ? err.message : String(err);
445
- this.logWarn(`tmux check failed: ${tick.windows.error}`);
446
- }
447
-
448
- // Windows sync: always clear on transition to not-running, otherwise only sync on changes.
449
- try {
450
- const shouldClear = !tick.tmuxRunning && lastWindowsHash !== "[]";
451
- const shouldInventory = tick.tmuxRunning;
452
-
453
- if (shouldInventory) {
454
- const inventory = await windows.inventory({
455
- session: tmuxSessionFilter,
456
- includeNvim: true,
457
- includeAgent: true,
458
- includeBuffers: true,
459
- });
460
-
461
- // Update paneId -> window mapping for input.
462
- inventoryByPaneId.clear();
463
- for (const state of inventory) {
464
- inventoryByPaneId.set(state.window.paneId, state.window);
465
- }
466
-
467
- const syncResult = await this.syncWindows(deviceId, {
468
- lastHash: lastWindowsHash,
469
- inventory,
470
- });
471
- tick.windows.synced = syncResult.synced;
472
- tick.windows.count = syncResult.count;
473
- lastWindowsHash = syncResult.hash;
474
-
475
- if (syncResult.synced) {
476
- this.logInfo(`Synced ${syncResult.count} windows`);
477
- }
478
-
479
- // Pane snapshots: sync every tick while tmux is running.
480
- try {
481
- const panesResult = await this.syncPanes(
482
- deviceId,
483
- inventory,
484
- paneHashByPaneId,
485
- flags.redact as "none" | RedactMode,
486
- );
487
- tick.panes.synced = panesResult.synced;
488
- tick.panes.count = panesResult.count;
489
- tick.panes.redacted = panesResult.redacted;
490
- } catch (err) {
491
- tick.panes.error = err instanceof Error ? err.message : String(err);
492
- this.logWarn(`Panes sync failed: ${tick.panes.error}`);
493
- }
494
-
495
- // Nvim buffer content: sync terminal buffer scrollback via nvim RPC
496
- try {
497
- const bufResult = await this.syncNvimBufferContent(
498
- deviceId,
499
- inventory,
500
- bufferContentHashByKey,
501
- );
502
- tick.bufferContent.synced = bufResult.synced;
503
- tick.bufferContent.count = bufResult.count;
504
- if (bufResult.synced) {
505
- this.logInfo(`Synced ${bufResult.count} nvim buffers`);
506
- }
507
- } catch (err) {
508
- tick.bufferContent.error = err instanceof Error ? err.message : String(err);
509
- this.logWarn(`Buffer content sync failed: ${tick.bufferContent.error}`);
510
- }
511
- } else if (shouldClear) {
512
- const syncResult = await this.syncWindows(deviceId, { clear: true });
513
- tick.windows.synced = syncResult.synced;
514
- tick.windows.count = syncResult.count;
515
- lastWindowsHash = syncResult.hash;
516
-
517
- if (syncResult.synced) {
518
- this.logInfo("tmux not running; cleared windows");
519
- }
520
-
521
- // Best-effort: clear panes when tmux stops.
522
- try {
523
- await getHttpClient().mutation(api.panes.clearByDevice, { deviceId });
524
- paneHashByPaneId.clear();
525
- tick.panes.synced = true;
526
- tick.panes.count = 0;
527
- } catch (err) {
528
- tick.panes.error = err instanceof Error ? err.message : String(err);
529
- this.logWarn(`Panes clear failed: ${tick.panes.error}`);
530
- }
531
- }
532
- } catch (err) {
533
- tick.windows.error = err instanceof Error ? err.message : String(err);
534
- this.logWarn(`Windows sync failed: ${tick.windows.error}`);
535
- }
536
-
537
- // Session sync (best-effort)
538
- if (!flags["windows-only"]) {
539
- try {
540
- const sessionResult = await this.syncSessions(deviceId, sessionSyncState);
541
- tick.sessions.created = sessionResult.created;
542
- tick.sessions.updated = sessionResult.updated;
543
- if (sessionResult.created || sessionResult.updated) {
544
- this.logInfo(
545
- `Synced sessions (created=${sessionResult.created}, updated=${sessionResult.updated})`,
546
- );
547
- }
548
- } catch (err) {
549
- tick.sessions.error = err instanceof Error ? err.message : String(err);
550
- this.logWarn(`Session sync failed: ${tick.sessions.error}`);
551
- }
552
- }
553
-
554
- // Projects sync (Convex → local cache)
555
- try {
556
- const projectsResult = await this.syncProjects(deviceId);
557
- tick.projects.synced = projectsResult.synced;
558
- tick.projects.count = projectsResult.count;
559
- if (projectsResult.synced) {
560
- this.logInfo(`Synced ${projectsResult.count} projects to local cache`);
561
- }
562
- } catch (err) {
563
- tick.projects.error = err instanceof Error ? err.message : String(err);
564
- // Don't warn every tick - user might not be logged in
565
- }
566
-
567
- // Config sync + Content sync — set up Convex subscriptions once when clerkId is available.
568
- // Subscriptions fire instantly on change (no polling). Runners are initialized lazily.
569
- if (this.clerkId && !configSyncUnsub) {
570
- const clerkId = this.clerkId;
571
-
572
- // Initialize config sync runner
573
- configSyncRunner = new ConfigSyncRunner({
574
- fetchSnapshot: async () => {
575
- return await getHttpClient().query(api.configSync.getSnapshot, { clerkId });
576
- },
577
- decrypt: configSyncDecrypt,
578
- });
579
- this.logInfo("Config sync: subscription active");
580
-
581
- // Subscribe — fires immediately with current state, then on every change
582
- configSyncUnsub = subscribe(api.configSync.getSnapshot, { clerkId }, (snapshot) => {
583
- if (configSyncInFlight) return;
584
- configSyncInFlight = true;
585
- configSyncRunner!.tick({ snapshot }).then((result) => {
586
- lastConfigSyncResult = {
587
- ok: result.ok,
588
- reason: result.reason,
589
- agents: result.changes,
590
- };
591
- if (result.changes.length > 0) {
592
- const updated = result.changes.filter((c) => c.action === "updated");
593
- if (updated.length > 0) {
594
- this.logInfo(
595
- `Config sync: ${updated.map((c) => c.agent).join(", ")} updated`,
596
- );
597
- }
598
- }
599
- }).catch((err) => {
600
- lastConfigSyncResult = {
601
- ok: false,
602
- reason: "error",
603
- agents: [],
604
- error: err instanceof Error ? err.message : String(err),
605
- };
606
- }).finally(() => {
607
- configSyncInFlight = false;
608
- });
609
- });
610
-
611
- // Initialize content sync runner
612
- contentSyncRunner = new ContentSyncRunner({
613
- fetchSnapshot: async () => {
614
- return await getHttpClient().query(api.contentSync.getSnapshot, { clerkId });
615
- },
616
- });
617
- this.logInfo("Content sync: subscription active");
618
-
619
- // Subscribe
620
- contentSyncUnsub = subscribe(api.contentSync.getSnapshot, { clerkId }, (snapshot) => {
621
- if (contentSyncInFlight) return;
622
- contentSyncInFlight = true;
623
- contentSyncRunner!.tick({ snapshot }).then((result) => {
624
- lastContentSyncResult = {
625
- ok: result.ok,
626
- reason: result.reason,
627
- changes: result.changes,
628
- };
629
- if (result.changes.length > 0) {
630
- const actionable = result.changes.filter((c) => c.action !== "unchanged");
631
- if (actionable.length > 0) {
632
- this.logInfo(
633
- `Content sync: ${actionable.map((c) => `${c.type}/${c.name}:${c.action}`).join(", ")}`,
634
- );
635
- }
636
- }
637
- }).catch((err) => {
638
- lastContentSyncResult = {
639
- ok: false,
640
- reason: "error",
641
- changes: [],
642
- error: err instanceof Error ? err.message : String(err),
643
- };
644
- }).finally(() => {
645
- contentSyncInFlight = false;
646
- });
647
- });
648
- }
649
-
650
- // Report last subscription sync results in tick
651
- if (lastConfigSyncResult) {
652
- tick.configSync = lastConfigSyncResult;
653
- }
654
- if (lastContentSyncResult) {
655
- tick.contentSync = lastContentSyncResult;
656
- }
657
-
658
- // Desktop provider heartbeat bridge (best-effort)
659
- try {
660
- await this.syncDesktopHeartbeat(deviceId);
661
- if (this.desktopProviderRegistered) {
662
- tick.desktopHeartbeat.ok = true;
663
- }
664
- } catch (err) {
665
- tick.desktopHeartbeat.ok = false;
666
- tick.desktopHeartbeat.error = err instanceof Error ? err.message : String(err);
667
- this.logWarn(`Desktop heartbeat failed: ${tick.desktopHeartbeat.error}`);
668
- }
669
-
670
- // Launch sandbox command worker once we have a connectionId
671
- if (this.desktopConnectionId && !sandboxCmdWorker) {
672
- sandboxCmdWorker = this.runSandboxCommandWorker(
673
- this.desktopConnectionId,
674
- sandboxCmdState,
675
- stopController.signal,
676
- );
677
- this.logInfo("Sandbox command worker active");
678
- }
679
-
680
- // Update tick with live sandbox state
681
- tick.sandboxCommands.enabled = this.desktopConnectionId !== null;
682
- tick.sandboxCommands.processed = sandboxCmdState.processed;
683
- tick.sandboxCommands.failed = sandboxCmdState.failed;
684
- tick.sandboxCommands.activeServers = this.sandboxServerProcesses.size;
685
-
686
- if (this.ndjsonEnabled()) {
687
- this.outputNdjson([tick]);
688
- }
689
-
690
- await sleep(intervalSec * 1000, stopController.signal);
691
- }
692
- } finally {
693
- process.off("SIGINT", onSigint);
694
- process.off("SIGTERM", onSigterm);
695
- stopController.abort();
696
- try {
697
- if (cmdQueueWorker) await cmdQueueWorker;
698
- } catch {
699
- // ignore
700
- }
701
- try {
702
- if (sandboxCmdWorker) await sandboxCmdWorker;
703
- } catch {
704
- // ignore
705
- }
706
- // Kill any spawned server processes
707
- for (const [, proc] of this.sandboxServerProcesses) {
708
- try {
709
- proc.kill("SIGTERM");
710
- } catch {
711
- // ignore
712
- }
713
- }
714
- this.sandboxServerProcesses.clear();
715
- for (const unsub of inputUnsubscribers) {
716
- try {
717
- unsub();
718
- } catch {
719
- // ignore
720
- }
721
- }
722
- // Clean up sync subscriptions
723
- if (configSyncUnsub) {
724
- try { configSyncUnsub(); } catch { /* ignore */ }
725
- }
726
- if (contentSyncUnsub) {
727
- try { contentSyncUnsub(); } catch { /* ignore */ }
728
- }
729
- closeClients();
730
- }
731
-
732
- const stoppedAt = new Date().toISOString();
733
- const result = { status: "stopped", deviceId, startedAt, stoppedAt };
734
- if (this.ndjsonEnabled()) {
735
- this.outputNdjson([result]);
736
- }
737
- return result;
738
- }
739
-
740
- private async syncWindows(
741
- deviceId: string,
742
- options?: {
743
- clear?: boolean;
744
- lastHash?: string | null;
745
- inventory?: Awaited<ReturnType<typeof windows.inventory>>;
746
- },
747
- ): Promise<{ synced: boolean; count: number; hash: string }> {
748
- if (options?.clear) {
749
- const result = await getHttpClient().mutation(api.windows.sync, { deviceId, windows: [] });
750
- return { synced: true, count: result.synced, hash: "[]" };
751
- }
752
-
753
- const states =
754
- options?.inventory ??
755
- (await windows.inventory({
756
- includeNvim: true,
757
- includeAgent: true,
758
- includeBuffers: true,
759
- }));
760
-
761
- const sorted = states.sort((a, b) => {
762
- if (a.window.session !== b.window.session)
763
- return a.window.session.localeCompare(b.window.session);
764
- if (a.window.window !== b.window.window) return a.window.window - b.window.window;
765
- return a.window.pane - b.window.pane;
766
- });
767
-
768
- // Build PID → sessionId map for agent↔session linkage
769
- const pidToSessionId = new Map<number, string>();
770
- try {
771
- const activeSessions = getActiveSessionManager().list();
772
- for (const s of activeSessions) {
773
- if (s.pid && s.status === "running") {
774
- pidToSessionId.set(s.pid, s.session_id);
775
- }
776
- }
777
- } catch {
778
- // Best-effort — don't block sync if session manager fails
779
- }
780
-
781
- const windowData = sorted.map((state) => ({
782
- paneId:
783
- state.window.paneId ||
784
- `${state.window.session}:${state.window.window}.${state.window.pane}`,
785
- tmuxSession: state.window.session,
786
- windowIndex: state.window.window,
787
- windowName: state.window.windowName,
788
- paneIndex: state.window.pane,
789
- cwd: state.window.cwd || "",
790
- title: state.window.title || undefined,
791
- isActive: state.window.active,
792
- cols: state.window.size.cols,
793
- rows: state.window.size.rows,
794
- hasNvim: state.nvim?.available ?? false,
795
- nvimPid: state.nvim?.pid ?? undefined,
796
- nvimServerAddr: state.nvim?.serverAddr ?? undefined,
797
- agentType: state.agent?.type ?? undefined,
798
- agentPid: state.agent?.pid ?? undefined,
799
- sessionId: state.agent?.pid ? pidToSessionId.get(state.agent.pid) : undefined,
800
- nvimCurrentBuffer: state.nvim?.currentBuffer
801
- ? {
802
- name: state.nvim.currentBuffer.name,
803
- path: state.nvim.currentBuffer.path,
804
- modified: state.nvim.currentBuffer.modified,
805
- filetype: state.nvim.currentBuffer.filetype,
806
- lineCount: state.nvim.currentBuffer.lineCount,
807
- }
808
- : undefined,
809
- nvimBuffers:
810
- state.buffers.length > 0
811
- ? state.buffers.map((b) => ({
812
- name: b.name,
813
- path: b.path,
814
- modified: b.modified,
815
- filetype: b.filetype,
816
- lineCount: b.lineCount,
817
- }))
818
- : undefined,
819
- }));
820
-
821
- const hash = JSON.stringify(windowData);
822
- if (options?.lastHash === hash) {
823
- return { synced: false, count: windowData.length, hash };
824
- }
825
-
826
- const syncResult = await getHttpClient().mutation(api.windows.sync, {
827
- deviceId,
828
- windows: windowData,
829
- });
830
- return { synced: true, count: syncResult.synced, hash };
831
- }
832
-
833
- private async syncPanes(
834
- deviceId: string,
835
- inventory: Awaited<ReturnType<typeof windows.inventory>>,
836
- lastHashByPaneId: Map<string, string>,
837
- redactMode: "none" | RedactMode,
838
- ): Promise<{ synced: boolean; count: number; redacted: number }> {
839
- const updates: { paneId: string; paneIndex: number; content: string }[] = [];
840
- let redacted = 0;
841
-
842
- for (const state of inventory) {
843
- const paneId =
844
- state.window.paneId ||
845
- `${state.window.session}:${state.window.window}.${state.window.pane}`;
846
-
847
- const raw = await capturePane(state.window.window, state.window.pane, {
848
- session: state.window.session,
849
- lines: 100,
850
- });
851
-
852
- let content = raw;
853
-
854
- if (redactMode !== "none") {
855
- const title = state.window.title ?? "";
856
- if (title && isSensitiveCommand(title)) {
857
- content = "***REDACTED***";
858
- redacted++;
859
- } else {
860
- const result = redactContent(raw, redactMode);
861
- content = result.content;
862
- if (result.wasRedacted) redacted++;
863
- }
864
- }
865
-
866
- const hash = createHash("sha1").update(content).digest("hex");
867
- const prev = lastHashByPaneId.get(paneId);
868
- if (prev === hash) continue;
869
-
870
- lastHashByPaneId.set(paneId, hash);
871
- updates.push({ paneId, paneIndex: state.window.pane, content });
872
- }
873
-
874
- if (updates.length === 0) {
875
- return { synced: false, count: 0, redacted: 0 };
876
- }
877
-
878
- const result = await getHttpClient().mutation(api.panes.sync, {
879
- deviceId,
880
- panes: updates,
881
- });
882
-
883
- return { synced: true, count: result.total, redacted };
884
- }
885
-
886
- private async syncNvimBufferContent(
887
- deviceId: string,
888
- inventory: Awaited<ReturnType<typeof windows.inventory>>,
889
- hashByKey: Map<string, string>,
890
- ): Promise<{ synced: boolean; count: number }> {
891
- let totalSynced = 0;
892
-
893
- for (const state of inventory) {
894
- const serverAddr = state.nvim?.serverAddr;
895
- if (!serverAddr) continue;
896
-
897
- const paneId =
898
- state.window.paneId ||
899
- `${state.window.session}:${state.window.window}.${state.window.pane}`;
900
-
901
- const buffers = await nvim.listBuffers(serverAddr);
902
- if (!buffers || buffers.length === 0) continue;
903
-
904
- const bufferData: {
905
- bufnr: number;
906
- name: string;
907
- content: string;
908
- lineCount: number;
909
- isTerminal: boolean;
910
- }[] = [];
911
-
912
- let anyChanged = false;
913
- for (const buf of buffers) {
914
- const isTerminal = buf.name.startsWith("term://");
915
-
916
- // Read last 200 lines — enough for context, bounded storage
917
- const lines = await nvim.getBufferLines(serverAddr, buf.bufnr, 1, "$");
918
- if (!lines) continue;
919
-
920
- const tail = lines.length > 200 ? lines.slice(-200) : lines;
921
- const content = tail.join("\n");
922
-
923
- // Track if content changed via hash
924
- const key = `${paneId}:${buf.bufnr}`;
925
- const hash = createHash("sha1").update(content).digest("hex");
926
- if (hashByKey.get(key) !== hash) {
927
- hashByKey.set(key, hash);
928
- anyChanged = true;
929
- }
930
-
931
- bufferData.push({
932
- bufnr: buf.bufnr,
933
- name: buf.name,
934
- content,
935
- lineCount: lines.length,
936
- isTerminal,
937
- });
938
- }
939
-
940
- // Only sync if something changed (but always send ALL buffers so stale removal works)
941
- if (!anyChanged || bufferData.length === 0) continue;
942
-
943
- await getHttpClient().mutation(api.nvimBufferContent.sync, {
944
- deviceId,
945
- paneId,
946
- buffers: bufferData,
947
- });
948
-
949
- totalSynced += bufferData.length;
950
- }
951
-
952
- return { synced: totalSynced > 0, count: totalSynced };
953
- }
954
-
955
- private async syncDeviceHeartbeat(deviceId: string): Promise<void> {
956
- await getHttpClient().mutation(api.devices.upsert, {
957
- deviceId,
958
- hostname: hostname(),
959
- platform: process.platform,
960
- metadata: {
961
- user: process.env.USER || process.env.LOGNAME || undefined,
962
- version: this.config.version || undefined,
963
- },
964
- });
965
- }
966
-
967
- private async syncDesktopHeartbeat(deviceId: string): Promise<void> {
968
- if (!this.desktopProviderRegistered) return;
969
-
970
- if (!this.clerkId) {
971
- throw new Error("Desktop provider registered, but clerkId is missing");
972
- }
973
-
974
- if (!this.desktopProviderId) {
975
- throw new Error("Desktop provider registered, but providerId is missing");
976
- }
977
-
978
- const result = await getHttpClient().mutation(api["functions/sandbox/desktop"].heartbeat, {
979
- clerkId: this.clerkId,
980
- providerId: this.desktopProviderId,
981
- machineId: deviceId,
982
- machineName: hostname(),
983
- platform: process.platform,
984
- });
985
-
986
- if (result?.connectionId) {
987
- this.desktopConnectionId = result.connectionId;
988
- }
989
- }
990
-
991
- private async syncSessions(
992
- deviceId: string,
993
- stateBySessionId: Map<string, SessionSyncState>,
994
- ): Promise<{ created: number; updated: number }> {
995
- const manager = getActiveSessionManager();
996
-
997
- // Avoid scanning huge histories every tick.
998
- // Note: manager.list() already reaps orphaned terminal-mode sessions
999
- // (marks them "failed" if their tmux pane is gone — see isTmuxPaneAlive check).
1000
- const sessions = manager.list().slice(0, 200);
1001
-
1002
- let created = 0;
1003
- let updated = 0;
1004
-
1005
- for (const session of sessions) {
1006
- const prev = stateBySessionId.get(session.session_id);
1007
- if (prev?.status === session.status) {
1008
- if (session.status === "running") continue;
1009
- if (prev.endedAt !== undefined) continue;
1010
- }
1011
-
1012
- const next: SessionSyncState = {
1013
- status: session.status,
1014
- cwd: prev?.cwd ?? session.cwd,
1015
- traceId: prev?.traceId ?? session.trace_id,
1016
- sessionId: session.session_id,
1017
- project: session.project,
1018
- startCommitSha: prev?.startCommitSha ?? session.start_commit_sha,
1019
- };
1020
- if (session.status !== "running") {
1021
- next.endedAt = prev?.endedAt ?? Date.now();
1022
- }
1023
-
1024
- const updateArgs = {
1025
- sessionId: session.session_id,
1026
- status: session.status,
1027
- pid: session.pid || undefined,
1028
- endedAt: next.endedAt,
1029
- metadata:
1030
- session.exit_code !== undefined || session.read_only !== undefined
1031
- ? {
1032
- exitCode: session.exit_code,
1033
- readOnly: session.read_only,
1034
- }
1035
- : undefined,
1036
- };
1037
-
1038
- let updatedRemote = false;
1039
- try {
1040
- const res = await getHttpClient().mutation(api.sessions.update, updateArgs);
1041
-
1042
- // Newer backend returns `{ updated: false }` instead of throwing when missing.
1043
- if (
1044
- res &&
1045
- typeof res === "object" &&
1046
- "updated" in res &&
1047
- (res as { updated?: boolean }).updated === false
1048
- ) {
1049
- updatedRemote = false;
1050
- } else {
1051
- updatedRemote = true;
1052
- }
1053
- } catch (err) {
1054
- if (!isSessionNotFoundError(err, session.session_id)) {
1055
- throw err;
1056
- }
1057
- updatedRemote = false;
1058
- }
1059
-
1060
- if (updatedRemote) {
1061
- // Emit file_activity on session completion (Agent Trace producer)
1062
- if (
1063
- prev?.status === "running" &&
1064
- session.status !== "running" &&
1065
- prev.startCommitSha &&
1066
- prev.cwd &&
1067
- prev.traceId
1068
- ) {
1069
- try {
1070
- const changes = getGitDiff(prev.cwd, prev.startCommitSha);
1071
- if (changes.length > 0) {
1072
- await getHttpClient().mutation(api.contextBus.publish, {
1073
- traceId: prev.traceId,
1074
- sessionId: session.session_id,
1075
- project: session.project,
1076
- type: "file_activity",
1077
- title: `${changes.length} file(s) changed`,
1078
- content: JSON.stringify(changes),
1079
- metadata: {
1080
- startCommitSha: prev.startCommitSha,
1081
- endCommitSha: getHeadSha(prev.cwd),
1082
- agent: session.agent,
1083
- sessionStatus: session.status,
1084
- },
1085
- });
1086
- }
1087
- } catch {
1088
- // Best-effort — never block session sync
1089
- }
1090
- }
1091
-
1092
- updated++;
1093
- stateBySessionId.set(session.session_id, next);
1094
- continue;
1095
- }
1096
-
1097
- // Create (only if not found)
1098
- const goal = session.goal?.trim();
1099
- // Prefer sha captured at spawn time (from active-sessions.ts start()),
1100
- // fall back to current HEAD if not available (e.g. old session format)
1101
- const headSha = session.start_commit_sha || getHeadSha(session.cwd);
1102
- next.startCommitSha = headSha;
1103
-
1104
- await getHttpClient().mutation(api.sessions.create, {
1105
- sessionId: session.session_id,
1106
- agent: session.agent,
1107
- project: session.project,
1108
- goal: goal ? truncate(goal, 1000) : undefined,
1109
- issue: session.issue,
1110
- status: session.status,
1111
- pid: session.pid || undefined,
1112
- cwd: session.cwd,
1113
- deviceId,
1114
- parentSessionId: session.parent_session_id,
1115
- traceId: session.trace_id,
1116
- promptPath: session.prompt_path,
1117
- outputPath: session.output_path,
1118
- startCommitSha: headSha,
1119
- metadata:
1120
- session.exit_code !== undefined || session.read_only !== undefined
1121
- ? {
1122
- exitCode: session.exit_code,
1123
- readOnly: session.read_only,
1124
- }
1125
- : undefined,
1126
- });
1127
-
1128
- created++;
1129
- stateBySessionId.set(session.session_id, next);
1130
- }
1131
-
1132
- return { created, updated };
1133
- }
1134
-
1135
- private lastProjectsHash: string | null = null;
1136
- private desktopProviderRegistered = false;
1137
- private clerkId: string | null = null;
1138
- private desktopProviderId: string | null = null;
1139
- private desktopConnectionId: string | null = null;
1140
- private sandboxServerProcesses = new Map<string, ChildProcess>();
1141
-
1142
- private async syncProjects(deviceId: string): Promise<{ synced: boolean; count: number }> {
1143
- // Query Convex for projects associated with this device's user
1144
- let result = await getHttpClient().query(api.projects.listByDeviceId, { deviceId });
1145
-
1146
- if (!result.clerkId) {
1147
- // User not logged in — can't sync
1148
- return { synced: false, count: 0 };
1149
- }
1150
-
1151
- // Store clerkId for other operations
1152
- this.clerkId = result.clerkId;
1153
-
1154
- // Register this machine as a Desktop sandbox provider (once per daemon lifecycle)
1155
- if (!this.desktopProviderRegistered) {
1156
- try {
1157
- const reg = await getHttpClient().mutation(
1158
- api["functions/sandbox/providers"].registerDesktopProvider,
1159
- {
1160
- clerkId: result.clerkId,
1161
- deviceId,
1162
- machineName: hostname(),
1163
- platform: process.platform as "darwin" | "linux" | "win32",
1164
- },
1165
- );
1166
- this.desktopProviderId = reg.providerId;
1167
- this.desktopProviderRegistered = true;
1168
- this.logInfo(`Desktop sandbox provider registered${reg.isNew ? "" : " (existing)"}.`);
1169
- } catch (err) {
1170
- this.logWarn(
1171
- `Desktop provider registration failed: ${err instanceof Error ? err.message : String(err)}`,
1172
- );
1173
- }
1174
- }
1175
-
1176
- // Push local project configs UP to Convex (idempotent, hash-deduped)
1177
- try {
1178
- const pushResult = await this.pushLocalProjects(result.clerkId);
1179
- if (pushResult.pushed > 0) {
1180
- this.logInfo(`Pushed ${pushResult.pushed} projects to Convex`);
1181
- // Re-fetch to get the updated list
1182
- result = await getHttpClient().query(api.projects.listByDeviceId, { deviceId });
1183
- }
1184
- } catch (err) {
1185
- this.logWarn(`Project push failed: ${err instanceof Error ? err.message : String(err)}`);
1186
- }
1187
-
1188
- if (result.projects.length === 0) {
1189
- this.logWarn(`syncProjects: 0 projects after push (clerkId=${result.clerkId})`);
1190
- return { synced: false, count: 0 };
1191
- }
1192
-
1193
- // Build the projects.json structure matching existing format
1194
- const projectsMap: Record<
1195
- string,
1196
- {
1197
- name: string;
1198
- emoji: string;
1199
- linearTeam: string | null;
1200
- color: string;
1201
- }
1202
- > = {};
1203
-
1204
- for (const project of result.projects) {
1205
- projectsMap[project.name] = {
1206
- name: project.name,
1207
- emoji: project.emoji || "📁",
1208
- linearTeam: project.linearTeam || null,
1209
- color: project.color || "#bec2c8",
1210
- };
1211
- }
1212
-
1213
- const projectsJson = {
1214
- projects: projectsMap,
1215
- defaultEmoji: "📁",
1216
- lastUpdated: new Date().toISOString(),
1217
- };
1218
-
1219
- // Check if content changed
1220
- const hash = createHash("sha1").update(JSON.stringify(projectsMap)).digest("hex");
1221
- if (hash === this.lastProjectsHash) {
1222
- return { synced: false, count: result.projects.length };
1223
- }
1224
-
1225
- // Write to ~/.abbie/projects.json
1226
- const abbieDir = join(homedir(), ".abbie");
1227
- if (!existsSync(abbieDir)) {
1228
- mkdirSync(abbieDir, { recursive: true });
1229
- }
1230
-
1231
- const projectsPath = join(abbieDir, "projects.json");
1232
- writeFileSync(projectsPath, JSON.stringify(projectsJson, null, 2), "utf-8");
1233
-
1234
- this.lastProjectsHash = hash;
1235
- return { synced: true, count: result.projects.length };
1236
- }
1237
-
1238
- private lastProjectsPushHash: string | null = null;
1239
-
1240
- /**
1241
- * Push local project configs (.abbie/config.json) UP to Convex projects table.
1242
- * Idempotent — uses hash dedup to avoid redundant mutations.
1243
- */
1244
- private async pushLocalProjects(clerkId: string): Promise<{ pushed: number }> {
1245
- const discovered = discoverProjects().filter((p) => p.hasConfig && p.configPath);
1246
-
1247
- // Build normalized config data for hashing
1248
- const configData: Array<{
1249
- name: string;
1250
- emoji: string;
1251
- color: string;
1252
- team?: string;
1253
- path: string;
1254
- }> = [];
1255
- for (const p of discovered) {
1256
- const loaded = loadConfig(p.configPath!);
1257
- if (!loaded?.config?.name) continue;
1258
- configData.push({
1259
- name: loaded.config.name,
1260
- emoji: loaded.config.emoji?.trim() || "📁",
1261
- color: "#bec2c8",
1262
- team: loaded.config.team,
1263
- path: p.path,
1264
- });
1265
- }
1266
-
1267
- const hash = createHash("sha1").update(JSON.stringify(configData)).digest("hex");
1268
- if (hash === this.lastProjectsPushHash) {
1269
- return { pushed: 0 };
1270
- }
1271
-
1272
- let pushed = 0;
1273
- for (const cfg of configData) {
1274
- try {
1275
- await getHttpClient().mutation(api.projects.upsert, {
1276
- clerkId,
1277
- name: cfg.name,
1278
- emoji: cfg.emoji,
1279
- color: cfg.color,
1280
- linearTeam: cfg.team,
1281
- path: cfg.path,
1282
- });
1283
- pushed++;
1284
- } catch (err) {
1285
- this.logWarn(
1286
- `Failed to push project ${cfg.name}: ${err instanceof Error ? err.message : String(err)}`,
1287
- );
1288
- }
1289
- }
1290
-
1291
- this.lastProjectsPushHash = hash;
1292
- return { pushed };
1293
- }
1294
-
1295
- private async runCommandQueueWorker(
1296
- deviceId: string,
1297
- state: { processed: number; failed: number; error: string | null; inFlight: boolean },
1298
- signal: AbortSignal,
1299
- ): Promise<void> {
1300
- type ClaimResult = FunctionReturnType<typeof api.commandQueue.claim>;
1301
- type Claimed = Exclude<ClaimResult, null>;
1302
-
1303
- while (!signal.aborted) {
1304
- if (state.inFlight) {
1305
- await sleep(250, signal);
1306
- continue;
1307
- }
1308
-
1309
- let claimed: Claimed | null = null;
1310
-
1311
- try {
1312
- const next = await getHttpClient().mutation(api.commandQueue.claim, { deviceId });
1313
- if (!next) {
1314
- await sleep(1000, signal);
1315
- continue;
1316
- }
1317
-
1318
- claimed = next;
1319
- state.inFlight = true;
1320
-
1321
- const result = await this.executeQueuedCommand(claimed.command, claimed.args);
1322
- if (result.success) {
1323
- await getHttpClient().mutation(api.commandQueue.complete, { id: claimed.id, result });
1324
- state.processed++;
1325
- state.error = null;
1326
- this.logInfo(`Command executed: ${claimed.command}`);
1327
- } else {
1328
- const errorMsg = result.error || "Unknown error";
1329
- await getHttpClient().mutation(api.commandQueue.fail, {
1330
- id: claimed.id,
1331
- error: errorMsg,
1332
- });
1333
- state.failed++;
1334
- state.error = errorMsg;
1335
- this.logWarn(`Command failed: ${claimed.command} - ${errorMsg}`);
1336
- }
1337
- } catch (err) {
1338
- const errorMsg = err instanceof Error ? err.message : String(err);
1339
- state.failed++;
1340
- state.error = errorMsg;
1341
- if (claimed) {
1342
- try {
1343
- await getHttpClient().mutation(api.commandQueue.fail, {
1344
- id: claimed.id,
1345
- error: errorMsg,
1346
- });
1347
- } catch {
1348
- // ignore
1349
- }
1350
- }
1351
- await sleep(1000, signal);
1352
- } finally {
1353
- state.inFlight = false;
1354
- }
1355
- }
1356
- }
1357
-
1358
- /**
1359
- * Background worker that polls the sandbox command queue for pending
1360
- * commands (startServer, stopServer, exec) and executes them locally.
1361
- */
1362
- private async runSandboxCommandWorker(
1363
- connectionId: string,
1364
- state: { processed: number; failed: number; error: string | null },
1365
- signal: AbortSignal,
1366
- ): Promise<void> {
1367
- type PendingCommand = {
1368
- _id: string;
1369
- commandType: "exec" | "startServer" | "stopServer";
1370
- cmd: string[];
1371
- serverId?: string;
1372
- runId?: string;
1373
- };
1374
-
1375
- while (!signal.aborted) {
1376
- try {
1377
- const pending = await getHttpClient().query(
1378
- api["functions/sandbox/desktop"].listPendingCommands,
1379
- { connectionId },
1380
- );
1381
-
1382
- if (!pending || pending.length === 0) {
1383
- await sleep(2000, signal);
1384
- continue;
1385
- }
1386
-
1387
- for (const cmd of pending as PendingCommand[]) {
1388
- if (signal.aborted) break;
1389
-
1390
- const { _id: commandId, commandType } = cmd;
1391
-
1392
- // Claim the command
1393
- try {
1394
- await getHttpClient().mutation(api["functions/sandbox/desktop"].claimCommand, {
1395
- commandId,
1396
- });
1397
- } catch (_err) {
1398
- // Another daemon may have claimed it — skip
1399
- continue;
1400
- }
1401
-
1402
- try {
1403
- await this.executeSandboxCommand(cmd);
1404
- state.processed++;
1405
- state.error = null;
1406
- this.logInfo(`Sandbox command executed: ${commandType}`);
1407
- } catch (err) {
1408
- const errorMsg = err instanceof Error ? err.message : String(err);
1409
- state.failed++;
1410
- state.error = errorMsg;
1411
- this.logWarn(`Sandbox command failed: ${commandType} - ${errorMsg}`);
1412
-
1413
- // Report failure
1414
- try {
1415
- await getHttpClient().mutation(api["functions/sandbox/desktop"].completeCommand, {
1416
- commandId,
1417
- status: "failed" as const,
1418
- stderr: errorMsg.slice(0, 2000),
1419
- });
1420
- } catch {
1421
- // ignore
1422
- }
1423
- }
1424
- }
1425
- } catch (err) {
1426
- state.error = err instanceof Error ? err.message : String(err);
1427
- // Don't spam on persistent errors
1428
- await sleep(5000, signal);
1429
- }
1430
-
1431
- await sleep(1000, signal);
1432
- }
1433
- }
1434
-
1435
- /**
1436
- * Execute a single sandbox command locally.
1437
- * Handles startServer (spawn detached process), stopServer (kill by PID),
1438
- * and exec (run command and capture output).
1439
- */
1440
- private async executeSandboxCommand(cmd: { _id: string; commandType: "exec" | "startServer" | "stopServer"; cmd: string[]; serverId?: string; runId?: string }): Promise<void> {
1441
- const c = cmd as {
1442
- _id: string;
1443
- commandType: "exec" | "startServer" | "stopServer";
1444
- cmd: string[];
1445
- serverId?: string;
1446
- workdir?: string;
1447
- env?: Record<string, string>;
1448
- timeoutMs?: number;
1449
- serverOpts?: {
1450
- port: number;
1451
- name?: string;
1452
- healthCheck?: { path?: string; timeoutMs?: number; intervalMs?: number };
1453
- };
1454
- };
1455
-
1456
- const commandId = c._id;
1457
-
1458
- switch (c.commandType) {
1459
- case "startServer": {
1460
- if (!c.serverId) {
1461
- throw new Error("startServer command missing serverId");
1462
- }
1463
-
1464
- const [command, ...args] = c.cmd;
1465
- if (!command) {
1466
- throw new Error("startServer command has empty cmd array");
1467
- }
1468
-
1469
- const proc = spawnProcess(command, args, {
1470
- cwd: c.workdir ?? process.cwd(),
1471
- env: { ...process.env, ...(c.env ?? {}) },
1472
- detached: true,
1473
- stdio: ["ignore", "pipe", "pipe"],
1474
- });
1475
-
1476
- // Track the process by serverId
1477
- this.sandboxServerProcesses.set(c.serverId, proc);
1478
-
1479
- const pid = proc.pid;
1480
- let stdoutBuf = "";
1481
- let stderrBuf = "";
1482
-
1483
- proc.stdout?.on("data", (chunk: Buffer) => {
1484
- stdoutBuf += chunk.toString();
1485
- // Keep only last 4KB for reporting
1486
- if (stdoutBuf.length > 4096) {
1487
- stdoutBuf = stdoutBuf.slice(-4096);
1488
- }
1489
- });
1490
-
1491
- proc.stderr?.on("data", (chunk: Buffer) => {
1492
- stderrBuf += chunk.toString();
1493
- if (stderrBuf.length > 4096) {
1494
- stderrBuf = stderrBuf.slice(-4096);
1495
- }
1496
- });
1497
-
1498
- // Report running status to Convex
1499
- const publicUrl = c.serverOpts ? `http://localhost:${c.serverOpts.port}` : undefined;
1500
-
1501
- await getHttpClient().mutation(api["functions/sandbox/desktop"].updateServerStatus, {
1502
- serverId: c.serverId,
1503
- status: "running",
1504
- providerServerId: pid ? String(pid) : undefined,
1505
- publicUrl,
1506
- });
1507
-
1508
- // Complete the command
1509
- await getHttpClient().mutation(api["functions/sandbox/desktop"].completeCommand, {
1510
- commandId,
1511
- status: "completed" as const,
1512
- stdout: `Server started with PID ${pid}`,
1513
- });
1514
-
1515
- // Monitor process exit
1516
- proc.on("exit", (code, sig) => {
1517
- this.sandboxServerProcesses.delete(c.serverId!);
1518
-
1519
- // Best-effort status update
1520
- getHttpClient()
1521
- .mutation(api["functions/sandbox/desktop"].updateServerStatus, {
1522
- serverId: c.serverId!,
1523
- status: code === 0 || sig === "SIGTERM" ? "stopped" : "failed",
1524
- error:
1525
- code !== 0 && code !== null
1526
- ? `Process exited with code ${code}${stderrBuf ? `: ${stderrBuf.slice(0, 500)}` : ""}`
1527
- : undefined,
1528
- })
1529
- .catch(() => {
1530
- // ignore
1531
- });
1532
- });
1533
-
1534
- proc.unref();
1535
- break;
1536
- }
1537
-
1538
- case "stopServer": {
1539
- if (!c.serverId) {
1540
- throw new Error("stopServer command missing serverId");
1541
- }
1542
-
1543
- const existing = this.sandboxServerProcesses.get(c.serverId);
1544
- if (existing) {
1545
- existing.kill("SIGTERM");
1546
- this.sandboxServerProcesses.delete(c.serverId);
1547
- }
1548
-
1549
- await getHttpClient().mutation(api["functions/sandbox/desktop"].updateServerStatus, {
1550
- serverId: c.serverId,
1551
- status: "stopped",
1552
- });
1553
-
1554
- await getHttpClient().mutation(api["functions/sandbox/desktop"].completeCommand, {
1555
- commandId,
1556
- status: "completed" as const,
1557
- stdout: existing
1558
- ? `Server stopped (PID ${existing.pid})`
1559
- : "Server not found locally (may have already exited)",
1560
- });
1561
- break;
1562
- }
1563
-
1564
- case "exec": {
1565
- const startTime = Date.now();
1566
- const [execCmd, ...execArgs] = c.cmd;
1567
- if (!execCmd) {
1568
- throw new Error("exec command has empty cmd array");
1569
- }
1570
-
1571
- const result = await new Promise<{
1572
- exitCode: number;
1573
- stdout: string;
1574
- stderr: string;
1575
- }>((resolve) => {
1576
- let stdout = "";
1577
- let stderr = "";
1578
-
1579
- const proc = spawnProcess(execCmd, execArgs, {
1580
- cwd: c.workdir ?? process.cwd(),
1581
- env: { ...process.env, ...(c.env ?? {}) },
1582
- stdio: ["ignore", "pipe", "pipe"],
1583
- timeout: c.timeoutMs ?? 30_000,
1584
- });
1585
-
1586
- proc.stdout?.on("data", (chunk: Buffer) => {
1587
- stdout += chunk.toString();
1588
- });
1589
- proc.stderr?.on("data", (chunk: Buffer) => {
1590
- stderr += chunk.toString();
1591
- });
1592
-
1593
- proc.on("close", (code) => {
1594
- resolve({
1595
- exitCode: code ?? 1,
1596
- stdout: stdout.slice(0, 8000),
1597
- stderr: stderr.slice(0, 8000),
1598
- });
1599
- });
1600
- proc.on("error", (err) => {
1601
- resolve({
1602
- exitCode: 1,
1603
- stdout: "",
1604
- stderr: err.message,
1605
- });
1606
- });
1607
- });
1608
-
1609
- const durationMs = Date.now() - startTime;
1610
-
1611
- await getHttpClient().mutation(api["functions/sandbox/desktop"].completeCommand, {
1612
- commandId,
1613
- status: result.exitCode === 0 ? ("completed" as const) : ("failed" as const),
1614
- exitCode: result.exitCode,
1615
- stdout: result.stdout,
1616
- stderr: result.stderr,
1617
- durationMs,
1618
- });
1619
- break;
1620
- }
1621
-
1622
- default:
1623
- throw new Error(`Unknown sandbox command type: ${c.commandType}`);
1624
- }
1625
- }
1626
-
1627
- /**
1628
- * Execute a queued command from the chat agent.
1629
- * Currently supports "abbie session start" for spawning local agent sessions.
1630
- */
1631
- private async executeQueuedCommand(
1632
- command: string,
1633
- args: unknown,
1634
- ): Promise<
1635
- | ({ success: true; error?: undefined } & Record<string, unknown>)
1636
- | { success: false; error: string }
1637
- > {
1638
- switch (command) {
1639
- case "abbie session start": {
1640
- if (!args || typeof args !== "object") {
1641
- return {
1642
- success: false,
1643
- error: 'Invalid args for "abbie session start": expected an object',
1644
- };
1645
- }
1646
-
1647
- const sessionArgs = args as Partial<{
1648
- agent: "claude" | "codex" | "copilot" | "gemini";
1649
- project: string;
1650
- goal: string;
1651
- issue?: string;
1652
- cwd?: string;
1653
- }>;
1654
-
1655
- const allowedAgents = new Set(AGENTS);
1656
- if (!sessionArgs.agent || !allowedAgents.has(sessionArgs.agent)) {
1657
- return {
1658
- success: false,
1659
- error:
1660
- 'Invalid args for "abbie session start": missing or invalid `agent` (claude|codex|copilot|gemini)',
1661
- };
1662
- }
1663
-
1664
- if (typeof sessionArgs.project !== "string" || sessionArgs.project.trim().length === 0) {
1665
- return {
1666
- success: false,
1667
- error: 'Invalid args for "abbie session start": missing `project`',
1668
- };
1669
- }
1670
-
1671
- if (typeof sessionArgs.goal !== "string" || sessionArgs.goal.trim().length === 0) {
1672
- return {
1673
- success: false,
1674
- error: 'Invalid args for "abbie session start": missing `goal`',
1675
- };
1676
- }
1677
-
1678
- const manager = getActiveSessionManager();
1679
- const result = await manager.start({
1680
- agent: sessionArgs.agent,
1681
- project: sessionArgs.project,
1682
- goal: sessionArgs.goal,
1683
- issue: sessionArgs.issue,
1684
- cwd: sessionArgs.cwd,
1685
- });
1686
-
1687
- return {
1688
- success: true,
1689
- sessionId: result.session_id,
1690
- };
1691
- }
1692
-
1693
- case "read-buffer": {
1694
- if (!args || typeof args !== "object") {
1695
- return { success: false, error: 'Invalid args for "read-buffer": expected an object' };
1696
- }
1697
-
1698
- const bufArgs = args as Partial<{
1699
- paneId: string;
1700
- bufnr: number;
1701
- lines: number;
1702
- }>;
1703
-
1704
- if (!bufArgs.paneId) {
1705
- return { success: false, error: 'Missing "paneId" for read-buffer' };
1706
- }
1707
-
1708
- // Find nvim server for this pane from inventory
1709
- const inv = await windows.inventory({
1710
- includeNvim: true,
1711
- includeAgent: false,
1712
- includeBuffers: false,
1713
- });
1714
-
1715
- const paneState = inv.find((s) => s.window.paneId === bufArgs.paneId);
1716
-
1717
- if (!paneState?.nvim?.serverAddr) {
1718
- return { success: false, error: `No nvim server found for pane ${bufArgs.paneId}` };
1719
- }
1720
-
1721
- const serverAddr = paneState.nvim.serverAddr;
1722
-
1723
- // If no bufnr, list all buffers
1724
- if (bufArgs.bufnr === undefined) {
1725
- const bufs = await nvim.listBuffers(serverAddr);
1726
- if (!bufs) return { success: false, error: "Failed to list nvim buffers" };
1727
- return {
1728
- success: true,
1729
- buffers: bufs.map((b) => ({
1730
- bufnr: b.bufnr,
1731
- name: b.name,
1732
- isTerminal: b.name.startsWith("term://"),
1733
- })),
1734
- };
1735
- }
1736
-
1737
- // Read specific buffer
1738
- const maxLines = bufArgs.lines ?? 200;
1739
- const allLines = await nvim.getBufferLines(serverAddr, bufArgs.bufnr, 1, "$");
1740
- if (!allLines) {
1741
- return { success: false, error: `Failed to read buffer ${bufArgs.bufnr}` };
1742
- }
1743
-
1744
- const tail = allLines.length > maxLines ? allLines.slice(-maxLines) : allLines;
1745
-
1746
- return {
1747
- success: true,
1748
- bufnr: bufArgs.bufnr,
1749
- totalLines: allLines.length,
1750
- returnedLines: tail.length,
1751
- content: tail.join("\n"),
1752
- };
1753
- }
1754
-
1755
- case "terminal-input": {
1756
- if (!args || typeof args !== "object") {
1757
- return { success: false, error: 'Invalid args for "terminal-input": expected an object' };
1758
- }
1759
-
1760
- const inputArgs = args as Partial<{ paneId: string; data: string }>;
1761
- if (!inputArgs.paneId || !inputArgs.data) {
1762
- return { success: false, error: 'Missing "paneId" or "data" for terminal-input' };
1763
- }
1764
-
1765
- try {
1766
- const { spawn: spawnProcess } = await import("node:child_process");
1767
- // Split on newlines and send Enter for each boundary (same as sendToTmux)
1768
- const parts = inputArgs.data.split(/\r\n|\r|\n/);
1769
- for (let i = 0; i < parts.length; i++) {
1770
- const part = parts[i];
1771
- if (part.length > 0) {
1772
- await new Promise<void>((resolve, reject) => {
1773
- const proc = spawnProcess("tmux", [
1774
- "send-keys",
1775
- "-t",
1776
- inputArgs.paneId!,
1777
- "-l",
1778
- part,
1779
- ]);
1780
- proc.on("close", (code) =>
1781
- code === 0 ? resolve() : reject(new Error(`tmux exit ${code}`)),
1782
- );
1783
- proc.on("error", reject);
1784
- });
1785
- }
1786
- if (i < parts.length - 1) {
1787
- await new Promise<void>((resolve, reject) => {
1788
- const proc = spawnProcess("tmux", ["send-keys", "-t", inputArgs.paneId!, "Enter"]);
1789
- proc.on("close", (code) =>
1790
- code === 0 ? resolve() : reject(new Error(`tmux exit ${code}`)),
1791
- );
1792
- proc.on("error", reject);
1793
- });
1794
- }
1795
- }
1796
- return { success: true };
1797
- } catch (err) {
1798
- return { success: false, error: `terminal-input failed: ${(err as Error).message}` };
1799
- }
1800
- }
1801
-
1802
- default:
1803
- return {
1804
- success: false,
1805
- error: `Unknown command: ${command}`,
1806
- };
1807
- }
1808
- }
1809
- }