orcasynth 1.4.1 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/dist/advisor/mcpConfig.js +26 -0
  2. package/dist/advisor/service.js +74 -0
  3. package/dist/api/server.js +85 -6
  4. package/dist/cli/commands.js +26 -0
  5. package/dist/cli/index.js +9 -2
  6. package/dist/daemon/bootstrap.js +14 -1
  7. package/dist/integrations/projectFiles.js +16 -0
  8. package/dist/integrations/usage/claude.js +7 -7
  9. package/dist/integrations/usage/codex.js +5 -7
  10. package/dist/integrations/usage/index.js +2 -8
  11. package/dist/integrations/usage/walk.js +14 -0
  12. package/dist/mcp/server.js +34 -0
  13. package/dist/mcp/tools.js +17 -0
  14. package/dist/overseer/sessionInfo.js +7 -1
  15. package/dist/overseer/stuckDetector.js +4 -10
  16. package/dist/prompts/advisor.md +13 -0
  17. package/dist/shared/apiClient.js +23 -0
  18. package/dist/shared/id.js +7 -0
  19. package/dist/shared/time.js +12 -0
  20. package/dist/store/cascade.js +24 -0
  21. package/dist/store/db.js +7 -0
  22. package/dist/store/projectStore.js +8 -6
  23. package/dist/store/schema.sql +4 -2
  24. package/dist/store/taskStore.js +2 -8
  25. package/dist/store/userStore.js +26 -1
  26. package/dist/tmux/driver.js +8 -0
  27. package/dist/tmux/fakeDriver.js +9 -0
  28. package/package.json +4 -2
  29. package/prompts/advisor.md +13 -0
  30. package/web-dist/.next/BUILD_ID +1 -1
  31. package/web-dist/.next/build-manifest.json +3 -3
  32. package/web-dist/.next/server/app/_global-error.html +1 -1
  33. package/web-dist/.next/server/app/_global-error.rsc +1 -1
  34. package/web-dist/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  35. package/web-dist/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/web-dist/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/web-dist/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/web-dist/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/web-dist/.next/server/app/_not-found/page/react-loadable-manifest.json +10 -1
  40. package/web-dist/.next/server/app/_not-found/page.js.nft.json +1 -1
  41. package/web-dist/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  42. package/web-dist/.next/server/app/_not-found.html +1 -1
  43. package/web-dist/.next/server/app/_not-found.rsc +11 -11
  44. package/web-dist/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  45. package/web-dist/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  46. package/web-dist/.next/server/app/_not-found.segments/_index.segment.rsc +6 -6
  47. package/web-dist/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  48. package/web-dist/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  49. package/web-dist/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  50. package/web-dist/.next/server/app/account/page/react-loadable-manifest.json +10 -1
  51. package/web-dist/.next/server/app/account/page.js.nft.json +1 -1
  52. package/web-dist/.next/server/app/account/page_client-reference-manifest.js +1 -1
  53. package/web-dist/.next/server/app/account.html +1 -1
  54. package/web-dist/.next/server/app/account.rsc +13 -13
  55. package/web-dist/.next/server/app/account.segments/_full.segment.rsc +13 -13
  56. package/web-dist/.next/server/app/account.segments/_head.segment.rsc +4 -4
  57. package/web-dist/.next/server/app/account.segments/_index.segment.rsc +6 -6
  58. package/web-dist/.next/server/app/account.segments/_tree.segment.rsc +2 -2
  59. package/web-dist/.next/server/app/account.segments/account/__PAGE__.segment.rsc +4 -4
  60. package/web-dist/.next/server/app/account.segments/account.segment.rsc +3 -3
  61. package/web-dist/.next/server/app/dash/page/react-loadable-manifest.json +10 -1
  62. package/web-dist/.next/server/app/dash/page.js.nft.json +1 -1
  63. package/web-dist/.next/server/app/dash/page_client-reference-manifest.js +1 -1
  64. package/web-dist/.next/server/app/dash.html +1 -1
  65. package/web-dist/.next/server/app/dash.rsc +13 -13
  66. package/web-dist/.next/server/app/dash.segments/_full.segment.rsc +13 -13
  67. package/web-dist/.next/server/app/dash.segments/_head.segment.rsc +4 -4
  68. package/web-dist/.next/server/app/dash.segments/_index.segment.rsc +6 -6
  69. package/web-dist/.next/server/app/dash.segments/_tree.segment.rsc +2 -2
  70. package/web-dist/.next/server/app/dash.segments/dash/__PAGE__.segment.rsc +4 -4
  71. package/web-dist/.next/server/app/dash.segments/dash.segment.rsc +3 -3
  72. package/web-dist/.next/server/app/escalations/page/react-loadable-manifest.json +10 -1
  73. package/web-dist/.next/server/app/escalations/page.js.nft.json +1 -1
  74. package/web-dist/.next/server/app/escalations/page_client-reference-manifest.js +1 -1
  75. package/web-dist/.next/server/app/escalations.html +1 -1
  76. package/web-dist/.next/server/app/escalations.rsc +13 -13
  77. package/web-dist/.next/server/app/escalations.segments/_full.segment.rsc +13 -13
  78. package/web-dist/.next/server/app/escalations.segments/_head.segment.rsc +4 -4
  79. package/web-dist/.next/server/app/escalations.segments/_index.segment.rsc +6 -6
  80. package/web-dist/.next/server/app/escalations.segments/_tree.segment.rsc +2 -2
  81. package/web-dist/.next/server/app/escalations.segments/escalations/__PAGE__.segment.rsc +4 -4
  82. package/web-dist/.next/server/app/escalations.segments/escalations.segment.rsc +3 -3
  83. package/web-dist/.next/server/app/index.html +1 -1
  84. package/web-dist/.next/server/app/index.rsc +13 -13
  85. package/web-dist/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  86. package/web-dist/.next/server/app/index.segments/_full.segment.rsc +13 -13
  87. package/web-dist/.next/server/app/index.segments/_head.segment.rsc +4 -4
  88. package/web-dist/.next/server/app/index.segments/_index.segment.rsc +6 -6
  89. package/web-dist/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  90. package/web-dist/.next/server/app/kanban/page/react-loadable-manifest.json +10 -1
  91. package/web-dist/.next/server/app/kanban/page.js.nft.json +1 -1
  92. package/web-dist/.next/server/app/kanban/page_client-reference-manifest.js +1 -1
  93. package/web-dist/.next/server/app/kanban.html +1 -1
  94. package/web-dist/.next/server/app/kanban.rsc +13 -13
  95. package/web-dist/.next/server/app/kanban.segments/_full.segment.rsc +13 -13
  96. package/web-dist/.next/server/app/kanban.segments/_head.segment.rsc +4 -4
  97. package/web-dist/.next/server/app/kanban.segments/_index.segment.rsc +6 -6
  98. package/web-dist/.next/server/app/kanban.segments/_tree.segment.rsc +2 -2
  99. package/web-dist/.next/server/app/kanban.segments/kanban/__PAGE__.segment.rsc +4 -4
  100. package/web-dist/.next/server/app/kanban.segments/kanban.segment.rsc +3 -3
  101. package/web-dist/.next/server/app/onboarding/page/react-loadable-manifest.json +10 -1
  102. package/web-dist/.next/server/app/onboarding/page.js.nft.json +1 -1
  103. package/web-dist/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  104. package/web-dist/.next/server/app/onboarding.html +1 -1
  105. package/web-dist/.next/server/app/onboarding.rsc +13 -13
  106. package/web-dist/.next/server/app/onboarding.segments/_full.segment.rsc +13 -13
  107. package/web-dist/.next/server/app/onboarding.segments/_head.segment.rsc +4 -4
  108. package/web-dist/.next/server/app/onboarding.segments/_index.segment.rsc +6 -6
  109. package/web-dist/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
  110. package/web-dist/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +4 -4
  111. package/web-dist/.next/server/app/onboarding.segments/onboarding.segment.rsc +3 -3
  112. package/web-dist/.next/server/app/page/react-loadable-manifest.json +10 -1
  113. package/web-dist/.next/server/app/page.js.nft.json +1 -1
  114. package/web-dist/.next/server/app/page_client-reference-manifest.js +1 -1
  115. package/web-dist/.next/server/app/projects/page/react-loadable-manifest.json +8 -0
  116. package/web-dist/.next/server/app/projects/page.js.nft.json +1 -1
  117. package/web-dist/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  118. package/web-dist/.next/server/app/projects.html +1 -1
  119. package/web-dist/.next/server/app/projects.rsc +13 -13
  120. package/web-dist/.next/server/app/projects.segments/_full.segment.rsc +13 -13
  121. package/web-dist/.next/server/app/projects.segments/_head.segment.rsc +4 -4
  122. package/web-dist/.next/server/app/projects.segments/_index.segment.rsc +6 -6
  123. package/web-dist/.next/server/app/projects.segments/_tree.segment.rsc +2 -2
  124. package/web-dist/.next/server/app/projects.segments/projects/__PAGE__.segment.rsc +4 -4
  125. package/web-dist/.next/server/app/projects.segments/projects.segment.rsc +3 -3
  126. package/web-dist/.next/server/app/sessions/page/react-loadable-manifest.json +10 -1
  127. package/web-dist/.next/server/app/sessions/page.js.nft.json +1 -1
  128. package/web-dist/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  129. package/web-dist/.next/server/app/sessions.html +1 -1
  130. package/web-dist/.next/server/app/sessions.rsc +13 -13
  131. package/web-dist/.next/server/app/sessions.segments/_full.segment.rsc +13 -13
  132. package/web-dist/.next/server/app/sessions.segments/_head.segment.rsc +4 -4
  133. package/web-dist/.next/server/app/sessions.segments/_index.segment.rsc +6 -6
  134. package/web-dist/.next/server/app/sessions.segments/_tree.segment.rsc +2 -2
  135. package/web-dist/.next/server/app/sessions.segments/sessions/__PAGE__.segment.rsc +4 -4
  136. package/web-dist/.next/server/app/sessions.segments/sessions.segment.rsc +3 -3
  137. package/web-dist/.next/server/app/settings/page/react-loadable-manifest.json +10 -1
  138. package/web-dist/.next/server/app/settings/page.js.nft.json +1 -1
  139. package/web-dist/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  140. package/web-dist/.next/server/app/settings.html +1 -1
  141. package/web-dist/.next/server/app/settings.rsc +13 -13
  142. package/web-dist/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  143. package/web-dist/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  144. package/web-dist/.next/server/app/settings.segments/_index.segment.rsc +6 -6
  145. package/web-dist/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  146. package/web-dist/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  147. package/web-dist/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  148. package/web-dist/.next/server/app/tasks/page/react-loadable-manifest.json +10 -1
  149. package/web-dist/.next/server/app/tasks/page.js.nft.json +1 -1
  150. package/web-dist/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  151. package/web-dist/.next/server/app/tasks.html +1 -1
  152. package/web-dist/.next/server/app/tasks.rsc +13 -13
  153. package/web-dist/.next/server/app/tasks.segments/_full.segment.rsc +13 -13
  154. package/web-dist/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  155. package/web-dist/.next/server/app/tasks.segments/_index.segment.rsc +6 -6
  156. package/web-dist/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  157. package/web-dist/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  158. package/web-dist/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  159. package/web-dist/.next/server/app/timeline/page/react-loadable-manifest.json +10 -1
  160. package/web-dist/.next/server/app/timeline/page.js.nft.json +1 -1
  161. package/web-dist/.next/server/app/timeline/page_client-reference-manifest.js +1 -1
  162. package/web-dist/.next/server/app/timeline.html +1 -1
  163. package/web-dist/.next/server/app/timeline.rsc +13 -13
  164. package/web-dist/.next/server/app/timeline.segments/_full.segment.rsc +13 -13
  165. package/web-dist/.next/server/app/timeline.segments/_head.segment.rsc +4 -4
  166. package/web-dist/.next/server/app/timeline.segments/_index.segment.rsc +6 -6
  167. package/web-dist/.next/server/app/timeline.segments/_tree.segment.rsc +2 -2
  168. package/web-dist/.next/server/app/timeline.segments/timeline/__PAGE__.segment.rsc +4 -4
  169. package/web-dist/.next/server/app/timeline.segments/timeline.segment.rsc +3 -3
  170. package/web-dist/.next/server/app/users/page/react-loadable-manifest.json +10 -1
  171. package/web-dist/.next/server/app/users/page.js.nft.json +1 -1
  172. package/web-dist/.next/server/app/users/page_client-reference-manifest.js +1 -1
  173. package/web-dist/.next/server/app/users.html +1 -1
  174. package/web-dist/.next/server/app/users.rsc +13 -13
  175. package/web-dist/.next/server/app/users.segments/_full.segment.rsc +13 -13
  176. package/web-dist/.next/server/app/users.segments/_head.segment.rsc +4 -4
  177. package/web-dist/.next/server/app/users.segments/_index.segment.rsc +6 -6
  178. package/web-dist/.next/server/app/users.segments/_tree.segment.rsc +2 -2
  179. package/web-dist/.next/server/app/users.segments/users/__PAGE__.segment.rsc +4 -4
  180. package/web-dist/.next/server/app/users.segments/users.segment.rsc +3 -3
  181. package/web-dist/.next/server/chunks/[root-of-the-server]__0bvp8h1._.js +1 -1
  182. package/web-dist/.next/server/chunks/[root-of-the-server]__1dgbigm._.js +1 -1
  183. package/web-dist/.next/server/chunks/[root-of-the-server]__1wxxtv8._.js +1 -1
  184. package/web-dist/.next/server/chunks/ssr/[root-of-the-server]__0yfatub._.js +3 -0
  185. package/web-dist/.next/server/chunks/ssr/_015zf-4._.js +1 -1
  186. package/web-dist/.next/server/chunks/ssr/_01rh28z._.js +3 -0
  187. package/web-dist/.next/server/chunks/ssr/_04ezsju._.js +3 -0
  188. package/web-dist/.next/server/chunks/ssr/_04o_q14._.js +1 -1
  189. package/web-dist/.next/server/chunks/ssr/_057a06r._.js +1 -1
  190. package/web-dist/.next/server/chunks/ssr/_081ml1k._.js +3 -0
  191. package/web-dist/.next/server/chunks/ssr/_0afhsmf._.js +3 -0
  192. package/web-dist/.next/server/chunks/ssr/_0m2j9hu._.js +3 -0
  193. package/web-dist/.next/server/chunks/ssr/_0tzourm._.js +1 -1
  194. package/web-dist/.next/server/chunks/ssr/_0ysqykx._.js +3 -0
  195. package/web-dist/.next/server/chunks/ssr/_10ak-sh._.js +3 -0
  196. package/web-dist/.next/server/chunks/ssr/_13rgpyg._.js +3 -0
  197. package/web-dist/.next/server/chunks/ssr/_1fp8enw._.js +3 -0
  198. package/web-dist/.next/server/chunks/ssr/_1std18n._.js +3 -0
  199. package/web-dist/.next/server/chunks/ssr/_1xwktd-._.js +1 -1
  200. package/web-dist/.next/server/chunks/ssr/_1zd7t3t._.js +3 -0
  201. package/web-dist/.next/server/chunks/ssr/app_dash_page_tsx_12v0wx-._.js +1 -1
  202. package/web-dist/.next/server/chunks/ssr/app_kanban_page_tsx_06_8oyf._.js +1 -1
  203. package/web-dist/.next/server/chunks/ssr/app_projects_page_tsx_1w-8z74._.js +3 -3
  204. package/web-dist/.next/server/chunks/ssr/app_tasks_page_tsx_1p6mxbw._.js +1 -1
  205. package/web-dist/.next/server/chunks/ssr/app_timeline_page_tsx_0thxir0._.js +1 -1
  206. package/web-dist/.next/server/chunks/ssr/components_shell_Shell_tsx_1e5c27h._.js +1 -1
  207. package/web-dist/.next/server/chunks/ssr/{node_modules_0h91jdk._.js → node_modules_next_dist_client_components_0bew68i._.js} +2 -2
  208. package/web-dist/.next/server/middleware-build-manifest.js +3 -3
  209. package/web-dist/.next/server/pages/404.html +1 -1
  210. package/web-dist/.next/server/pages/500.html +1 -1
  211. package/web-dist/.next/static/chunks/09gkeu3bc4xo0.js +1 -0
  212. package/web-dist/.next/static/chunks/0c6iuw5yay1w0.js +1 -0
  213. package/web-dist/.next/static/chunks/0ccjus_sicyov.js +1 -0
  214. package/web-dist/.next/static/chunks/0gor0_p3jg67f.js +1 -0
  215. package/web-dist/.next/static/chunks/0kd16q0244sp5.js +1 -0
  216. package/web-dist/.next/static/chunks/0x0pwu4mealh1.js +1 -0
  217. package/web-dist/.next/static/chunks/0yg3wh0jczxoa.js +1 -0
  218. package/web-dist/.next/static/chunks/0zms_--zk-t3b.js +1 -0
  219. package/web-dist/.next/static/chunks/11xbsx12drypd.js +1 -0
  220. package/web-dist/.next/static/chunks/{18zkogw4aykzc.js → 14mmxdnhdicgy.js} +1 -1
  221. package/web-dist/.next/static/chunks/1bgv4d9v71ij7.js +1 -0
  222. package/web-dist/.next/static/chunks/1j2hh1hlkxrip.js +1 -0
  223. package/web-dist/.next/static/chunks/2c16uuyhfnhr9.js +1 -0
  224. package/web-dist/.next/static/chunks/2g_lldfaqo8bq.js +1 -0
  225. package/web-dist/.next/static/chunks/2gak7jay2im1l.js +1 -0
  226. package/web-dist/.next/static/chunks/2pbhzo7fu4xcs.js +11 -0
  227. package/web-dist/.next/static/chunks/2z02etl-1qi3g.js +1 -0
  228. package/web-dist/.next/static/chunks/2zutw3iy49kee.js +1 -0
  229. package/web-dist/.next/static/chunks/30ztbacooyerd.js +1 -0
  230. package/web-dist/.next/static/chunks/33tqcj2ra3wol.js +1 -0
  231. package/web-dist/.next/static/chunks/34b41uiwytome.css +2 -0
  232. package/web-dist/.next/static/chunks/3uf01y_a4cq8y.js +1 -0
  233. package/web-dist/.next/server/chunks/ssr/[root-of-the-server]__1tooevx._.js +0 -3
  234. package/web-dist/.next/server/chunks/ssr/_085pshu._.js +0 -3
  235. package/web-dist/.next/server/chunks/ssr/_09x5h4x._.js +0 -3
  236. package/web-dist/.next/server/chunks/ssr/_0j9ppt0._.js +0 -3
  237. package/web-dist/.next/server/chunks/ssr/_0lctmoh._.js +0 -3
  238. package/web-dist/.next/server/chunks/ssr/_0tc9z5_._.js +0 -3
  239. package/web-dist/.next/server/chunks/ssr/_0zho8fx._.js +0 -3
  240. package/web-dist/.next/server/chunks/ssr/_1812xdn._.js +0 -3
  241. package/web-dist/.next/server/chunks/ssr/_1kom56q._.js +0 -3
  242. package/web-dist/.next/server/chunks/ssr/_1m2qx8p._.js +0 -3
  243. package/web-dist/.next/server/chunks/ssr/_1mjzb9s._.js +0 -3
  244. package/web-dist/.next/server/chunks/ssr/_1w24b42._.js +0 -3
  245. package/web-dist/.next/server/chunks/ssr/app_sessions_page_tsx_0r1_4_3._.js +0 -3
  246. package/web-dist/.next/static/chunks/05jfk0-07tiga.js +0 -11
  247. package/web-dist/.next/static/chunks/0x8sf96sz490-.css +0 -2
  248. package/web-dist/.next/static/chunks/0xgdqczv5tvu5.js +0 -1
  249. package/web-dist/.next/static/chunks/201p8-_l1h8w8.js +0 -1
  250. package/web-dist/.next/static/chunks/218i8jnxp91u8.js +0 -1
  251. package/web-dist/.next/static/chunks/2_uoi0tzglv_7.js +0 -1
  252. package/web-dist/.next/static/chunks/2ki5va2_5l6d6.js +0 -1
  253. package/web-dist/.next/static/chunks/2sm_cc2r9sjzp.js +0 -1
  254. package/web-dist/.next/static/chunks/2xur1zqckjlq3.js +0 -1
  255. package/web-dist/.next/static/chunks/35babynx5l240.js +0 -1
  256. package/web-dist/.next/static/chunks/3fqd824e6lt4z.js +0 -1
  257. package/web-dist/.next/static/chunks/3i0jpc47nxz7y.js +0 -1
  258. package/web-dist/.next/static/chunks/3soklmhd2y9yp.js +0 -1
  259. package/web-dist/.next/static/chunks/3ssnuxdh0r51g.js +0 -1
  260. package/web-dist/.next/static/chunks/3vmuunta80huz.js +0 -1
  261. package/web-dist/.next/static/chunks/3wo9m5v__3pi2.js +0 -1
  262. package/web-dist/.next/static/chunks/3ybvfq13cp-hb.js +0 -1
  263. package/web-dist/.next/static/chunks/3yqt3l54sr5si.js +0 -1
  264. /package/web-dist/.next/static/{qtJrbMvehxNiblgQJOzhz → HlF0dnC8pX1yTkvUuW9jW}/_buildManifest.js +0 -0
  265. /package/web-dist/.next/static/{qtJrbMvehxNiblgQJOzhz → HlF0dnC8pX1yTkvUuW9jW}/_clientMiddlewareManifest.js +0 -0
  266. /package/web-dist/.next/static/{qtJrbMvehxNiblgQJOzhz → HlF0dnC8pX1yTkvUuW9jW}/_ssgManifest.js +0 -0
