@creativeintelligence/abbie 0.1.4 → 0.1.5

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 (537) hide show
  1. package/dist/cli/commands/start.js +2 -1
  2. package/oclif.manifest.json +1 -1
  3. package/package.json +9 -5
  4. package/dist/cli/base-command.d.ts.map +0 -1
  5. package/dist/cli/commands/agent/list.d.ts.map +0 -1
  6. package/dist/cli/commands/annotation/ack.d.ts.map +0 -1
  7. package/dist/cli/commands/annotation/create.d.ts.map +0 -1
  8. package/dist/cli/commands/annotation/events.d.ts.map +0 -1
  9. package/dist/cli/commands/annotation/export.d.ts.map +0 -1
  10. package/dist/cli/commands/annotation/import.d.ts.map +0 -1
  11. package/dist/cli/commands/annotation/ingest.d.ts.map +0 -1
  12. package/dist/cli/commands/annotation/list.d.ts.map +0 -1
  13. package/dist/cli/commands/annotation/reply.d.ts.map +0 -1
  14. package/dist/cli/commands/annotation/resolve.d.ts.map +0 -1
  15. package/dist/cli/commands/auto/index.d.ts.map +0 -1
  16. package/dist/cli/commands/backlog/add.d.ts.map +0 -1
  17. package/dist/cli/commands/backlog/claim.d.ts.map +0 -1
  18. package/dist/cli/commands/backlog/complete.d.ts.map +0 -1
  19. package/dist/cli/commands/backlog/list.d.ts.map +0 -1
  20. package/dist/cli/commands/backlog/pick.d.ts.map +0 -1
  21. package/dist/cli/commands/backlog/sync.d.ts.map +0 -1
  22. package/dist/cli/commands/bootstrap.d.ts.map +0 -1
  23. package/dist/cli/commands/bridge.d.ts.map +0 -1
  24. package/dist/cli/commands/context/inject.d.ts.map +0 -1
  25. package/dist/cli/commands/context/list.d.ts.map +0 -1
  26. package/dist/cli/commands/context/publish.d.ts.map +0 -1
  27. package/dist/cli/commands/context/read.d.ts.map +0 -1
  28. package/dist/cli/commands/daemon.d.ts.map +0 -1
  29. package/dist/cli/commands/digest/index.d.ts.map +0 -1
  30. package/dist/cli/commands/docs/lint.d.ts.map +0 -1
  31. package/dist/cli/commands/docs/sync.d.ts.map +0 -1
  32. package/dist/cli/commands/doctor.d.ts.map +0 -1
  33. package/dist/cli/commands/find/index.d.ts.map +0 -1
  34. package/dist/cli/commands/gc.d.ts.map +0 -1
  35. package/dist/cli/commands/history/index.d.ts.map +0 -1
  36. package/dist/cli/commands/hooks/guard.d.ts.map +0 -1
  37. package/dist/cli/commands/hooks/list.d.ts.map +0 -1
  38. package/dist/cli/commands/hooks/lock.d.ts.map +0 -1
  39. package/dist/cli/commands/hooks/status.d.ts.map +0 -1
  40. package/dist/cli/commands/hooks/test.d.ts.map +0 -1
  41. package/dist/cli/commands/hooks/unlock.d.ts.map +0 -1
  42. package/dist/cli/commands/index.d.ts.map +0 -1
  43. package/dist/cli/commands/list.d.ts.map +0 -1
  44. package/dist/cli/commands/list.e2e.test.d.ts +0 -1
  45. package/dist/cli/commands/list.e2e.test.js +0 -47
  46. package/dist/cli/commands/login.d.ts.map +0 -1
  47. package/dist/cli/commands/logout.d.ts.map +0 -1
  48. package/dist/cli/commands/panes/broker.d.ts.map +0 -1
  49. package/dist/cli/commands/panes/pipe-sink.d.ts.map +0 -1
  50. package/dist/cli/commands/panes/snapshot.d.ts.map +0 -1
  51. package/dist/cli/commands/plan.d.ts.map +0 -1
  52. package/dist/cli/commands/plan.e2e.test.d.ts +0 -1
  53. package/dist/cli/commands/plan.e2e.test.js +0 -74
  54. package/dist/cli/commands/preview/index.d.ts.map +0 -1
  55. package/dist/cli/commands/preview/init.d.ts.map +0 -1
  56. package/dist/cli/commands/preview/list.d.ts.map +0 -1
  57. package/dist/cli/commands/preview/status.d.ts.map +0 -1
  58. package/dist/cli/commands/preview/stop.d.ts.map +0 -1
  59. package/dist/cli/commands/preview/sync.d.ts.map +0 -1
  60. package/dist/cli/commands/preview/watch.d.ts.map +0 -1
  61. package/dist/cli/commands/project/add.d.ts.map +0 -1
  62. package/dist/cli/commands/project/list.d.ts.map +0 -1
  63. package/dist/cli/commands/project/remove.d.ts.map +0 -1
  64. package/dist/cli/commands/push.d.ts.map +0 -1
  65. package/dist/cli/commands/reference/add.d.ts.map +0 -1
  66. package/dist/cli/commands/reference/delete.d.ts.map +0 -1
  67. package/dist/cli/commands/reference/extract.d.ts.map +0 -1
  68. package/dist/cli/commands/reference/list.d.ts.map +0 -1
  69. package/dist/cli/commands/reference/normalize.d.ts.map +0 -1
  70. package/dist/cli/commands/reference/open.d.ts.map +0 -1
  71. package/dist/cli/commands/reference/save.d.ts.map +0 -1
  72. package/dist/cli/commands/reference/search.d.ts.map +0 -1
  73. package/dist/cli/commands/reference/show.d.ts.map +0 -1
  74. package/dist/cli/commands/reference/update-index.d.ts.map +0 -1
  75. package/dist/cli/commands/reference/update.d.ts.map +0 -1
  76. package/dist/cli/commands/report/blocked.d.ts.map +0 -1
  77. package/dist/cli/commands/report/complete.d.ts.map +0 -1
  78. package/dist/cli/commands/report/progress.d.ts.map +0 -1
  79. package/dist/cli/commands/report/start.d.ts.map +0 -1
  80. package/dist/cli/commands/resource/acquire.d.ts.map +0 -1
  81. package/dist/cli/commands/resource/list.d.ts.map +0 -1
  82. package/dist/cli/commands/resource/release.d.ts.map +0 -1
  83. package/dist/cli/commands/resource/wait.d.ts.map +0 -1
  84. package/dist/cli/commands/review.d.ts.map +0 -1
  85. package/dist/cli/commands/session/attach.d.ts.map +0 -1
  86. package/dist/cli/commands/session/await.d.ts.map +0 -1
  87. package/dist/cli/commands/session/complete.d.ts.map +0 -1
  88. package/dist/cli/commands/session/create.d.ts.map +0 -1
  89. package/dist/cli/commands/session/heartbeat.d.ts.map +0 -1
  90. package/dist/cli/commands/session/list.d.ts.map +0 -1
  91. package/dist/cli/commands/session/mark-done.d.ts.map +0 -1
  92. package/dist/cli/commands/session/mine.d.ts.map +0 -1
  93. package/dist/cli/commands/session/replay.d.ts.map +0 -1
  94. package/dist/cli/commands/session/run.d.ts.map +0 -1
  95. package/dist/cli/commands/session/show.d.ts.map +0 -1
  96. package/dist/cli/commands/session/start.d.ts.map +0 -1
  97. package/dist/cli/commands/session/state/cleanup.d.ts.map +0 -1
  98. package/dist/cli/commands/session/state/end.d.ts.map +0 -1
  99. package/dist/cli/commands/session/state/get.d.ts.map +0 -1
  100. package/dist/cli/commands/session/state/init.d.ts.map +0 -1
  101. package/dist/cli/commands/session/state/list.d.ts.map +0 -1
  102. package/dist/cli/commands/session/state/update.d.ts.map +0 -1
  103. package/dist/cli/commands/session/stop.d.ts.map +0 -1
  104. package/dist/cli/commands/session/view.d.ts.map +0 -1
  105. package/dist/cli/commands/start.d.ts.map +0 -1
  106. package/dist/cli/commands/state/dump.d.ts.map +0 -1
  107. package/dist/cli/commands/status.d.ts.map +0 -1
  108. package/dist/cli/commands/sync.d.ts.map +0 -1
  109. package/dist/cli/commands/trace/export.d.ts.map +0 -1
  110. package/dist/cli/commands/triage/claim.d.ts.map +0 -1
  111. package/dist/cli/commands/triage/list.d.ts.map +0 -1
  112. package/dist/cli/commands/triage/next.d.ts.map +0 -1
  113. package/dist/cli/commands/triage/pull.d.ts.map +0 -1
  114. package/dist/cli/commands/triage/stats.d.ts.map +0 -1
  115. package/dist/cli/commands/tunnel/list.d.ts.map +0 -1
  116. package/dist/cli/commands/tunnel/start.d.ts.map +0 -1
  117. package/dist/cli/commands/tunnel/stop.d.ts.map +0 -1
  118. package/dist/cli/commands/tunnel/url.d.ts.map +0 -1
  119. package/dist/cli/commands/web/start.d.ts.map +0 -1
  120. package/dist/cli/commands/windows/context.d.ts.map +0 -1
  121. package/dist/cli/commands/windows/focus.d.ts.map +0 -1
  122. package/dist/cli/commands/windows/list.d.ts.map +0 -1
  123. package/dist/cli/commands/windows/map.d.ts.map +0 -1
  124. package/dist/cli/commands/windows/read.d.ts.map +0 -1
  125. package/dist/cli/commands/windows/search.d.ts.map +0 -1
  126. package/dist/cli/commands/windows/show.d.ts.map +0 -1
  127. package/dist/cli/commands/windows/watch.d.ts.map +0 -1
  128. package/dist/lib/active-sessions.d.ts.map +0 -1
  129. package/dist/lib/agent-adapters.d.ts.map +0 -1
  130. package/dist/lib/agent-sessions.d.ts.map +0 -1
  131. package/dist/lib/agent-trace.d.ts.map +0 -1
  132. package/dist/lib/analytics.d.ts.map +0 -1
  133. package/dist/lib/annotations-convex.d.ts.map +0 -1
  134. package/dist/lib/annotations.d.ts.map +0 -1
  135. package/dist/lib/auto/discover.d.ts.map +0 -1
  136. package/dist/lib/auto/ideate.d.ts.map +0 -1
  137. package/dist/lib/auto/spawn.d.ts.map +0 -1
  138. package/dist/lib/auto/workspace.d.ts.map +0 -1
  139. package/dist/lib/backlog.d.ts.map +0 -1
  140. package/dist/lib/backlog.test.d.ts +0 -1
  141. package/dist/lib/backlog.test.js +0 -162
  142. package/dist/lib/config-loader.d.ts.map +0 -1
  143. package/dist/lib/config-sync/adapters/claude.d.ts.map +0 -1
  144. package/dist/lib/config-sync/adapters/codex.d.ts.map +0 -1
  145. package/dist/lib/config-sync/adapters/copilot.d.ts.map +0 -1
  146. package/dist/lib/config-sync/adapters/gemini.d.ts.map +0 -1
  147. package/dist/lib/config-sync/adapters/index.d.ts.map +0 -1
  148. package/dist/lib/config-sync/adapters/opencode.d.ts.map +0 -1
  149. package/dist/lib/config-sync/index.d.ts.map +0 -1
  150. package/dist/lib/config-sync/types.d.ts.map +0 -1
  151. package/dist/lib/config-sync/writer.d.ts.map +0 -1
  152. package/dist/lib/content-sync/index.d.ts.map +0 -1
  153. package/dist/lib/content-sync/types.d.ts.map +0 -1
  154. package/dist/lib/contracts.d.ts.map +0 -1
  155. package/dist/lib/convex.d.ts.map +0 -1
  156. package/dist/lib/device.d.ts.map +0 -1
  157. package/dist/lib/digest/index.d.ts.map +0 -1
  158. package/dist/lib/docs/lint.d.ts.map +0 -1
  159. package/dist/lib/docs/lint.test.d.ts +0 -1
  160. package/dist/lib/docs/lint.test.js +0 -120
  161. package/dist/lib/docs/sync.d.ts.map +0 -1
  162. package/dist/lib/doctor/index.d.ts.map +0 -1
  163. package/dist/lib/doctor/repos.d.ts.map +0 -1
  164. package/dist/lib/doctor/templates.d.ts.map +0 -1
  165. package/dist/lib/errors.d.ts.map +0 -1
  166. package/dist/lib/events.d.ts.map +0 -1
  167. package/dist/lib/heartbeat.d.ts.map +0 -1
  168. package/dist/lib/heartbeat.test.d.ts +0 -1
  169. package/dist/lib/heartbeat.test.js +0 -124
  170. package/dist/lib/hooks/adapters/claude.d.ts.map +0 -1
  171. package/dist/lib/hooks/adapters/codex.d.ts.map +0 -1
  172. package/dist/lib/hooks/adapters/copilot.d.ts.map +0 -1
  173. package/dist/lib/hooks/context.d.ts.map +0 -1
  174. package/dist/lib/hooks/index.d.ts.map +0 -1
  175. package/dist/lib/hooks/registry.d.ts.map +0 -1
  176. package/dist/lib/hooks/runner.d.ts.map +0 -1
  177. package/dist/lib/hooks/types.d.ts.map +0 -1
  178. package/dist/lib/index.test.d.ts +0 -1
  179. package/dist/lib/index.test.js +0 -334
  180. package/dist/lib/managed-session.d.ts.map +0 -1
  181. package/dist/lib/math.d.ts.map +0 -1
  182. package/dist/lib/math.test.d.ts +0 -1
  183. package/dist/lib/math.test.js +0 -238
  184. package/dist/lib/ngrok.d.ts.map +0 -1
  185. package/dist/lib/nvim/discovery.d.ts.map +0 -1
  186. package/dist/lib/nvim/discovery.test.d.ts +0 -1
  187. package/dist/lib/nvim/discovery.test.js +0 -131
  188. package/dist/lib/nvim/index.d.ts.map +0 -1
  189. package/dist/lib/nvim/remote.d.ts.map +0 -1
  190. package/dist/lib/nvim/remote.test.d.ts +0 -1
  191. package/dist/lib/nvim/remote.test.js +0 -18
  192. package/dist/lib/panes/broker.d.ts.map +0 -1
  193. package/dist/lib/panes/index.d.ts.map +0 -1
  194. package/dist/lib/panes/server.d.ts.map +0 -1
  195. package/dist/lib/preview/detect.d.ts.map +0 -1
  196. package/dist/lib/preview/index.d.ts.map +0 -1
  197. package/dist/lib/preview/manager.d.ts.map +0 -1
  198. package/dist/lib/preview/schema.d.ts.map +0 -1
  199. package/dist/lib/preview/sprite.d.ts.map +0 -1
  200. package/dist/lib/preview/watcher.d.ts.map +0 -1
  201. package/dist/lib/process/index.d.ts.map +0 -1
  202. package/dist/lib/process/snapshot.d.ts.map +0 -1
  203. package/dist/lib/process/snapshot.test.d.ts +0 -1
  204. package/dist/lib/process/snapshot.test.js +0 -127
  205. package/dist/lib/project-identity.d.ts.map +0 -1
  206. package/dist/lib/provider-auth.d.ts.map +0 -1
  207. package/dist/lib/references.d.ts.map +0 -1
  208. package/dist/lib/report.d.ts.map +0 -1
  209. package/dist/lib/resources.d.ts.map +0 -1
  210. package/dist/lib/resources.test.d.ts +0 -1
  211. package/dist/lib/resources.test.js +0 -94
  212. package/dist/lib/runner.d.ts.map +0 -1
  213. package/dist/lib/runner.test.d.ts +0 -1
  214. package/dist/lib/runner.test.js +0 -234
  215. package/dist/lib/session-artifacts.d.ts.map +0 -1
  216. package/dist/lib/session-manager.d.ts.map +0 -1
  217. package/dist/lib/session-manager.test.d.ts +0 -1
  218. package/dist/lib/session-manager.test.js +0 -310
  219. package/dist/lib/session-result.d.ts.map +0 -1
  220. package/dist/lib/session-state.d.ts.map +0 -1
  221. package/dist/lib/slack-workspace.d.ts.map +0 -1
  222. package/dist/lib/tmux/bridge.d.ts.map +0 -1
  223. package/dist/lib/tmux/context.d.ts.map +0 -1
  224. package/dist/lib/tmux/context.test.d.ts +0 -1
  225. package/dist/lib/tmux/context.test.js +0 -215
  226. package/dist/lib/tmux/index.d.ts.map +0 -1
  227. package/dist/lib/tmux/map.d.ts.map +0 -1
  228. package/dist/lib/tmux/map.test.d.ts +0 -1
  229. package/dist/lib/tmux/map.test.js +0 -80
  230. package/dist/lib/tmux/panes.d.ts.map +0 -1
  231. package/dist/lib/tmux/redaction.d.ts.map +0 -1
  232. package/dist/lib/tmux/redaction.test.d.ts +0 -1
  233. package/dist/lib/tmux/redaction.test.js +0 -183
  234. package/dist/lib/triage-llm.d.ts.map +0 -1
  235. package/dist/lib/triage-tracker.d.ts.map +0 -1
  236. package/dist/lib/triage.d.ts.map +0 -1
  237. package/dist/lib/types.d.ts.map +0 -1
  238. package/dist/lib/windows/index.d.ts.map +0 -1
  239. package/dist/lib/windows/inventory.d.ts.map +0 -1
  240. package/dist/lib/windows/inventory.test.d.ts +0 -1
  241. package/dist/lib/windows/inventory.test.js +0 -292
  242. package/dist/lib/windows/types.d.ts.map +0 -1
  243. package/dist/lib.d.ts.map +0 -1
  244. package/dist/web/app/@overlay/default.d.ts +0 -1
  245. package/dist/web/app/@overlay/default.js +0 -3
  246. package/dist/web/app/about/page.d.ts +0 -1
  247. package/dist/web/app/about/page.js +0 -6
  248. package/dist/web/app/layout.d.ts +0 -7
  249. package/dist/web/app/layout.js +0 -19
  250. package/dist/web/app/page.d.ts +0 -1
  251. package/dist/web/app/page.js +0 -38
  252. package/dist/web/app/settings/page.d.ts +0 -1
  253. package/dist/web/app/settings/page.js +0 -6
  254. package/dist/web/app/shortcuts/page.d.ts +0 -1
  255. package/dist/web/app/shortcuts/page.js +0 -6
  256. package/dist/web/atoms/sidebar.d.ts +0 -16
  257. package/dist/web/atoms/sidebar.js +0 -58
  258. package/dist/web/atoms/state.d.ts +0 -24
  259. package/dist/web/atoms/state.js +0 -43
  260. package/dist/web/components/command-palette.d.ts +0 -6
  261. package/dist/web/components/command-palette.js +0 -297
  262. package/dist/web/components/main-view.d.ts +0 -1
  263. package/dist/web/components/main-view.js +0 -93
  264. package/dist/web/components/overlay/page-overlay.d.ts +0 -16
  265. package/dist/web/components/overlay/page-overlay.js +0 -84
  266. package/dist/web/components/pane-view.d.ts +0 -5
  267. package/dist/web/components/pane-view.js +0 -168
  268. package/dist/web/components/project-files-view.d.ts +0 -6
  269. package/dist/web/components/project-files-view.js +0 -205
  270. package/dist/web/components/project-planning-view.d.ts +0 -6
  271. package/dist/web/components/project-planning-view.js +0 -146
  272. package/dist/web/components/session-view.d.ts +0 -6
  273. package/dist/web/components/session-view.js +0 -211
  274. package/dist/web/components/sessions-list-view.d.ts +0 -1
  275. package/dist/web/components/sessions-list-view.js +0 -118
  276. package/dist/web/components/sidebar/index.d.ts +0 -9
  277. package/dist/web/components/sidebar/index.js +0 -29
  278. package/dist/web/components/sidebar/kbd.d.ts +0 -17
  279. package/dist/web/components/sidebar/kbd.js +0 -47
  280. package/dist/web/components/sidebar/nav-group.d.ts +0 -22
  281. package/dist/web/components/sidebar/nav-group.js +0 -43
  282. package/dist/web/components/sidebar/nav-item.d.ts +0 -17
  283. package/dist/web/components/sidebar/nav-item.js +0 -38
  284. package/dist/web/components/sidebar/sidebar-header.d.ts +0 -9
  285. package/dist/web/components/sidebar/sidebar-header.js +0 -18
  286. package/dist/web/components/sidebar/sidebar-nav.d.ts +0 -7
  287. package/dist/web/components/sidebar/sidebar-nav.js +0 -505
  288. package/dist/web/components/sidebar/sidebar-search.d.ts +0 -7
  289. package/dist/web/components/sidebar/sidebar-search.js +0 -18
  290. package/dist/web/components/sidebar/sidebar-settings.d.ts +0 -4
  291. package/dist/web/components/sidebar/sidebar-settings.js +0 -28
  292. package/dist/web/components/sidebar/sidebar-transition.d.ts +0 -12
  293. package/dist/web/components/sidebar/sidebar-transition.js +0 -58
  294. package/dist/web/components/status-bar.d.ts +0 -1
  295. package/dist/web/components/status-bar.js +0 -53
  296. package/dist/web/components/terminal.d.ts +0 -4
  297. package/dist/web/components/terminal.js +0 -324
  298. package/dist/web/components/toast.d.ts +0 -7
  299. package/dist/web/components/toast.js +0 -72
  300. package/dist/web/hooks/use-keybindings.d.ts +0 -30
  301. package/dist/web/hooks/use-keybindings.js +0 -322
  302. package/dist/web/hooks/use-state.d.ts +0 -11
  303. package/dist/web/hooks/use-state.js +0 -84
  304. package/dist/web/lib/api.d.ts +0 -179
  305. package/dist/web/lib/api.js +0 -79
  306. package/dist/web/lib/paths.d.ts +0 -2
  307. package/dist/web/lib/paths.js +0 -26
  308. package/dist/web/lib/utils.d.ts +0 -2
  309. package/dist/web/lib/utils.js +0 -5
  310. package/dist/web/lib/ws.d.ts +0 -18
  311. package/dist/web/lib/ws.js +0 -138
  312. package/dist/web/next.config.d.ts +0 -3
  313. package/dist/web/next.config.js +0 -9
  314. package/scripts/generate-manifest.ts +0 -44
  315. package/src/cli/base-command.ts +0 -233
  316. package/src/cli/commands/__tests__/annotations.test.ts +0 -704
  317. package/src/cli/commands/__tests__/bridge.test.ts +0 -101
  318. package/src/cli/commands/__tests__/daemon.test.ts +0 -79
  319. package/src/cli/commands/agent/list.ts +0 -75
  320. package/src/cli/commands/annotation/ack.ts +0 -37
  321. package/src/cli/commands/annotation/create.ts +0 -67
  322. package/src/cli/commands/annotation/events.ts +0 -63
  323. package/src/cli/commands/annotation/export.ts +0 -67
  324. package/src/cli/commands/annotation/import.ts +0 -98
  325. package/src/cli/commands/annotation/ingest.ts +0 -112
  326. package/src/cli/commands/annotation/list.ts +0 -57
  327. package/src/cli/commands/annotation/reply.ts +0 -46
  328. package/src/cli/commands/annotation/resolve.ts +0 -37
  329. package/src/cli/commands/auto/index.ts +0 -755
  330. package/src/cli/commands/backlog/add.ts +0 -74
  331. package/src/cli/commands/backlog/claim.ts +0 -53
  332. package/src/cli/commands/backlog/complete.ts +0 -51
  333. package/src/cli/commands/backlog/list.ts +0 -107
  334. package/src/cli/commands/backlog/pick.ts +0 -50
  335. package/src/cli/commands/backlog/sync.ts +0 -131
  336. package/src/cli/commands/bootstrap.ts +0 -205
  337. package/src/cli/commands/bridge.ts +0 -233
  338. package/src/cli/commands/context/inject.ts +0 -103
  339. package/src/cli/commands/context/list.ts +0 -112
  340. package/src/cli/commands/context/publish.ts +0 -83
  341. package/src/cli/commands/context/read.ts +0 -85
  342. package/src/cli/commands/daemon.ts +0 -1809
  343. package/src/cli/commands/digest/index.ts +0 -245
  344. package/src/cli/commands/docs/lint.ts +0 -93
  345. package/src/cli/commands/docs/sync.ts +0 -90
  346. package/src/cli/commands/doctor.ts +0 -267
  347. package/src/cli/commands/find/index.ts +0 -313
  348. package/src/cli/commands/history/index.ts +0 -269
  349. package/src/cli/commands/hooks/guard.ts +0 -71
  350. package/src/cli/commands/hooks/list.ts +0 -139
  351. package/src/cli/commands/hooks/lock.ts +0 -47
  352. package/src/cli/commands/hooks/status.ts +0 -56
  353. package/src/cli/commands/hooks/test.ts +0 -190
  354. package/src/cli/commands/hooks/unlock.ts +0 -56
  355. package/src/cli/commands/list.e2e.test.ts +0 -58
  356. package/src/cli/commands/list.ts +0 -96
  357. package/src/cli/commands/login.ts +0 -236
  358. package/src/cli/commands/logout.ts +0 -45
  359. package/src/cli/commands/panes/broker.ts +0 -68
  360. package/src/cli/commands/panes/pipe-sink.ts +0 -101
  361. package/src/cli/commands/panes/snapshot.ts +0 -156
  362. package/src/cli/commands/plan.e2e.test.ts +0 -90
  363. package/src/cli/commands/plan.ts +0 -68
  364. package/src/cli/commands/preview/index.ts +0 -282
  365. package/src/cli/commands/preview/list.ts +0 -116
  366. package/src/cli/commands/preview/status.ts +0 -137
  367. package/src/cli/commands/preview/stop.ts +0 -165
  368. package/src/cli/commands/project/add.ts +0 -110
  369. package/src/cli/commands/project/list.ts +0 -136
  370. package/src/cli/commands/project/remove.ts +0 -60
  371. package/src/cli/commands/push.ts +0 -115
  372. package/src/cli/commands/reference/add.ts +0 -266
  373. package/src/cli/commands/reference/delete.ts +0 -67
  374. package/src/cli/commands/reference/extract.ts +0 -389
  375. package/src/cli/commands/reference/list.ts +0 -117
  376. package/src/cli/commands/reference/normalize.ts +0 -90
  377. package/src/cli/commands/reference/open.ts +0 -92
  378. package/src/cli/commands/reference/save.ts +0 -103
  379. package/src/cli/commands/reference/search.ts +0 -85
  380. package/src/cli/commands/reference/show.ts +0 -174
  381. package/src/cli/commands/reference/update-index.ts +0 -103
  382. package/src/cli/commands/reference/update.ts +0 -136
  383. package/src/cli/commands/report/blocked.ts +0 -165
  384. package/src/cli/commands/report/complete.ts +0 -179
  385. package/src/cli/commands/report/progress.ts +0 -142
  386. package/src/cli/commands/report/start.ts +0 -140
  387. package/src/cli/commands/resource/acquire.ts +0 -116
  388. package/src/cli/commands/resource/list.ts +0 -77
  389. package/src/cli/commands/resource/release.ts +0 -64
  390. package/src/cli/commands/resource/wait.ts +0 -93
  391. package/src/cli/commands/review.ts +0 -105
  392. package/src/cli/commands/session/attach.ts +0 -200
  393. package/src/cli/commands/session/await.ts +0 -83
  394. package/src/cli/commands/session/complete.ts +0 -100
  395. package/src/cli/commands/session/create.ts +0 -92
  396. package/src/cli/commands/session/heartbeat.ts +0 -63
  397. package/src/cli/commands/session/list.ts +0 -281
  398. package/src/cli/commands/session/mark-done.ts +0 -132
  399. package/src/cli/commands/session/mine.ts +0 -189
  400. package/src/cli/commands/session/replay.ts +0 -659
  401. package/src/cli/commands/session/run.ts +0 -44
  402. package/src/cli/commands/session/show.ts +0 -177
  403. package/src/cli/commands/session/start.ts +0 -580
  404. package/src/cli/commands/session/state/cleanup.ts +0 -61
  405. package/src/cli/commands/session/state/end.ts +0 -47
  406. package/src/cli/commands/session/state/get.ts +0 -65
  407. package/src/cli/commands/session/state/init.ts +0 -50
  408. package/src/cli/commands/session/state/list.ts +0 -68
  409. package/src/cli/commands/session/state/update.ts +0 -108
  410. package/src/cli/commands/session/stop.ts +0 -134
  411. package/src/cli/commands/session/view.ts +0 -186
  412. package/src/cli/commands/start.ts +0 -256
  413. package/src/cli/commands/state/dump.ts +0 -449
  414. package/src/cli/commands/status.ts +0 -244
  415. package/src/cli/commands/sync.ts +0 -174
  416. package/src/cli/commands/trace/export.ts +0 -255
  417. package/src/cli/commands/triage/claim.ts +0 -203
  418. package/src/cli/commands/triage/list.ts +0 -137
  419. package/src/cli/commands/triage/next.ts +0 -73
  420. package/src/cli/commands/triage/pull.ts +0 -97
  421. package/src/cli/commands/triage/stats.ts +0 -82
  422. package/src/cli/commands/tunnel/list.ts +0 -113
  423. package/src/cli/commands/tunnel/start.ts +0 -122
  424. package/src/cli/commands/tunnel/stop.ts +0 -108
  425. package/src/cli/commands/tunnel/url.ts +0 -82
  426. package/src/cli/commands/web/start.ts +0 -125
  427. package/src/cli/commands/windows/context.ts +0 -439
  428. package/src/cli/commands/windows/focus.ts +0 -130
  429. package/src/cli/commands/windows/list.ts +0 -213
  430. package/src/cli/commands/windows/map.ts +0 -223
  431. package/src/cli/commands/windows/read.ts +0 -279
  432. package/src/cli/commands/windows/search.ts +0 -219
  433. package/src/cli/commands/windows/show.ts +0 -188
  434. package/src/cli/commands/windows/watch.ts +0 -262
  435. package/src/lib/__tests__/annotations-convex.test.ts +0 -104
  436. package/src/lib/active-sessions.ts +0 -1486
  437. package/src/lib/agent-adapters.ts +0 -412
  438. package/src/lib/agent-sessions.ts +0 -671
  439. package/src/lib/agent-trace.test.ts +0 -296
  440. package/src/lib/agent-trace.ts +0 -513
  441. package/src/lib/analytics.ts +0 -178
  442. package/src/lib/annotations-convex.ts +0 -296
  443. package/src/lib/annotations.test.ts +0 -274
  444. package/src/lib/annotations.ts +0 -836
  445. package/src/lib/auto/discover.ts +0 -545
  446. package/src/lib/auto/ideate.ts +0 -412
  447. package/src/lib/auto/spawn.ts +0 -549
  448. package/src/lib/auto/workspace.ts +0 -282
  449. package/src/lib/backlog.test.ts +0 -194
  450. package/src/lib/backlog.ts +0 -428
  451. package/src/lib/config-loader.ts +0 -391
  452. package/src/lib/config-sync/adapters/claude.ts +0 -91
  453. package/src/lib/config-sync/adapters/codex.ts +0 -135
  454. package/src/lib/config-sync/adapters/copilot.ts +0 -98
  455. package/src/lib/config-sync/adapters/gemini.ts +0 -86
  456. package/src/lib/config-sync/adapters/index.ts +0 -39
  457. package/src/lib/config-sync/adapters/opencode.ts +0 -94
  458. package/src/lib/config-sync/index.ts +0 -399
  459. package/src/lib/config-sync/types.ts +0 -92
  460. package/src/lib/config-sync/writer.ts +0 -188
  461. package/src/lib/content-sync/index.ts +0 -882
  462. package/src/lib/content-sync/types.ts +0 -104
  463. package/src/lib/contracts.test.ts +0 -186
  464. package/src/lib/contracts.ts +0 -195
  465. package/src/lib/convex.ts +0 -54
  466. package/src/lib/device.ts +0 -41
  467. package/src/lib/digest/index.ts +0 -529
  468. package/src/lib/docs/lint.test.ts +0 -135
  469. package/src/lib/docs/lint.ts +0 -310
  470. package/src/lib/docs/sync.ts +0 -184
  471. package/src/lib/doctor/index.ts +0 -647
  472. package/src/lib/doctor/repos.ts +0 -381
  473. package/src/lib/doctor/templates.ts +0 -191
  474. package/src/lib/errors.ts +0 -111
  475. package/src/lib/events.ts +0 -479
  476. package/src/lib/heartbeat.test.ts +0 -164
  477. package/src/lib/heartbeat.ts +0 -326
  478. package/src/lib/hooks/adapters/claude.ts +0 -115
  479. package/src/lib/hooks/adapters/codex.ts +0 -92
  480. package/src/lib/hooks/adapters/copilot.ts +0 -141
  481. package/src/lib/hooks/context.ts +0 -174
  482. package/src/lib/hooks/index.ts +0 -39
  483. package/src/lib/hooks/registry.ts +0 -101
  484. package/src/lib/hooks/runner.ts +0 -208
  485. package/src/lib/hooks/types.ts +0 -89
  486. package/src/lib/index.test.ts +0 -426
  487. package/src/lib/managed-session.ts +0 -117
  488. package/src/lib/math.test.ts +0 -299
  489. package/src/lib/math.ts +0 -120
  490. package/src/lib/ngrok.ts +0 -441
  491. package/src/lib/nvim/discovery.test.ts +0 -157
  492. package/src/lib/nvim/discovery.ts +0 -181
  493. package/src/lib/nvim/index.ts +0 -28
  494. package/src/lib/nvim/remote.test.ts +0 -21
  495. package/src/lib/nvim/remote.ts +0 -184
  496. package/src/lib/panes/README.md +0 -20
  497. package/src/lib/panes/broker.ts +0 -261
  498. package/src/lib/panes/index.ts +0 -1
  499. package/src/lib/panes/server.ts +0 -379
  500. package/src/lib/preview/detect.ts +0 -184
  501. package/src/lib/preview/index.ts +0 -1
  502. package/src/lib/process/index.ts +0 -16
  503. package/src/lib/process/snapshot.test.ts +0 -188
  504. package/src/lib/process/snapshot.ts +0 -257
  505. package/src/lib/provider-auth.ts +0 -258
  506. package/src/lib/references.ts +0 -481
  507. package/src/lib/report.ts +0 -973
  508. package/src/lib/resources.test.ts +0 -132
  509. package/src/lib/resources.ts +0 -429
  510. package/src/lib/runner.test.ts +0 -288
  511. package/src/lib/runner.ts +0 -1223
  512. package/src/lib/session-artifacts.test.ts +0 -107
  513. package/src/lib/session-artifacts.ts +0 -193
  514. package/src/lib/session-manager.test.ts +0 -402
  515. package/src/lib/session-manager.ts +0 -150
  516. package/src/lib/session-result.test.ts +0 -98
  517. package/src/lib/session-result.ts +0 -262
  518. package/src/lib/session-state.ts +0 -470
  519. package/src/lib/slack-workspace.ts +0 -25
  520. package/src/lib/tmux/bridge.ts +0 -439
  521. package/src/lib/tmux/context.test.ts +0 -242
  522. package/src/lib/tmux/context.ts +0 -380
  523. package/src/lib/tmux/index.ts +0 -46
  524. package/src/lib/tmux/map.test.ts +0 -99
  525. package/src/lib/tmux/map.ts +0 -252
  526. package/src/lib/tmux/panes.ts +0 -170
  527. package/src/lib/tmux/redaction.test.ts +0 -231
  528. package/src/lib/tmux/redaction.ts +0 -201
  529. package/src/lib/triage-llm.ts +0 -312
  530. package/src/lib/triage-tracker.ts +0 -400
  531. package/src/lib/triage.ts +0 -655
  532. package/src/lib/types.ts +0 -61
  533. package/src/lib/windows/index.ts +0 -2
  534. package/src/lib/windows/inventory.test.ts +0 -370
  535. package/src/lib/windows/inventory.ts +0 -352
  536. package/src/lib/windows/types.ts +0 -46
  537. 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
- }