@@ -0,0 +1,26 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Write the per-program MCP config into the advisor session's cwd so the spawned CLI auto-connects
4
+ * to Orca's MCP server. Each CLI has its own config schema — claude reads `.mcp.json`, opencode reads
5
+ * `opencode.json`, codex reads a TOML config. The schemas are version-sensitive: VERIFY each against
6
+ * the installed CLI version's docs. The `orca api` CLI verb is the always-available fallback, so an
7
+ * imperfect MCP wiring for one program degrades gracefully rather than removing the advisor's reach. */
8
+ export function writeMcpConfig(program, cwd, token, mcpUrl) {
9
+ const auth = `Bearer ${token}`;
10
+ if (program.startsWith('claude')) {
11
+ writeFileSync(join(cwd, '.mcp.json'), JSON.stringify({
12
+ mcpServers: { orca: { type: 'http', url: mcpUrl, headers: { Authorization: auth } } },
13
+ }, null, 2));
14
+ }
15
+ else if (program.startsWith('opencode')) {
16
+ writeFileSync(join(cwd, 'opencode.json'), JSON.stringify({
17
+ $schema: 'https://opencode.ai/config.json',
18
+ mcp: { orca: { type: 'remote', url: mcpUrl, headers: { Authorization: auth }, enabled: true } },
19
+ }, null, 2));
20
+ }
21
+ else if (program.startsWith('codex')) {
22
+ // Codex reads MCP servers from its TOML config. Written project-local; VERIFY the exact key/path
23
+ // (and whether the installed codex needs a `--config`/`-c` flag to pick this up) for the version.
24
+ writeFileSync(join(cwd, '.codex-mcp.toml'), `[mcp_servers.orca]\nurl = "${mcpUrl}"\nbearer_token = "${token}"\n`);
25
+ }
26
+ }
@@ -0,0 +1,74 @@
1
+ import { resolveExecutor } from '../overseer/routing.js';
2
+ import { render } from '../prompts/index.js';
3
+ import { logger } from '../shared/logger.js';
4
+ const log = logger('advisor');
5
+ /** Per-user advisor lifecycle: a persistent `orca-advisor-<userId>` agent session that controls Orca
6
+ * on the user's behalf with a full-scope token. Chosen exec is remembered and auto-started on login. */
7
+ export class AdvisorService {
8
+ d;
9
+ constructor(d) {
10
+ this.d = d;
11
+ }
12
+ session(userId) { return `orca-advisor-${userId}`; }
13
+ /** An exec must be globally allowed AND (for a restricted non-admin) on the user's own allow-list. */
14
+ execAllowed(userId, exec) {
15
+ const u = this.d.users.get(userId);
16
+ if (!u)
17
+ return false;
18
+ if (!this.d.config.get().allowedExecs.includes(exec))
19
+ return false;
20
+ if (u.is_admin || u.allowed_execs.length === 0)
21
+ return true;
22
+ return u.allowed_execs.includes(exec);
23
+ }
24
+ async status(userId) {
25
+ const u = this.d.users.get(userId);
26
+ const name = this.session(userId);
27
+ const running = (await this.d.tmux.list()).includes(name);
28
+ return { running, exec: u?.advisor_exec ?? '', session: running ? name : null };
29
+ }
30
+ async start(userId, exec) {
31
+ if (!this.execAllowed(userId, exec))
32
+ throw new Error('exec not allowed for user');
33
+ const name = this.session(userId);
34
+ if ((await this.d.tmux.list()).includes(name))
35
+ return { session: name }; // already live — idempotent
36
+ this.d.users.setAdvisorExec(userId, exec); // remember the choice for autostart
37
+ const spec = resolveExecutor([`exec:${exec}`], this.d.fallback);
38
+ const token = this.d.users.ensureAdvisorToken(userId); // full-scope, reused across restarts
39
+ const cwd = this.d.advisorDir(userId);
40
+ await this.d.prepareMcp?.(spec.program, cwd, token, this.d.url);
41
+ const u = this.d.users.get(userId);
42
+ const rawPrompt = render('advisor', { userName: u.name || u.username });
43
+ // agentName `advisor-<id>` → SpawnService names the tmux session `orca-advisor-<id>`. The full
44
+ // advisor token overrides the daemon's agent service token via extraEnv, so the advisor acts with
45
+ // the user's own rights. The cwd is a neutral per-user dir, not a project checkout.
46
+ await this.d.spawn.launch({
47
+ projectId: this.d.projectId ?? 0,
48
+ projectPath: cwd,
49
+ taskId: name,
50
+ agentName: `advisor-${userId}`,
51
+ spec,
52
+ rawPrompt,
53
+ extraEnv: { ORCA_TOKEN: token, ORCA_URL: this.d.url },
54
+ });
55
+ log.info(`advisor started for user ${userId} (${spec.program}/${spec.model})`);
56
+ return { session: name };
57
+ }
58
+ async stop(userId) {
59
+ await this.d.tmux.kill(this.session(userId));
60
+ }
61
+ /** Bring the user's advisor back up after login, if they set one up and left autostart on. Never
62
+ * throws — a spawn failure must not block the login response. */
63
+ async ensureOnLogin(userId) {
64
+ const u = this.d.users.get(userId);
65
+ if (!u || !u.advisor_exec || !u.advisor_autostart)
66
+ return;
67
+ try {
68
+ await this.start(userId, u.advisor_exec);
69
+ }
70
+ catch (e) {
71
+ log.error(`advisor autostart failed for user ${userId}`, e);
72
+ }
73
+ }
74
+ }
@@ -2,11 +2,11 @@ import { basename, dirname, join } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { homedir } from 'node:os';
4
4
  import { writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
5
- import { randomBytes, createHmac, timingSafeEqual } from 'node:crypto';
5
+ import { createHmac, timingSafeEqual } from 'node:crypto';
6
6
  import { hermesStatus, installHermesPlugin } from '../integrations/hermesInstall.js';
7
7
  import { detectClis } from '../integrations/cliDetection.js';
8
8
  import { readTaskUsage } from '../integrations/usage/index.js';
9
- import { listProjectFiles, readProjectFile, writeProjectFile, readProjectBytes, createProjectFile, createProjectDir, deleteProjectEntry, renameProjectEntry, copyProjectEntry, projectFileAtHead, projectFileDiff, projectCommitDiff, projectCommitFiles, projectCommitFileDiff, projectCommitLog, projectChangedFiles, projectWorkingDiff, projectReviewDiff } from '../integrations/projectFiles.js';
9
+ import { listProjectFiles, readProjectFile, writeProjectFile, readProjectBytes, createProjectFile, createProjectDir, deleteProjectEntry, renameProjectEntry, copyProjectEntry, projectFileAtHead, projectFileDiff, projectCommitDiff, projectCommitFiles, projectCommitFileDiff, projectCommitLog, projectChangedFiles, projectWorkingDiff, projectReviewDiff, isProjectImage } from '../integrations/projectFiles.js';
10
10
  import { Hono } from 'hono';
11
11
  import { cors } from 'hono/cors';
12
12
  import { streamSSE } from 'hono/streaming';
@@ -21,7 +21,9 @@ import { RelayClient } from '../inference/client.js';
21
21
  import { uniqueName } from '../daemon/uniqueName.js';
22
22
  import { assembleMissionDetail } from '../store/missionDetail.js';
23
23
  import { authMiddleware } from './auth.js';
24
+ import { handleMcpRequest } from '../mcp/server.js';
24
25
  import { logger } from '../shared/logger.js';
26
+ import { shortId } from '../shared/id.js';
25
27
  /** How many times an L3 mission auto-re-spawns a phase that the post-done review rejected before it
26
28
  * gives up and escalates to a human. Mirrors the stuck detector's `maxRelaunch` (2) so the two
27
29
  * bounded-retry loops behave consistently. */
@@ -36,6 +38,8 @@ const ORCA_VERSION = (() => {
36
38
  return '0.0.0';
37
39
  }
38
40
  })();
41
+ /** Port the daemon listens on — the MCP route reaches back into this same daemon's REST API at it. */
42
+ const ORCA_PORT = Number(process.env.ORCA_PORT ?? 4400);
39
43
  export function createServer(d) {
40
44
  const log = logger('api');
41
45
  // Core reasoning stores are optional in deps for back-compat with existing call sites/tests; the
@@ -107,6 +111,12 @@ export function createServer(d) {
107
111
  const p = c.req.path;
108
112
  if (!GATED.some((g) => p === g || p.startsWith(g + '/')))
109
113
  return next();
114
+ // An advisor session is per-user, not project-scoped: its access is governed by ownership in
115
+ // the route's own sessionAccessible check, so the project gate must not pre-empt it (the user
116
+ // need not be assigned to the daemon's project to reach their own advisor).
117
+ const sess = p.match(/^\/sessions\/([^/]+)/);
118
+ if (sess?.[1] && classifySession(decodeURIComponent(sess[1])).role === 'advisor')
119
+ return next();
110
120
  if (users.count() === 0)
111
121
  return next(); // setup mode — no users to gate yet
112
122
  const u = c.get('user');
@@ -148,7 +158,9 @@ export function createServer(d) {
148
158
  if (!user)
149
159
  return c.json({ error: 'invalid credentials' }, 401);
150
160
  loginHits.delete(ip); // a valid login clears the counter so an earlier typo streak can't lock the user out
151
- return c.json({ token: users.issueToken(user.id), user });
161
+ const token = users.issueToken(user.id);
162
+ void d.advisor?.ensureOnLogin(user.id); // fire-and-forget: bring the user's advisor back up; never block login
163
+ return c.json({ token, user });
152
164
  });
153
165
  app.post('/auth/logout', (c) => { const t = c.get('token'); if (t)
154
166
  users.revokeToken(t); return c.json({ ok: true }); });
@@ -427,6 +439,14 @@ export function createServer(d) {
427
439
  if (!d.userProjects || !d.users)
428
440
  return true; // open / single-user mode — no tenancy boundary
429
441
  const u = c.get('user');
442
+ // An advisor session belongs to exactly one user: only its owner (or an admin) may reach it, and
443
+ // never via an agent-scoped token. It has no task row, so the project check below can't apply.
444
+ const info = classifySession(name);
445
+ if (info.role === 'advisor') {
446
+ if (c.get('tokenScope') === 'agent')
447
+ return false;
448
+ return !!u && (u.id === info.userId || d.userProjects.isAdmin(u.id));
449
+ }
430
450
  // Admin sees every session — but NOT via an agent-scoped token (it's owned by the admin user yet
431
451
  // must stay confined to its working set; fall through to the project check below).
432
452
  if (c.get('tokenScope') !== 'agent' && u && d.userProjects.isAdmin(u.id))
@@ -468,7 +488,7 @@ export function createServer(d) {
468
488
  function persistPlan(job) {
469
489
  const path = pathFor(job.projectId);
470
490
  const allowedExecs = d.config.get().allowedExecs;
471
- const newId = () => `${basename(path)}-${randomBytes(4).toString('hex')}`;
491
+ const newId = () => shortId(basename(path));
472
492
  const epicId = job.epicId ?? newId();
473
493
  let epic = d.tasks.get(epicId);
474
494
  if (!epic) {
@@ -573,7 +593,8 @@ export function createServer(d) {
573
593
  return c.json({ error: 'forbidden' }, 403);
574
594
  }
575
595
  const id = Number(c.req.param('id'));
576
- if (!d.projects.get(id))
596
+ const cur = d.projects.get(id);
597
+ if (!cur)
577
598
  return c.json({ error: 'project not found' }, 404);
578
599
  const b = await c.req.json();
579
600
  const patch = {};
@@ -581,6 +602,13 @@ export function createServer(d) {
581
602
  patch.path = b.path.trim();
582
603
  if (typeof b.notes === 'string')
583
604
  patch.notes = b.notes;
605
+ // Icon is a project-relative image path. '' clears it; anything else must resolve to a real image
606
+ // file inside the project root (guards against path traversal / pointing at a non-image).
607
+ if (typeof b.icon === 'string') {
608
+ if (b.icon !== '' && !isProjectImage(cur.path, b.icon))
609
+ return c.json({ error: 'invalid icon path' }, 400);
610
+ patch.icon = b.icon;
611
+ }
584
612
  return c.json(d.projects.update(id, patch));
585
613
  });
586
614
  // Remove a project from orca entirely: cascades to its tasks, missions, agents and access grants
@@ -930,7 +958,7 @@ export function createServer(d) {
930
958
  const target = resolveTarget(c, b.project_id);
931
959
  if ('error' in target)
932
960
  return c.json({ error: target.error }, target.status);
933
- const id = b.id ?? `${basename(target.project.path)}-${randomBytes(4).toString('hex')}`;
961
+ const id = b.id ?? shortId(basename(target.project.path));
934
962
  const created = d.tasks.create({ id, project_id: target.project.id, title: b.title, type: b.type, priority: b.priority, description: b.description, scheduled_at: b.scheduled_at, autostart: b.autostart });
935
963
  if (Array.isArray(b.deps))
936
964
  d.tasks.setDeps(created.id, b.deps);
@@ -1373,6 +1401,18 @@ export function createServer(d) {
1373
1401
  await d.tmux.sendKeys(c.req.param('name'), keys);
1374
1402
  return c.json({ ok: true });
1375
1403
  });
1404
+ app.post('/sessions/:name/input', async (c) => {
1405
+ // Raw interactive input: the xterm `onData` bytes (printable chars, control codes, ESC sequences)
1406
+ // are forwarded verbatim to the pane via `send-keys -l`, so the advisor terminal behaves like a
1407
+ // real one. `-l` + `--` (in the driver) make a leading '-' safe, so no flag-token validation here.
1408
+ if (!sessionAccessible(c, c.req.param('name')))
1409
+ return c.json({ error: 'forbidden' }, 403);
1410
+ const { data } = await c.req.json().catch(() => ({}));
1411
+ if (typeof data !== 'string' || data.length === 0)
1412
+ return c.json({ error: 'data must be a non-empty string' }, 400);
1413
+ await d.tmux.sendRaw(c.req.param('name'), data);
1414
+ return c.json({ ok: true });
1415
+ });
1376
1416
  app.post('/sessions/:name/resize', async (c) => {
1377
1417
  if (!sessionAccessible(c, c.req.param('name')))
1378
1418
  return c.json({ error: 'forbidden' }, 403);
@@ -1415,6 +1455,45 @@ export function createServer(d) {
1415
1455
  clear();
1416
1456
  });
1417
1457
  });
1458
+ // Per-user advisor lifecycle. Full-scope (non-agent) callers only — a spawned agent must not be able
1459
+ // to start/stop a human's advisor. Each acts on the caller's own session (`orca-advisor-<userId>`).
1460
+ app.get('/advisor/status', async (c) => {
1461
+ if (!d.advisor)
1462
+ return c.json({ running: false, exec: '', session: null });
1463
+ if (c.get('tokenScope') === 'agent')
1464
+ return c.json({ error: 'forbidden' }, 403);
1465
+ return c.json(await d.advisor.status(c.get('user').id));
1466
+ });
1467
+ app.post('/advisor/start', async (c) => {
1468
+ if (!d.advisor)
1469
+ return c.json({ error: 'advisor unavailable' }, 503);
1470
+ if (c.get('tokenScope') === 'agent')
1471
+ return c.json({ error: 'forbidden' }, 403);
1472
+ const { exec } = await c.req.json().catch(() => ({}));
1473
+ if (typeof exec !== 'string' || !exec)
1474
+ return c.json({ error: 'exec required' }, 400);
1475
+ try {
1476
+ return c.json(await d.advisor.start(c.get('user').id, exec), 201);
1477
+ }
1478
+ catch (e) {
1479
+ return c.json({ error: e.message }, 403);
1480
+ } // exec not allowed for the user
1481
+ });
1482
+ app.post('/advisor/stop', async (c) => {
1483
+ if (!d.advisor)
1484
+ return c.json({ ok: true });
1485
+ if (c.get('tokenScope') === 'agent')
1486
+ return c.json({ error: 'forbidden' }, 403);
1487
+ await d.advisor.stop(c.get('user').id);
1488
+ return c.json({ ok: true });
1489
+ });
1490
+ // MCP endpoint: the advisor agent connects here to control Orca with native tools. Each request is
1491
+ // handled statelessly with the toolset bound to the caller's token, and every tool delegates to the
1492
+ // same `callOrcaApi` core as the `orca api` CLI verb — so a new REST endpoint needs zero edits here.
1493
+ app.all('/mcp', async (c) => {
1494
+ const token = c.get('token');
1495
+ return handleMcpRequest(c.req.raw, { url: `http://localhost:${ORCA_PORT}`, token });
1496
+ });
1418
1497
  app.get('/missions', c => {
1419
1498
  const allowed = accessibleProjects(c);
1420
1499
  const live = d.missions.live();
@@ -1,5 +1,31 @@
1
1
  import { start as realStart, stop as realStop, status as realStatus } from './launcher.js';
2
2
  import { update as realUpdate } from './update.js';
3
+ /** `orca api <METHOD> <path> [jsonBody]` — generic authenticated REST passthrough. Reads
4
+ * ORCA_URL/ORCA_TOKEN from the env the daemon injects into every spawned agent, so an agent can
5
+ * drive ANY endpoint without a per-endpoint CLI command (and a new endpoint needs zero CLI edits).
6
+ * Injectable for tests; returns a process exit code. */
7
+ export async function runApiCommand(args, env, deps) {
8
+ const [method, path, rawBody] = args;
9
+ if (!method || !path) {
10
+ deps.err('usage: orca api <METHOD> <path> [jsonBody]');
11
+ return 2;
12
+ }
13
+ let body;
14
+ if (rawBody !== undefined) {
15
+ try {
16
+ body = JSON.parse(rawBody);
17
+ }
18
+ catch {
19
+ deps.err('api: body must be valid JSON');
20
+ return 2;
21
+ }
22
+ }
23
+ const url = env.ORCA_URL ?? 'http://localhost:4400';
24
+ const token = env.ORCA_TOKEN ?? '';
25
+ const res = await deps.call(method, path, body, { url, token });
26
+ deps.out(res.data !== undefined ? JSON.stringify(res.data, null, 2) : res.text);
27
+ return res.ok ? 0 : 1;
28
+ }
3
29
  export function defaultLifecycleDeps(version) {
4
30
  return {
5
31
  version,
package/dist/cli/index.js CHANGED
@@ -4,7 +4,8 @@ import { readFileSync, realpathSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { OrcaClient } from './client.js';
7
- import { defaultLifecycleDeps, runLifecycle } from './commands.js';
7
+ import { defaultLifecycleDeps, runLifecycle, runApiCommand } from './commands.js';
8
+ import { callOrcaApi } from '../shared/apiClient.js';
8
9
  import { menu } from './menu.js';
9
10
  const BASE = process.env.ORCA_URL ?? 'http://localhost:4400';
10
11
  const USAGE = "usage: orca [command] [options] — run `orca --help` for the full command list";
@@ -35,6 +36,7 @@ TASKS
35
36
  --outcome ok|fail record the outcome
36
37
 
37
38
  AGENT-FACING (invoked by running agents — rarely needed by hand)
39
+ api <METHOD> <path> [body] generic authenticated REST call (needs ORCA_URL/ORCA_TOKEN)
38
40
  plan submit --phases '<json>' submit an autopilot plan (needs ORCA_PLAN_JOB)
39
41
  overseer poll wait for the next decision (needs ORCA_MISSION)
40
42
  overseer decide --id <id> … resolve a decision: --approve | --escalate | --choice <optionId>
@@ -49,7 +51,7 @@ Docs & issues: https://github.com/dragocz1995/orcasynth`;
49
51
  /** Commands that talk to the daemon API — only these justify auto-starting it. Everything else
50
52
  * (help, unknown verbs) must NOT spawn a daemon: a stray detached daemon squats the port and starves
51
53
  * the systemd-managed one into a restart loop. */
52
- const API_COMMANDS = new Set(['ls', 'ready', 'sessions', 'close', 'plan', 'overseer']);
54
+ const API_COMMANDS = new Set(['ls', 'ready', 'sessions', 'close', 'plan', 'overseer', 'api']);
53
55
  /** True only for verbs that need the daemon API up — the gate for ensureDaemon's auto-spawn. */
54
56
  export function needsDaemon(cmd) {
55
57
  return cmd !== undefined && API_COMMANDS.has(cmd);
@@ -102,6 +104,11 @@ export async function run(argv, c, env) {
102
104
  case 'sessions':
103
105
  console.log(JSON.stringify(await c.sessions(), null, 2));
104
106
  break;
107
+ case 'api': {
108
+ const code = await runApiCommand(argv.slice(1), env, { call: callOrcaApi, out: (s) => console.log(s), err: (s) => console.error(s) });
109
+ process.exit(code);
110
+ break;
111
+ }
105
112
  case 'close': {
106
113
  if (!arg) {
107
114
  console.error('usage: orca close <taskId> [--summary "<text>"] [--outcome ok|fail]');
@@ -27,9 +27,12 @@ import { UserProjectStore } from '../store/userProjectStore.js';
27
27
  import { RealGitReader } from '../git/gitReader.js';
28
28
  import { uniqueName } from './uniqueName.js';
29
29
  import { logger } from '../shared/logger.js';
30
+ import { AdvisorService } from '../advisor/service.js';
31
+ import { writeMcpConfig } from '../advisor/mcpConfig.js';
30
32
  import { fileURLToPath } from 'node:url';
31
33
  import { dirname, join } from 'node:path';
32
34
  import { randomBytes } from 'node:crypto';
35
+ import { mkdirSync } from 'node:fs';
33
36
  const log = logger('daemon');
34
37
  /** Build the overseer-model prompt that turns a finished mission's phase results into a short,
35
38
  * human-readable Czech summary shown on the epic in the dashboard. Kept terse so the relay returns
@@ -226,7 +229,17 @@ export function buildApp(opts) {
226
229
  // Per-process secret for short-lived signed avatar URLs (finding W2) — keeps the long-lived session
227
230
  // token out of <img> src query strings. Rotates on restart; links live ~5 min, so that's harmless.
228
231
  const avatarSecret = randomBytes(32).toString('hex');
229
- const app = createServer({ tasks, readiness, missions, engine, spawn, tmux, bus, events, agents, project: opts.project, fallback: { program: 'claude-code', model: 'sonnet' }, clock: new SystemClock(), config, users, projects, userProjects, git, avatarsDir, avatarSecret, planJobs, decisionQueue, pilot });
232
+ // Per-user advisor: a persistent assistant session controlling Orca on the user's behalf. Its cwd
233
+ // is a neutral per-user dir (alongside the DB, NOT a project checkout) so the per-program MCP config
234
+ // never pollutes a repo. Disabled for the in-memory DB (tests build their own AdvisorService).
235
+ const mcpUrl = `${orcaCli.url}/mcp`; // the daemon hosts the MCP server on its own /mcp route
236
+ const advisor = opts.dbPath === ':memory:' ? undefined : new AdvisorService({
237
+ spawn, tmux, users, config, fallback: { program: 'claude-code', model: 'sonnet' },
238
+ projectId: opts.project.id, url: orcaCli.url,
239
+ advisorDir: (id) => { const p = join(dirname(opts.dbPath), 'advisor', String(id)); mkdirSync(p, { recursive: true }); return p; },
240
+ prepareMcp: (program, cwd, token) => writeMcpConfig(program, cwd, token, mcpUrl),
241
+ });
242
+ const app = createServer({ tasks, readiness, missions, engine, spawn, tmux, bus, events, agents, project: opts.project, fallback: { program: 'claude-code', model: 'sonnet' }, clock: new SystemClock(), config, users, projects, userProjects, git, avatarsDir, avatarSecret, planJobs, decisionQueue, pilot, advisor });
230
243
  // Root-cause recovery: after a daemon crash/restart, tasks left 'in_progress' whose tmux
231
244
  // session is gone are zombies — revert them to 'open' so they can be picked up again. No grace
232
245
  // or relaunch counter here: a restart isn't an agent death, so it shouldn't spend the budget.
@@ -40,6 +40,22 @@ function safe(root, rel, forWrite = false) {
40
40
  throw new Error('path outside project');
41
41
  return abs;
42
42
  }
43
+ // Image extensions a project icon may point at. Matches what `/raw` serves and what the picker shows.
44
+ const IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'avif']);
45
+ /** True when `rel` resolves to a regular image file strictly inside the project root — used to validate
46
+ * a chosen project icon before persisting it. Never throws: a traversal/symlink escape, a missing
47
+ * file, a directory or a non-image extension all return false. */
48
+ export function isProjectImage(root, rel) {
49
+ if (!IMAGE_EXT.has((rel.split('.').pop() ?? '').toLowerCase()))
50
+ return false;
51
+ try {
52
+ const abs = safe(root, rel);
53
+ return statSync(abs).isFile();
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
43
59
  /** Flat list of a project's files and directories (relative paths), skipping VCS/build dirs. */
44
60
  export function listProjectFiles(root, maxDepth = 8) {
45
61
  const r = resolve(root);
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { EMPTY_USAGE, SESSION_MATCH_SKEW_MS } from './types.js';
3
+ import { EMPTY_USAGE } from './types.js';
4
+ import { pickNthSession } from './walk.js';
4
5
  /** claude-code stores one JSONL transcript per session under
5
6
  * ~/.claude/projects/<encoded-cwd>/<sessionUuid>.jsonl, where each assistant event carries
6
7
  * `message.usage`. Pick the session that started when this spawn ran and sum its usage.
@@ -10,24 +11,23 @@ export function claudeUsage(home, dir, sinceMs, nth = 0) {
10
11
  const projDir = join(home, '.claude', 'projects', dir.replace(/[/._]/g, '-'));
11
12
  if (!existsSync(projDir))
12
13
  return null;
13
- // Transcripts started at/after the spawn window, ordered by start; `nth` picks one so
14
- // concurrent agents in the same project map to distinct sessions instead of colliding.
14
+ // Transcripts that carry a start time; `pickNthSession` keeps those in the spawn window,
15
+ // orders by start, and picks the nth so concurrent agents map to distinct sessions.
15
16
  const sessions = [];
16
17
  for (const name of readdirSync(projDir)) {
17
18
  if (!name.endsWith('.jsonl'))
18
19
  continue;
19
20
  const p = join(projDir, name);
20
21
  const start = firstEventMs(p);
21
- if (start == null || start < sinceMs - SESSION_MATCH_SKEW_MS)
22
+ if (start == null)
22
23
  continue;
23
24
  sessions.push({ path: p, start });
24
25
  }
25
- sessions.sort((a, b) => a.start - b.start);
26
- const best = sessions[nth];
26
+ const best = pickNthSession(sessions, sinceMs, nth);
27
27
  if (!best)
28
28
  return null;
29
29
  const u = { ...EMPTY_USAGE };
30
- for (const line of readFileSync(best.path, 'utf8').split('\n')) {
30
+ for (const line of readFileSync(best, 'utf8').split('\n')) {
31
31
  if (!line.trim())
32
32
  continue;
33
33
  try {
@@ -1,8 +1,7 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { basename } from 'node:path';
4
- import { SESSION_MATCH_SKEW_MS } from './types.js';
5
- import { walkFiles } from './walk.js';
4
+ import { pickNthSession, walkFiles } from './walk.js';
6
5
  /** codex stores one rollout JSONL per session under ~/.codex/sessions/<Y>/<M>/<D>/rollout-*.jsonl,
7
6
  * carrying a cumulative `total_token_usage` object. Pick the rollout started when this spawn ran
8
7
  * and read its final cumulative usage. codex does not record cost (costUsd stays null). */
@@ -11,21 +10,20 @@ export function codexUsage(home, _dir, sinceMs, nth = 0) {
11
10
  if (!existsSync(root))
12
11
  return null;
13
12
  // codex rollouts aren't dir-scoped on disk, so concurrent codex agents can only be
14
- // disambiguated by start order; `nth` picks the rank-th rollout in the spawn window.
13
+ // disambiguated by start order; `pickNthSession` picks the rank-th rollout in the spawn window.
15
14
  const sessions = [];
16
15
  for (const f of walkFiles(root)) {
17
16
  if (!basename(f).startsWith('rollout-') || !f.endsWith('.jsonl'))
18
17
  continue;
19
18
  const start = rolloutStartMs(f);
20
- if (start == null || start < sinceMs - SESSION_MATCH_SKEW_MS)
19
+ if (start == null)
21
20
  continue;
22
21
  sessions.push({ path: f, start });
23
22
  }
24
- sessions.sort((a, b) => a.start - b.start);
25
- const best = sessions[nth];
23
+ const best = pickNthSession(sessions, sinceMs, nth);
26
24
  if (!best)
27
25
  return null;
28
- return finalUsage(best.path);
26
+ return finalUsage(best);
29
27
  }
30
28
  /** Start time of a rollout: its first event's ISO timestamp, else the timestamp in its filename. */
31
29
  function rolloutStartMs(path) {
@@ -4,13 +4,7 @@ import { opencodeUsage } from './opencode.js';
4
4
  import { claudeUsage } from './claude.js';
5
5
  import { codexUsage } from './codex.js';
6
6
  import { SESSION_MATCH_SKEW_MS } from './types.js';
7
- /** Parse a SQLite ("2026-06-19 11:13:20", UTC) or ISO timestamp to epoch ms. */
8
- function parseTs(ts) {
9
- if (!ts)
10
- return 0;
11
- const ms = Date.parse(ts.includes('T') ? ts : ts.replace(' ', 'T') + 'Z');
12
- return Number.isNaN(ms) ? 0 : ms;
13
- }
7
+ import { parseDbTs } from '../../shared/time.js';
14
8
  /** The precise spawn time (epoch ms) the agent launched, from the `started:<ms>` label — this is
15
9
  * sub-second and reflects real spawn order, unlike whole-second `created_at` (set at row insert).
16
10
  * Falls back to created_at for tasks launched before this label existed. */
@@ -21,7 +15,7 @@ function startedMs(task) {
21
15
  if (Number.isFinite(ms))
22
16
  return ms;
23
17
  }
24
- return parseTs(task.created_at);
18
+ return parseDbTs(task.created_at);
25
19
  }
26
20
  /** The resolved CLI program + model for a task (program normalized: 'opencode' | 'claude-code' |
27
21
  * 'codex' | …). */
@@ -1,5 +1,19 @@
1
1
  import { readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { SESSION_MATCH_SKEW_MS } from './types.js';
4
+ /**
5
+ * From a set of session candidates `{path, start}`, pick the nth one that started within the spawn
6
+ * window (`start >= sinceMs - SESSION_MATCH_SKEW_MS`), ordered by start time. `nth` lets concurrent
7
+ * agents in the same project map to distinct sessions instead of colliding. Returns null when no
8
+ * candidate qualifies or `nth` is out of range. Single source of truth for the claude/codex
9
+ * session-select logic (opencode selects via SQL).
10
+ */
11
+ export function pickNthSession(candidates, sinceMs, nth) {
12
+ const inWindow = candidates
13
+ .filter((c) => c.start >= sinceMs - SESSION_MATCH_SKEW_MS)
14
+ .sort((a, b) => a.start - b.start);
15
+ return inWindow[nth]?.path ?? null;
16
+ }
3
17
  /** Recursively yield every file path under `dir` (depth-limited). Returns [] if dir is missing. */
4
18
  export function walkFiles(dir, maxDepth = 4) {
5
19
  const out = [];
@@ -0,0 +1,34 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
3
+ import { z } from 'zod';
4
+ import { makeOrcaTools } from './tools.js';
5
+ /** Build an MCP server exposing the Orca toolset bound to one caller's token. Every tool delegates to
6
+ * `makeOrcaTools` → the shared `callOrcaApi` core, so there is no request logic here to maintain. */
7
+ function createOrcaMcpServer(deps) {
8
+ const tools = makeOrcaTools(deps);
9
+ const server = new McpServer({ name: 'orca', version: '1.0.0' });
10
+ const text = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data ?? null, null, 2) }] });
11
+ server.registerTool('orca_request', {
12
+ description: 'Call any Orca REST endpoint (full control). Generic escape hatch — every endpoint works without a dedicated tool.',
13
+ inputSchema: { method: z.string(), path: z.string(), body: z.unknown().optional() },
14
+ }, async (a) => text(await tools.orca_request({ method: a.method, path: a.path, body: a.body })));
15
+ server.registerTool('orca_tasks', { description: 'List all tasks.', inputSchema: {} }, async () => text(await tools.orca_tasks()));
16
+ server.registerTool('orca_create_task', {
17
+ description: 'Create a task.',
18
+ inputSchema: { title: z.string(), project_id: z.number().optional(), description: z.string().optional() },
19
+ }, async (a) => text(await tools.orca_create_task(a)));
20
+ server.registerTool('orca_plan', {
21
+ description: 'Plan a goal into an epic with phases (autopilot).',
22
+ inputSchema: { goal: z.string(), project_id: z.number().optional() },
23
+ }, async (a) => text(await tools.orca_plan(a)));
24
+ server.registerTool('orca_sessions', { description: 'List live agent sessions.', inputSchema: {} }, async () => text(await tools.orca_sessions()));
25
+ return server;
26
+ }
27
+ /** Stateless HTTP handler: a fresh server + transport per request, with the toolset bound to the
28
+ * request's bearer token, so each advisor connection acts with exactly its user's rights. */
29
+ export async function handleMcpRequest(req, deps) {
30
+ const server = createOrcaMcpServer(deps);
31
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
32
+ await server.connect(transport);
33
+ return transport.handleRequest(req);
34
+ }
@@ -0,0 +1,17 @@
1
+ import { callOrcaApi } from '../shared/apiClient.js';
2
+ export function makeOrcaTools(d) {
3
+ const call = d.call ?? callOrcaApi;
4
+ const req = async (method, path, body) => {
5
+ const r = await call(method, path, body, { url: d.url, token: d.token });
6
+ if (!r.ok)
7
+ throw new Error(`orca ${r.status}: ${r.text || JSON.stringify(r.data)}`);
8
+ return r.data;
9
+ };
10
+ return {
11
+ orca_request: (a) => req(a.method, a.path, a.body),
12
+ orca_tasks: () => req('GET', '/tasks'),
13
+ orca_create_task: (a) => req('POST', '/tasks', a),
14
+ orca_plan: (a) => req('POST', '/tasks/plan', a),
15
+ orca_sessions: () => req('GET', '/sessions'),
16
+ };
17
+ }
@@ -1,13 +1,19 @@
1
1
  const ORCA = 'orca-';
2
2
  const OVERSEER = 'overseer-';
3
3
  const PILOT = 'pilot-';
4
+ const ADVISOR = 'advisor-';
4
5
  /** Classify a live session name into its role + identity. Mirrors the spawn-time conventions:
5
- * overseer → `orca-overseer-<missionId>`, pilot → `orca-pilot-<name>`, worker → `orca-<name>`. */
6
+ * overseer → `orca-overseer-<missionId>`, pilot → `orca-pilot-<name>`, advisor → `orca-advisor-<userId>`,
7
+ * worker → `orca-<name>`. */
6
8
  export function classifySession(name) {
7
9
  const bare = name.startsWith(ORCA) ? name.slice(ORCA.length) : name;
8
10
  if (bare.startsWith(OVERSEER))
9
11
  return { name, role: 'overseer', agent: '', missionId: bare.slice(OVERSEER.length) };
10
12
  if (bare.startsWith(PILOT))
11
13
  return { name, role: 'pilot', agent: bare.slice(PILOT.length) };
14
+ if (bare.startsWith(ADVISOR)) {
15
+ const userId = Number(bare.slice(ADVISOR.length));
16
+ return { name, role: 'advisor', agent: '', userId: Number.isInteger(userId) ? userId : undefined };
17
+ }
12
18
  return { name, role: 'agent', agent: bare };
13
19
  }
@@ -1,3 +1,4 @@
1
+ import { parseDbTs } from '../shared/time.js';
1
2
  const agentOf = (t) => t.labels.find((l) => l.startsWith('agent:'))?.slice('agent:'.length) ?? null;
2
3
  /** Epoch-ms the task's agent was spawned: the precise `started:<ms>` label, falling back to the
3
4
  * whole-second `created_at` (stored UTC). Null only for a task that has neither. */
@@ -8,16 +9,9 @@ function startedOf(t) {
8
9
  if (Number.isFinite(n))
9
10
  return n;
10
11
  }
11
- if (t.created_at) {
12
- // SQLite `datetime('now')` is `YYYY-MM-DD HH:MM:SS` (UTC, no zone) normalise to ISO + 'Z'. But
13
- // if it already carries a zone (a 'T' separator implies an ISO string), parse it as-is — appending
14
- // a second 'Z' would yield `...ZZ` and a NaN. Brittle-format guard for #54.
15
- const iso = t.created_at.includes('T') ? t.created_at : t.created_at.replace(' ', 'T') + 'Z';
16
- const n = Date.parse(iso);
17
- if (Number.isFinite(n))
18
- return n;
19
- }
20
- return null;
12
+ // SQLite `datetime('now')` is `YYYY-MM-DD HH:MM:SS` (UTC, no zone); parseDbTs normalises it and
13
+ // returns 0 for an absent/unparseable value, which maps back to null (no usable start time).
14
+ return parseDbTs(t.created_at) || null;
21
15
  }
22
16
  /** in_progress tasks whose agent tmux session is no longer live — the agent exited or crashed
23
17
  * (no `orca close`), or the task never got an agent label. Shared by the startup zombie