@cluesmith/codev 2.0.0-rc.9 → 2.0.0

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/bin/af.js +2 -2
  2. package/bin/consult.js +1 -1
  3. package/dashboard/dist/assets/index-4n9zpWLY.css +32 -0
  4. package/dashboard/dist/assets/index-b38SaXk5.js +136 -0
  5. package/dashboard/dist/assets/index-b38SaXk5.js.map +1 -0
  6. package/dashboard/dist/index.html +14 -0
  7. package/dist/agent-farm/cli.d.ts.map +1 -1
  8. package/dist/agent-farm/cli.js +179 -104
  9. package/dist/agent-farm/cli.js.map +1 -1
  10. package/dist/agent-farm/commands/architect.d.ts +3 -3
  11. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  12. package/dist/agent-farm/commands/architect.js +20 -147
  13. package/dist/agent-farm/commands/architect.js.map +1 -1
  14. package/dist/agent-farm/commands/attach.d.ts +13 -0
  15. package/dist/agent-farm/commands/attach.d.ts.map +1 -0
  16. package/dist/agent-farm/commands/attach.js +144 -0
  17. package/dist/agent-farm/commands/attach.js.map +1 -0
  18. package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
  19. package/dist/agent-farm/commands/cleanup.js +35 -19
  20. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  21. package/dist/agent-farm/commands/consult.d.ts +3 -4
  22. package/dist/agent-farm/commands/consult.d.ts.map +1 -1
  23. package/dist/agent-farm/commands/consult.js +27 -37
  24. package/dist/agent-farm/commands/consult.js.map +1 -1
  25. package/dist/agent-farm/commands/index.d.ts +2 -2
  26. package/dist/agent-farm/commands/index.d.ts.map +1 -1
  27. package/dist/agent-farm/commands/index.js +2 -2
  28. package/dist/agent-farm/commands/index.js.map +1 -1
  29. package/dist/agent-farm/commands/open.d.ts +4 -2
  30. package/dist/agent-farm/commands/open.d.ts.map +1 -1
  31. package/dist/agent-farm/commands/open.js +33 -83
  32. package/dist/agent-farm/commands/open.js.map +1 -1
  33. package/dist/agent-farm/commands/send.d.ts +1 -1
  34. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  35. package/dist/agent-farm/commands/send.js +70 -79
  36. package/dist/agent-farm/commands/send.js.map +1 -1
  37. package/dist/agent-farm/commands/shell.d.ts +15 -0
  38. package/dist/agent-farm/commands/shell.d.ts.map +1 -0
  39. package/dist/agent-farm/commands/shell.js +50 -0
  40. package/dist/agent-farm/commands/shell.js.map +1 -0
  41. package/dist/agent-farm/commands/spawn-roles.d.ts +80 -0
  42. package/dist/agent-farm/commands/spawn-roles.d.ts.map +1 -0
  43. package/dist/agent-farm/commands/spawn-roles.js +278 -0
  44. package/dist/agent-farm/commands/spawn-roles.js.map +1 -0
  45. package/dist/agent-farm/commands/spawn-worktree.d.ts +96 -0
  46. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -0
  47. package/dist/agent-farm/commands/spawn-worktree.js +305 -0
  48. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -0
  49. package/dist/agent-farm/commands/spawn.d.ts +5 -1
  50. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  51. package/dist/agent-farm/commands/spawn.js +241 -561
  52. package/dist/agent-farm/commands/spawn.js.map +1 -1
  53. package/dist/agent-farm/commands/start.d.ts +10 -20
  54. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  55. package/dist/agent-farm/commands/start.js +45 -449
  56. package/dist/agent-farm/commands/start.js.map +1 -1
  57. package/dist/agent-farm/commands/status.d.ts +2 -0
  58. package/dist/agent-farm/commands/status.d.ts.map +1 -1
  59. package/dist/agent-farm/commands/status.js +75 -24
  60. package/dist/agent-farm/commands/status.js.map +1 -1
  61. package/dist/agent-farm/commands/stop.d.ts +6 -0
  62. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  63. package/dist/agent-farm/commands/stop.js +49 -109
  64. package/dist/agent-farm/commands/stop.js.map +1 -1
  65. package/dist/agent-farm/commands/tower-cloud.d.ts +48 -0
  66. package/dist/agent-farm/commands/tower-cloud.d.ts.map +1 -0
  67. package/dist/agent-farm/commands/tower-cloud.js +293 -0
  68. package/dist/agent-farm/commands/tower-cloud.js.map +1 -0
  69. package/dist/agent-farm/commands/tower.d.ts +9 -0
  70. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  71. package/dist/agent-farm/commands/tower.js +59 -19
  72. package/dist/agent-farm/commands/tower.js.map +1 -1
  73. package/dist/agent-farm/db/index.d.ts +6 -2
  74. package/dist/agent-farm/db/index.d.ts.map +1 -1
  75. package/dist/agent-farm/db/index.js +301 -19
  76. package/dist/agent-farm/db/index.js.map +1 -1
  77. package/dist/agent-farm/db/migrate.d.ts +0 -4
  78. package/dist/agent-farm/db/migrate.d.ts.map +1 -1
  79. package/dist/agent-farm/db/migrate.js +6 -55
  80. package/dist/agent-farm/db/migrate.js.map +1 -1
  81. package/dist/agent-farm/db/schema.d.ts +3 -3
  82. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  83. package/dist/agent-farm/db/schema.js +25 -19
  84. package/dist/agent-farm/db/schema.js.map +1 -1
  85. package/dist/agent-farm/db/types.d.ts +3 -13
  86. package/dist/agent-farm/db/types.d.ts.map +1 -1
  87. package/dist/agent-farm/db/types.js +3 -11
  88. package/dist/agent-farm/db/types.js.map +1 -1
  89. package/dist/agent-farm/hq-connector.d.ts +2 -6
  90. package/dist/agent-farm/hq-connector.d.ts.map +1 -1
  91. package/dist/agent-farm/hq-connector.js +2 -17
  92. package/dist/agent-farm/hq-connector.js.map +1 -1
  93. package/dist/agent-farm/lib/cloud-config.d.ts +59 -0
  94. package/dist/agent-farm/lib/cloud-config.d.ts.map +1 -0
  95. package/dist/agent-farm/lib/cloud-config.js +143 -0
  96. package/dist/agent-farm/lib/cloud-config.js.map +1 -0
  97. package/dist/agent-farm/lib/device-name.d.ts +25 -0
  98. package/dist/agent-farm/lib/device-name.d.ts.map +1 -0
  99. package/dist/agent-farm/lib/device-name.js +46 -0
  100. package/dist/agent-farm/lib/device-name.js.map +1 -0
  101. package/dist/agent-farm/lib/nonce-store.d.ts +28 -0
  102. package/dist/agent-farm/lib/nonce-store.d.ts.map +1 -0
  103. package/dist/agent-farm/lib/nonce-store.js +60 -0
  104. package/dist/agent-farm/lib/nonce-store.js.map +1 -0
  105. package/dist/agent-farm/lib/token-exchange.d.ts +18 -0
  106. package/dist/agent-farm/lib/token-exchange.d.ts.map +1 -0
  107. package/dist/agent-farm/lib/token-exchange.js +48 -0
  108. package/dist/agent-farm/lib/token-exchange.js.map +1 -0
  109. package/dist/agent-farm/lib/tower-client.d.ts +163 -0
  110. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -0
  111. package/dist/agent-farm/lib/tower-client.js +233 -0
  112. package/dist/agent-farm/lib/tower-client.js.map +1 -0
  113. package/dist/agent-farm/lib/tunnel-client.d.ts +117 -0
  114. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -0
  115. package/dist/agent-farm/lib/tunnel-client.js +504 -0
  116. package/dist/agent-farm/lib/tunnel-client.js.map +1 -0
  117. package/dist/agent-farm/servers/tower-instances.d.ts +82 -0
  118. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -0
  119. package/dist/agent-farm/servers/tower-instances.js +454 -0
  120. package/dist/agent-farm/servers/tower-instances.js.map +1 -0
  121. package/dist/agent-farm/servers/tower-routes.d.ts +34 -0
  122. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -0
  123. package/dist/agent-farm/servers/tower-routes.js +1445 -0
  124. package/dist/agent-farm/servers/tower-routes.js.map +1 -0
  125. package/dist/agent-farm/servers/tower-server.d.ts +5 -2
  126. package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
  127. package/dist/agent-farm/servers/tower-server.js +157 -475
  128. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  129. package/dist/agent-farm/servers/tower-terminals.d.ts +119 -0
  130. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -0
  131. package/dist/agent-farm/servers/tower-terminals.js +629 -0
  132. package/dist/agent-farm/servers/tower-terminals.js.map +1 -0
  133. package/dist/agent-farm/servers/tower-tunnel.d.ts +34 -0
  134. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -0
  135. package/dist/agent-farm/servers/tower-tunnel.js +473 -0
  136. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -0
  137. package/dist/agent-farm/servers/tower-types.d.ts +86 -0
  138. package/dist/agent-farm/servers/tower-types.d.ts.map +1 -0
  139. package/dist/agent-farm/servers/tower-types.js +6 -0
  140. package/dist/agent-farm/servers/tower-types.js.map +1 -0
  141. package/dist/agent-farm/servers/tower-utils.d.ts +58 -0
  142. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -0
  143. package/dist/agent-farm/servers/tower-utils.js +182 -0
  144. package/dist/agent-farm/servers/tower-utils.js.map +1 -0
  145. package/dist/agent-farm/servers/tower-websocket.d.ts +25 -0
  146. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -0
  147. package/dist/agent-farm/servers/tower-websocket.js +171 -0
  148. package/dist/agent-farm/servers/tower-websocket.js.map +1 -0
  149. package/dist/agent-farm/state.d.ts +6 -2
  150. package/dist/agent-farm/state.d.ts.map +1 -1
  151. package/dist/agent-farm/state.js +34 -25
  152. package/dist/agent-farm/state.js.map +1 -1
  153. package/dist/agent-farm/types.d.ts +49 -26
  154. package/dist/agent-farm/types.d.ts.map +1 -1
  155. package/dist/agent-farm/utils/config.d.ts +0 -5
  156. package/dist/agent-farm/utils/config.d.ts.map +1 -1
  157. package/dist/agent-farm/utils/config.js +12 -44
  158. package/dist/agent-farm/utils/config.js.map +1 -1
  159. package/dist/agent-farm/utils/deps.d.ts.map +1 -1
  160. package/dist/agent-farm/utils/deps.js +0 -32
  161. package/dist/agent-farm/utils/deps.js.map +1 -1
  162. package/dist/agent-farm/utils/file-tabs.d.ts +27 -0
  163. package/dist/agent-farm/utils/file-tabs.d.ts.map +1 -0
  164. package/dist/agent-farm/utils/file-tabs.js +46 -0
  165. package/dist/agent-farm/utils/file-tabs.js.map +1 -0
  166. package/dist/agent-farm/utils/gate-status.d.ts +16 -0
  167. package/dist/agent-farm/utils/gate-status.d.ts.map +1 -0
  168. package/dist/agent-farm/utils/gate-status.js +79 -0
  169. package/dist/agent-farm/utils/gate-status.js.map +1 -0
  170. package/dist/agent-farm/utils/gate-watcher.d.ts +38 -0
  171. package/dist/agent-farm/utils/gate-watcher.d.ts.map +1 -0
  172. package/dist/agent-farm/utils/gate-watcher.js +122 -0
  173. package/dist/agent-farm/utils/gate-watcher.js.map +1 -0
  174. package/dist/agent-farm/utils/index.d.ts +0 -1
  175. package/dist/agent-farm/utils/index.d.ts.map +1 -1
  176. package/dist/agent-farm/utils/index.js +0 -1
  177. package/dist/agent-farm/utils/index.js.map +1 -1
  178. package/dist/agent-farm/utils/notifications.d.ts +30 -0
  179. package/dist/agent-farm/utils/notifications.d.ts.map +1 -0
  180. package/dist/agent-farm/utils/notifications.js +121 -0
  181. package/dist/agent-farm/utils/notifications.js.map +1 -0
  182. package/dist/agent-farm/utils/server-utils.d.ts +5 -5
  183. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  184. package/dist/agent-farm/utils/server-utils.js +5 -16
  185. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  186. package/dist/agent-farm/utils/session.d.ts +32 -0
  187. package/dist/agent-farm/utils/session.d.ts.map +1 -0
  188. package/dist/agent-farm/utils/session.js +57 -0
  189. package/dist/agent-farm/utils/session.js.map +1 -0
  190. package/dist/agent-farm/utils/shell.d.ts +9 -22
  191. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  192. package/dist/agent-farm/utils/shell.js +34 -34
  193. package/dist/agent-farm/utils/shell.js.map +1 -1
  194. package/dist/cli.d.ts.map +1 -1
  195. package/dist/cli.js +6 -37
  196. package/dist/cli.js.map +1 -1
  197. package/dist/commands/adopt.d.ts.map +1 -1
  198. package/dist/commands/adopt.js +33 -4
  199. package/dist/commands/adopt.js.map +1 -1
  200. package/dist/commands/consult/index.d.ts +13 -2
  201. package/dist/commands/consult/index.d.ts.map +1 -1
  202. package/dist/commands/consult/index.js +244 -29
  203. package/dist/commands/consult/index.js.map +1 -1
  204. package/dist/commands/doctor.d.ts.map +1 -1
  205. package/dist/commands/doctor.js +96 -79
  206. package/dist/commands/doctor.js.map +1 -1
  207. package/dist/commands/init.d.ts.map +1 -1
  208. package/dist/commands/init.js +36 -3
  209. package/dist/commands/init.js.map +1 -1
  210. package/dist/commands/porch/build-counter.d.ts +5 -0
  211. package/dist/commands/porch/build-counter.d.ts.map +1 -0
  212. package/dist/commands/porch/build-counter.js +5 -0
  213. package/dist/commands/porch/build-counter.js.map +1 -0
  214. package/dist/commands/porch/checks.d.ts +3 -2
  215. package/dist/commands/porch/checks.d.ts.map +1 -1
  216. package/dist/commands/porch/checks.js +8 -2
  217. package/dist/commands/porch/checks.js.map +1 -1
  218. package/dist/commands/porch/index.d.ts +4 -0
  219. package/dist/commands/porch/index.d.ts.map +1 -1
  220. package/dist/commands/porch/index.js +109 -70
  221. package/dist/commands/porch/index.js.map +1 -1
  222. package/dist/commands/porch/next.d.ts +22 -0
  223. package/dist/commands/porch/next.d.ts.map +1 -0
  224. package/dist/commands/porch/next.js +571 -0
  225. package/dist/commands/porch/next.js.map +1 -0
  226. package/dist/commands/porch/plan.d.ts +11 -1
  227. package/dist/commands/porch/plan.d.ts.map +1 -1
  228. package/dist/commands/porch/plan.js +33 -5
  229. package/dist/commands/porch/plan.js.map +1 -1
  230. package/dist/commands/porch/prompts.d.ts.map +1 -1
  231. package/dist/commands/porch/prompts.js +44 -26
  232. package/dist/commands/porch/prompts.js.map +1 -1
  233. package/dist/commands/porch/protocol.d.ts +6 -4
  234. package/dist/commands/porch/protocol.d.ts.map +1 -1
  235. package/dist/commands/porch/protocol.js +59 -15
  236. package/dist/commands/porch/protocol.js.map +1 -1
  237. package/dist/commands/porch/state.d.ts +29 -2
  238. package/dist/commands/porch/state.d.ts.map +1 -1
  239. package/dist/commands/porch/state.js +71 -3
  240. package/dist/commands/porch/state.js.map +1 -1
  241. package/dist/commands/porch/types.d.ts +45 -2
  242. package/dist/commands/porch/types.d.ts.map +1 -1
  243. package/dist/commands/porch/verdict.d.ts +31 -0
  244. package/dist/commands/porch/verdict.d.ts.map +1 -0
  245. package/dist/commands/porch/verdict.js +59 -0
  246. package/dist/commands/porch/verdict.js.map +1 -0
  247. package/dist/commands/update.d.ts.map +1 -1
  248. package/dist/commands/update.js +18 -6
  249. package/dist/commands/update.js.map +1 -1
  250. package/dist/lib/scaffold.d.ts +13 -0
  251. package/dist/lib/scaffold.d.ts.map +1 -1
  252. package/dist/lib/scaffold.js +36 -0
  253. package/dist/lib/scaffold.js.map +1 -1
  254. package/dist/terminal/index.d.ts +8 -0
  255. package/dist/terminal/index.d.ts.map +1 -0
  256. package/dist/terminal/index.js +5 -0
  257. package/dist/terminal/index.js.map +1 -0
  258. package/dist/terminal/pty-manager.d.ts +69 -0
  259. package/dist/terminal/pty-manager.d.ts.map +1 -0
  260. package/dist/terminal/pty-manager.js +377 -0
  261. package/dist/terminal/pty-manager.js.map +1 -0
  262. package/dist/terminal/pty-session.d.ts +104 -0
  263. package/dist/terminal/pty-session.d.ts.map +1 -0
  264. package/dist/terminal/pty-session.js +327 -0
  265. package/dist/terminal/pty-session.js.map +1 -0
  266. package/dist/terminal/ring-buffer.d.ts +34 -0
  267. package/dist/terminal/ring-buffer.d.ts.map +1 -0
  268. package/dist/terminal/ring-buffer.js +94 -0
  269. package/dist/terminal/ring-buffer.js.map +1 -0
  270. package/dist/terminal/session-manager.d.ts +115 -0
  271. package/dist/terminal/session-manager.d.ts.map +1 -0
  272. package/dist/terminal/session-manager.js +582 -0
  273. package/dist/terminal/session-manager.js.map +1 -0
  274. package/dist/terminal/shellper-client.d.ts +66 -0
  275. package/dist/terminal/shellper-client.d.ts.map +1 -0
  276. package/dist/terminal/shellper-client.js +234 -0
  277. package/dist/terminal/shellper-client.js.map +1 -0
  278. package/dist/terminal/shellper-main.d.ts +19 -0
  279. package/dist/terminal/shellper-main.d.ts.map +1 -0
  280. package/dist/terminal/shellper-main.js +153 -0
  281. package/dist/terminal/shellper-main.js.map +1 -0
  282. package/dist/terminal/shellper-process.d.ts +75 -0
  283. package/dist/terminal/shellper-process.d.ts.map +1 -0
  284. package/dist/terminal/shellper-process.js +279 -0
  285. package/dist/terminal/shellper-process.js.map +1 -0
  286. package/dist/terminal/shellper-protocol.d.ts +115 -0
  287. package/dist/terminal/shellper-protocol.d.ts.map +1 -0
  288. package/dist/terminal/shellper-protocol.js +214 -0
  289. package/dist/terminal/shellper-protocol.js.map +1 -0
  290. package/dist/terminal/shellper-replay-buffer.d.ts +38 -0
  291. package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -0
  292. package/dist/terminal/shellper-replay-buffer.js +94 -0
  293. package/dist/terminal/shellper-replay-buffer.js.map +1 -0
  294. package/dist/terminal/ws-protocol.d.ts +27 -0
  295. package/dist/terminal/ws-protocol.d.ts.map +1 -0
  296. package/dist/terminal/ws-protocol.js +44 -0
  297. package/dist/terminal/ws-protocol.js.map +1 -0
  298. package/package.json +17 -5
  299. package/skeleton/.claude/skills/af/SKILL.md +89 -0
  300. package/skeleton/.claude/skills/codev/SKILL.md +41 -0
  301. package/skeleton/.claude/skills/consult/SKILL.md +81 -0
  302. package/skeleton/.claude/skills/generate-image/SKILL.md +56 -0
  303. package/skeleton/DEPENDENCIES.md +4 -62
  304. package/skeleton/builders.md +1 -1
  305. package/skeleton/consult-types/impl-review.md +18 -9
  306. package/skeleton/consult-types/integration-review.md +1 -1
  307. package/skeleton/consult-types/plan-review.md +1 -1
  308. package/skeleton/consult-types/pr-ready.md +1 -1
  309. package/skeleton/consult-types/spec-review.md +1 -1
  310. package/skeleton/porch/prompts/defend.md +1 -1
  311. package/skeleton/porch/prompts/evaluate.md +2 -2
  312. package/skeleton/porch/prompts/implement.md +1 -1
  313. package/skeleton/porch/prompts/plan.md +1 -1
  314. package/skeleton/porch/prompts/review.md +4 -4
  315. package/skeleton/porch/prompts/specify.md +1 -1
  316. package/skeleton/porch/prompts/understand.md +2 -2
  317. package/skeleton/protocol-schema.json +282 -0
  318. package/skeleton/protocols/bugfix/builder-prompt.md +60 -0
  319. package/skeleton/protocols/bugfix/prompts/fix.md +77 -0
  320. package/skeleton/protocols/bugfix/prompts/investigate.md +77 -0
  321. package/skeleton/protocols/bugfix/prompts/pr.md +84 -0
  322. package/skeleton/protocols/bugfix/protocol.json +20 -33
  323. package/skeleton/protocols/experiment/builder-prompt.md +52 -0
  324. package/skeleton/protocols/experiment/protocol.json +101 -0
  325. package/skeleton/protocols/experiment/protocol.md +3 -3
  326. package/skeleton/protocols/experiment/templates/notes.md +1 -1
  327. package/skeleton/protocols/maintain/builder-prompt.md +46 -0
  328. package/skeleton/protocols/maintain/prompts/audit.md +111 -0
  329. package/skeleton/protocols/maintain/prompts/clean.md +91 -0
  330. package/skeleton/protocols/maintain/prompts/sync.md +113 -0
  331. package/skeleton/protocols/maintain/prompts/verify.md +110 -0
  332. package/skeleton/protocols/maintain/protocol.json +141 -0
  333. package/skeleton/protocols/maintain/protocol.md +17 -11
  334. package/skeleton/protocols/protocol-schema.json +54 -1
  335. package/skeleton/protocols/spir/builder-prompt.md +66 -0
  336. package/skeleton/protocols/spir/prompts/implement.md +208 -0
  337. package/skeleton/protocols/{spider → spir}/prompts/plan.md +6 -70
  338. package/skeleton/protocols/{spider → spir}/prompts/review.md +20 -39
  339. package/skeleton/protocols/{spider → spir}/prompts/specify.md +24 -59
  340. package/skeleton/protocols/{spider → spir}/protocol.json +30 -10
  341. package/skeleton/protocols/{spider → spir}/protocol.md +35 -21
  342. package/skeleton/protocols/spir/templates/review.md +89 -0
  343. package/skeleton/protocols/tick/builder-prompt.md +56 -0
  344. package/skeleton/protocols/tick/protocol.json +7 -2
  345. package/skeleton/protocols/tick/protocol.md +18 -18
  346. package/skeleton/protocols/tick/templates/review.md +1 -1
  347. package/skeleton/resources/commands/agent-farm.md +63 -46
  348. package/skeleton/resources/commands/codev.md +0 -2
  349. package/skeleton/resources/commands/overview.md +7 -17
  350. package/skeleton/resources/workflow-reference.md +4 -4
  351. package/skeleton/roles/architect.md +151 -306
  352. package/skeleton/roles/builder.md +115 -332
  353. package/skeleton/roles/consultant.md +6 -6
  354. package/skeleton/templates/AGENTS.md +2 -2
  355. package/skeleton/templates/CLAUDE.md +2 -2
  356. package/skeleton/templates/cheatsheet.md +7 -5
  357. package/skeleton/templates/projectlist.md +1 -1
  358. package/templates/dashboard/index.html +17 -16
  359. package/templates/dashboard/js/dialogs.js +7 -7
  360. package/templates/dashboard/js/files.js +2 -2
  361. package/templates/dashboard/js/main.js +4 -4
  362. package/templates/dashboard/js/projects.js +3 -3
  363. package/templates/dashboard/js/tabs.js +1 -1
  364. package/templates/dashboard/js/utils.js +22 -1
  365. package/templates/open.html +26 -0
  366. package/templates/tower.html +731 -91
  367. package/dist/agent-farm/commands/kickoff.d.ts +0 -20
  368. package/dist/agent-farm/commands/kickoff.d.ts.map +0 -1
  369. package/dist/agent-farm/commands/kickoff.js +0 -273
  370. package/dist/agent-farm/commands/kickoff.js.map +0 -1
  371. package/dist/agent-farm/commands/rename.d.ts +0 -13
  372. package/dist/agent-farm/commands/rename.d.ts.map +0 -1
  373. package/dist/agent-farm/commands/rename.js +0 -33
  374. package/dist/agent-farm/commands/rename.js.map +0 -1
  375. package/dist/agent-farm/commands/tutorial.d.ts +0 -10
  376. package/dist/agent-farm/commands/tutorial.d.ts.map +0 -1
  377. package/dist/agent-farm/commands/tutorial.js +0 -49
  378. package/dist/agent-farm/commands/tutorial.js.map +0 -1
  379. package/dist/agent-farm/commands/util.d.ts +0 -15
  380. package/dist/agent-farm/commands/util.d.ts.map +0 -1
  381. package/dist/agent-farm/commands/util.js +0 -108
  382. package/dist/agent-farm/commands/util.js.map +0 -1
  383. package/dist/agent-farm/servers/dashboard-server.d.ts +0 -7
  384. package/dist/agent-farm/servers/dashboard-server.d.ts.map +0 -1
  385. package/dist/agent-farm/servers/dashboard-server.js +0 -1858
  386. package/dist/agent-farm/servers/dashboard-server.js.map +0 -1
  387. package/dist/agent-farm/servers/open-server.d.ts +0 -7
  388. package/dist/agent-farm/servers/open-server.d.ts.map +0 -1
  389. package/dist/agent-farm/servers/open-server.js +0 -315
  390. package/dist/agent-farm/servers/open-server.js.map +0 -1
  391. package/dist/agent-farm/tutorial/index.d.ts +0 -8
  392. package/dist/agent-farm/tutorial/index.d.ts.map +0 -1
  393. package/dist/agent-farm/tutorial/index.js +0 -8
  394. package/dist/agent-farm/tutorial/index.js.map +0 -1
  395. package/dist/agent-farm/tutorial/prompts.d.ts +0 -57
  396. package/dist/agent-farm/tutorial/prompts.d.ts.map +0 -1
  397. package/dist/agent-farm/tutorial/prompts.js +0 -147
  398. package/dist/agent-farm/tutorial/prompts.js.map +0 -1
  399. package/dist/agent-farm/tutorial/runner.d.ts +0 -52
  400. package/dist/agent-farm/tutorial/runner.d.ts.map +0 -1
  401. package/dist/agent-farm/tutorial/runner.js +0 -204
  402. package/dist/agent-farm/tutorial/runner.js.map +0 -1
  403. package/dist/agent-farm/tutorial/state.d.ts +0 -26
  404. package/dist/agent-farm/tutorial/state.d.ts.map +0 -1
  405. package/dist/agent-farm/tutorial/state.js +0 -89
  406. package/dist/agent-farm/tutorial/state.js.map +0 -1
  407. package/dist/agent-farm/tutorial/steps/first-spec.d.ts +0 -7
  408. package/dist/agent-farm/tutorial/steps/first-spec.d.ts.map +0 -1
  409. package/dist/agent-farm/tutorial/steps/first-spec.js +0 -136
  410. package/dist/agent-farm/tutorial/steps/first-spec.js.map +0 -1
  411. package/dist/agent-farm/tutorial/steps/implementation.d.ts +0 -7
  412. package/dist/agent-farm/tutorial/steps/implementation.d.ts.map +0 -1
  413. package/dist/agent-farm/tutorial/steps/implementation.js +0 -76
  414. package/dist/agent-farm/tutorial/steps/implementation.js.map +0 -1
  415. package/dist/agent-farm/tutorial/steps/index.d.ts +0 -10
  416. package/dist/agent-farm/tutorial/steps/index.d.ts.map +0 -1
  417. package/dist/agent-farm/tutorial/steps/index.js +0 -10
  418. package/dist/agent-farm/tutorial/steps/index.js.map +0 -1
  419. package/dist/agent-farm/tutorial/steps/planning.d.ts +0 -7
  420. package/dist/agent-farm/tutorial/steps/planning.d.ts.map +0 -1
  421. package/dist/agent-farm/tutorial/steps/planning.js +0 -143
  422. package/dist/agent-farm/tutorial/steps/planning.js.map +0 -1
  423. package/dist/agent-farm/tutorial/steps/review.d.ts +0 -7
  424. package/dist/agent-farm/tutorial/steps/review.d.ts.map +0 -1
  425. package/dist/agent-farm/tutorial/steps/review.js +0 -78
  426. package/dist/agent-farm/tutorial/steps/review.js.map +0 -1
  427. package/dist/agent-farm/tutorial/steps/setup.d.ts +0 -7
  428. package/dist/agent-farm/tutorial/steps/setup.d.ts.map +0 -1
  429. package/dist/agent-farm/tutorial/steps/setup.js +0 -126
  430. package/dist/agent-farm/tutorial/steps/setup.js.map +0 -1
  431. package/dist/agent-farm/tutorial/steps/welcome.d.ts +0 -7
  432. package/dist/agent-farm/tutorial/steps/welcome.d.ts.map +0 -1
  433. package/dist/agent-farm/tutorial/steps/welcome.js +0 -50
  434. package/dist/agent-farm/tutorial/steps/welcome.js.map +0 -1
  435. package/dist/agent-farm/utils/orphan-handler.d.ts +0 -27
  436. package/dist/agent-farm/utils/orphan-handler.d.ts.map +0 -1
  437. package/dist/agent-farm/utils/orphan-handler.js +0 -149
  438. package/dist/agent-farm/utils/orphan-handler.js.map +0 -1
  439. package/dist/agent-farm/utils/port-registry.d.ts +0 -58
  440. package/dist/agent-farm/utils/port-registry.d.ts.map +0 -1
  441. package/dist/agent-farm/utils/port-registry.js +0 -166
  442. package/dist/agent-farm/utils/port-registry.js.map +0 -1
  443. package/dist/agent-farm/utils/terminal-ports.d.ts +0 -18
  444. package/dist/agent-farm/utils/terminal-ports.d.ts.map +0 -1
  445. package/dist/agent-farm/utils/terminal-ports.js +0 -35
  446. package/dist/agent-farm/utils/terminal-ports.js.map +0 -1
  447. package/dist/commands/pcheck/cache.d.ts +0 -48
  448. package/dist/commands/pcheck/cache.d.ts.map +0 -1
  449. package/dist/commands/pcheck/cache.js +0 -170
  450. package/dist/commands/pcheck/cache.js.map +0 -1
  451. package/dist/commands/pcheck/evaluator.d.ts +0 -15
  452. package/dist/commands/pcheck/evaluator.d.ts.map +0 -1
  453. package/dist/commands/pcheck/evaluator.js +0 -246
  454. package/dist/commands/pcheck/evaluator.js.map +0 -1
  455. package/dist/commands/pcheck/index.d.ts +0 -12
  456. package/dist/commands/pcheck/index.d.ts.map +0 -1
  457. package/dist/commands/pcheck/index.js +0 -249
  458. package/dist/commands/pcheck/index.js.map +0 -1
  459. package/dist/commands/pcheck/parser.d.ts +0 -39
  460. package/dist/commands/pcheck/parser.d.ts.map +0 -1
  461. package/dist/commands/pcheck/parser.js +0 -155
  462. package/dist/commands/pcheck/parser.js.map +0 -1
  463. package/dist/commands/pcheck/types.d.ts +0 -82
  464. package/dist/commands/pcheck/types.d.ts.map +0 -1
  465. package/dist/commands/pcheck/types.js +0 -5
  466. package/dist/commands/pcheck/types.js.map +0 -1
  467. package/dist/commands/porch/claude.d.ts +0 -29
  468. package/dist/commands/porch/claude.d.ts.map +0 -1
  469. package/dist/commands/porch/claude.js +0 -79
  470. package/dist/commands/porch/claude.js.map +0 -1
  471. package/dist/commands/porch/consultation.d.ts +0 -56
  472. package/dist/commands/porch/consultation.d.ts.map +0 -1
  473. package/dist/commands/porch/consultation.js +0 -330
  474. package/dist/commands/porch/consultation.js.map +0 -1
  475. package/dist/commands/porch/notifications.d.ts +0 -99
  476. package/dist/commands/porch/notifications.d.ts.map +0 -1
  477. package/dist/commands/porch/notifications.js +0 -223
  478. package/dist/commands/porch/notifications.js.map +0 -1
  479. package/dist/commands/porch/plan-parser.d.ts +0 -38
  480. package/dist/commands/porch/plan-parser.d.ts.map +0 -1
  481. package/dist/commands/porch/plan-parser.js +0 -166
  482. package/dist/commands/porch/plan-parser.js.map +0 -1
  483. package/dist/commands/porch/protocol-loader.d.ts +0 -46
  484. package/dist/commands/porch/protocol-loader.d.ts.map +0 -1
  485. package/dist/commands/porch/protocol-loader.js +0 -262
  486. package/dist/commands/porch/protocol-loader.js.map +0 -1
  487. package/dist/commands/porch/repl.d.ts +0 -33
  488. package/dist/commands/porch/repl.d.ts.map +0 -1
  489. package/dist/commands/porch/repl.js +0 -206
  490. package/dist/commands/porch/repl.js.map +0 -1
  491. package/dist/commands/porch/run.d.ts +0 -15
  492. package/dist/commands/porch/run.d.ts.map +0 -1
  493. package/dist/commands/porch/run.js +0 -551
  494. package/dist/commands/porch/run.js.map +0 -1
  495. package/dist/commands/porch/signal-parser.d.ts +0 -102
  496. package/dist/commands/porch/signal-parser.d.ts.map +0 -1
  497. package/dist/commands/porch/signal-parser.js +0 -199
  498. package/dist/commands/porch/signal-parser.js.map +0 -1
  499. package/dist/commands/porch/signals.d.ts +0 -35
  500. package/dist/commands/porch/signals.d.ts.map +0 -1
  501. package/dist/commands/porch/signals.js +0 -76
  502. package/dist/commands/porch/signals.js.map +0 -1
  503. package/dist/commands/porch2/checks.d.ts +0 -29
  504. package/dist/commands/porch2/checks.d.ts.map +0 -1
  505. package/dist/commands/porch2/checks.js +0 -141
  506. package/dist/commands/porch2/checks.js.map +0 -1
  507. package/dist/commands/porch2/index.d.ts +0 -38
  508. package/dist/commands/porch2/index.d.ts.map +0 -1
  509. package/dist/commands/porch2/index.js +0 -483
  510. package/dist/commands/porch2/index.js.map +0 -1
  511. package/dist/commands/porch2/plan.d.ts +0 -70
  512. package/dist/commands/porch2/plan.d.ts.map +0 -1
  513. package/dist/commands/porch2/plan.js +0 -227
  514. package/dist/commands/porch2/plan.js.map +0 -1
  515. package/dist/commands/porch2/protocol.d.ts +0 -37
  516. package/dist/commands/porch2/protocol.d.ts.map +0 -1
  517. package/dist/commands/porch2/protocol.js +0 -183
  518. package/dist/commands/porch2/protocol.js.map +0 -1
  519. package/dist/commands/porch2/state.d.ts +0 -35
  520. package/dist/commands/porch2/state.d.ts.map +0 -1
  521. package/dist/commands/porch2/state.js +0 -124
  522. package/dist/commands/porch2/state.js.map +0 -1
  523. package/dist/commands/porch2/types.d.ts +0 -79
  524. package/dist/commands/porch2/types.d.ts.map +0 -1
  525. package/dist/commands/porch2/types.js +0 -8
  526. package/dist/commands/porch2/types.js.map +0 -1
  527. package/dist/commands/tower.d.ts +0 -16
  528. package/dist/commands/tower.d.ts.map +0 -1
  529. package/dist/commands/tower.js +0 -21
  530. package/dist/commands/tower.js.map +0 -1
  531. package/skeleton/config.json +0 -7
  532. package/skeleton/protocols/spider/prompts/defend.md +0 -215
  533. package/skeleton/protocols/spider/prompts/evaluate.md +0 -241
  534. package/skeleton/protocols/spider/prompts/implement.md +0 -149
  535. package/skeleton/protocols/spider/templates/review.md +0 -207
  536. /package/skeleton/protocols/{spider → spir}/templates/plan.md +0 -0
  537. /package/skeleton/protocols/{spider → spir}/templates/spec.md +0 -0
@@ -1,1858 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Dashboard server for Agent Farm.
4
- * Serves the split-pane dashboard UI and provides state/tab management APIs.
5
- */
6
- import http from 'node:http';
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
- import net from 'node:net';
10
- import httpProxy from 'http-proxy';
11
- import { spawn, execSync, exec } from 'node:child_process';
12
- import { promisify } from 'node:util';
13
- import { randomUUID } from 'node:crypto';
14
- import { fileURLToPath } from 'node:url';
15
- const execAsync = promisify(exec);
16
- import { Command } from 'commander';
17
- import { getPortForTerminal } from '../utils/terminal-ports.js';
18
- import { escapeHtml, parseJsonBody, isRequestAllowed as isRequestAllowedBase, } from '../utils/server-utils.js';
19
- import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils, tryAddUtil, removeUtil, getBuilder, getBuilders, removeBuilder, upsertBuilder, clearState, } from '../state.js';
20
- import { spawnTtyd } from '../utils/shell.js';
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = path.dirname(__filename);
23
- // Default dashboard port
24
- const DEFAULT_DASHBOARD_PORT = 4200;
25
- // Parse arguments with Commander for proper --help and validation
26
- const program = new Command()
27
- .name('dashboard-server')
28
- .description('Dashboard server for Agent Farm')
29
- .argument('[port]', 'Port to listen on', String(DEFAULT_DASHBOARD_PORT))
30
- .argument('[bindHost]', 'Host to bind to (default: localhost, use 0.0.0.0 for remote)')
31
- .option('-p, --port <port>', 'Port to listen on (overrides positional argument)')
32
- .option('-b, --bind <host>', 'Host to bind to (overrides positional argument)')
33
- .parse(process.argv);
34
- const opts = program.opts();
35
- const args = program.args;
36
- // Support both positional arg and --port flag (flag takes precedence)
37
- const portArg = opts.port || args[0] || String(DEFAULT_DASHBOARD_PORT);
38
- const port = parseInt(portArg, 10);
39
- // Bind host: flag > positional arg > default (undefined = localhost)
40
- const bindHost = opts.bind || args[1] || undefined;
41
- if (isNaN(port) || port < 1 || port > 65535) {
42
- console.error(`Error: Invalid port "${portArg}". Must be a number between 1 and 65535.`);
43
- process.exit(1);
44
- }
45
- // Configuration - ports are relative to the dashboard port
46
- // This ensures multi-project support (e.g., dashboard on 4300 uses 4350 for annotations)
47
- const CONFIG = {
48
- dashboardPort: port,
49
- architectPort: port + 1,
50
- builderPortStart: port + 10,
51
- utilPortStart: port + 30,
52
- openPortStart: port + 50,
53
- maxTabs: 20, // DoS protection: max concurrent tabs
54
- };
55
- // Find project root by looking for .agent-farm directory
56
- function findProjectRoot() {
57
- let dir = process.cwd();
58
- while (dir !== '/') {
59
- if (fs.existsSync(path.join(dir, '.agent-farm'))) {
60
- return dir;
61
- }
62
- if (fs.existsSync(path.join(dir, 'codev'))) {
63
- return dir;
64
- }
65
- dir = path.dirname(dir);
66
- }
67
- return process.cwd();
68
- }
69
- // Get project name from root path, with truncation for long names
70
- function getProjectName(projectRoot) {
71
- const baseName = path.basename(projectRoot);
72
- const maxLength = 30;
73
- if (baseName.length <= maxLength) {
74
- return baseName;
75
- }
76
- // Truncate with ellipsis for very long names
77
- return '...' + baseName.slice(-(maxLength - 3));
78
- }
79
- function findTemplatePath(filename, required = false) {
80
- // Templates are at package root: packages/codev/templates/
81
- // From compiled: dist/agent-farm/servers/ -> ../../../templates/
82
- // From source: src/agent-farm/servers/ -> ../../../templates/
83
- const pkgPath = path.resolve(__dirname, '../../../templates/', filename);
84
- if (fs.existsSync(pkgPath))
85
- return pkgPath;
86
- if (required) {
87
- throw new Error(`Template not found: ${filename}`);
88
- }
89
- return null;
90
- }
91
- const projectRoot = findProjectRoot();
92
- // Use modular dashboard template (Spec 0060)
93
- const templatePath = findTemplatePath('dashboard/index.html', true);
94
- // Clean up dead processes from state (called on state load)
95
- function cleanupDeadProcesses() {
96
- // Clean up dead shell processes
97
- for (const util of getUtils()) {
98
- if (!isProcessRunning(util.pid)) {
99
- console.log(`Auto-closing shell tab ${util.name} (process ${util.pid} exited)`);
100
- if (util.tmuxSession) {
101
- killTmuxSession(util.tmuxSession);
102
- }
103
- removeUtil(util.id);
104
- }
105
- }
106
- // Clean up dead annotation processes
107
- for (const annotation of getAnnotations()) {
108
- if (!isProcessRunning(annotation.pid)) {
109
- console.log(`Auto-closing file tab ${annotation.file} (process ${annotation.pid} exited)`);
110
- removeAnnotation(annotation.id);
111
- }
112
- }
113
- }
114
- // Load state with cleanup
115
- function loadStateWithCleanup() {
116
- cleanupDeadProcesses();
117
- return loadState();
118
- }
119
- // Generate unique ID using crypto for collision resistance
120
- function generateId(prefix) {
121
- const uuid = randomUUID().replace(/-/g, '').substring(0, 8).toUpperCase();
122
- return `${prefix}${uuid}`;
123
- }
124
- // Get all ports currently used in state
125
- function getUsedPorts(state) {
126
- const ports = new Set();
127
- if (state.architect?.port)
128
- ports.add(state.architect.port);
129
- for (const builder of state.builders || []) {
130
- if (builder.port)
131
- ports.add(builder.port);
132
- }
133
- for (const util of state.utils || []) {
134
- if (util.port)
135
- ports.add(util.port);
136
- }
137
- for (const annotation of state.annotations || []) {
138
- if (annotation.port)
139
- ports.add(annotation.port);
140
- }
141
- return ports;
142
- }
143
- // Find available port in range (checks both state and actual availability)
144
- async function findAvailablePort(startPort, state) {
145
- // Get ports already allocated in state
146
- const usedPorts = state ? getUsedPorts(state) : new Set();
147
- // Skip ports already in state
148
- let port = startPort;
149
- while (usedPorts.has(port)) {
150
- port++;
151
- }
152
- // Then verify the port is actually available for binding
153
- return new Promise((resolve) => {
154
- const server = net.createServer();
155
- server.listen(port, () => {
156
- const { port: boundPort } = server.address();
157
- server.close(() => resolve(boundPort));
158
- });
159
- server.on('error', () => {
160
- resolve(findAvailablePort(port + 1, state));
161
- });
162
- });
163
- }
164
- // Wait for a port to be accepting connections (server ready)
165
- async function waitForPortReady(port, timeoutMs = 5000) {
166
- const startTime = Date.now();
167
- const pollInterval = 100; // Check every 100ms
168
- while (Date.now() - startTime < timeoutMs) {
169
- const isReady = await new Promise((resolve) => {
170
- const socket = new net.Socket();
171
- socket.setTimeout(pollInterval);
172
- socket.on('connect', () => {
173
- socket.destroy();
174
- resolve(true);
175
- });
176
- socket.on('error', () => {
177
- socket.destroy();
178
- resolve(false);
179
- });
180
- socket.on('timeout', () => {
181
- socket.destroy();
182
- resolve(false);
183
- });
184
- socket.connect(port, '127.0.0.1');
185
- });
186
- if (isReady) {
187
- return true;
188
- }
189
- // Wait before next poll
190
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
191
- }
192
- return false;
193
- }
194
- // Kill tmux session
195
- function killTmuxSession(sessionName) {
196
- try {
197
- execSync(`tmux kill-session -t "${sessionName}" 2>/dev/null`, { stdio: 'ignore' });
198
- }
199
- catch {
200
- // Session may not exist
201
- }
202
- }
203
- // Check if a process is running
204
- function isProcessRunning(pid) {
205
- try {
206
- // Signal 0 doesn't kill, just checks if process exists
207
- process.kill(pid, 0);
208
- return true;
209
- }
210
- catch {
211
- return false;
212
- }
213
- }
214
- // Graceful process termination with two-phase shutdown
215
- async function killProcessGracefully(pid, tmuxSession) {
216
- // First kill tmux session if provided
217
- if (tmuxSession) {
218
- killTmuxSession(tmuxSession);
219
- }
220
- try {
221
- // First try SIGTERM
222
- process.kill(pid, 'SIGTERM');
223
- // Wait up to 500ms for process to exit
224
- await new Promise((resolve) => {
225
- let attempts = 0;
226
- const checkInterval = setInterval(() => {
227
- attempts++;
228
- try {
229
- // Signal 0 checks if process exists
230
- process.kill(pid, 0);
231
- if (attempts >= 5) {
232
- // Process still alive after 500ms, use SIGKILL
233
- clearInterval(checkInterval);
234
- try {
235
- process.kill(pid, 'SIGKILL');
236
- }
237
- catch {
238
- // Already dead
239
- }
240
- resolve();
241
- }
242
- }
243
- catch {
244
- // Process is dead
245
- clearInterval(checkInterval);
246
- resolve();
247
- }
248
- }, 100);
249
- });
250
- }
251
- catch {
252
- // Process may already be dead
253
- }
254
- }
255
- // Spawn detached process with error handling
256
- function spawnDetached(command, args, cwd) {
257
- try {
258
- const child = spawn(command, args, {
259
- cwd,
260
- detached: true,
261
- stdio: 'ignore',
262
- });
263
- child.on('error', (err) => {
264
- console.error(`Failed to spawn ${command}:`, err.message);
265
- });
266
- child.unref();
267
- return child.pid || null;
268
- }
269
- catch (err) {
270
- console.error(`Failed to spawn ${command}:`, err.message);
271
- return null;
272
- }
273
- }
274
- // Check if tmux session exists
275
- function tmuxSessionExists(sessionName) {
276
- try {
277
- execSync(`tmux has-session -t "${sessionName}" 2>/dev/null`, { stdio: 'ignore' });
278
- return true;
279
- }
280
- catch {
281
- return false;
282
- }
283
- }
284
- // Create a persistent tmux session and attach ttyd to it
285
- // Idempotent: if session exists, just spawn ttyd to attach to it
286
- function spawnTmuxWithTtyd(sessionName, shellCommand, ttydPort, cwd) {
287
- try {
288
- // Only create session if it doesn't exist (idempotent)
289
- if (!tmuxSessionExists(sessionName)) {
290
- // Create tmux session with the shell command
291
- execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 "${shellCommand}"`, { cwd, stdio: 'ignore' });
292
- // Hide the tmux status bar (dashboard has its own tabs)
293
- execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
294
- // Enable mouse support in the session
295
- execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
296
- // Enable OSC 52 clipboard (allows copy to browser clipboard via ttyd)
297
- execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
298
- // Enable passthrough for hyperlinks and clipboard
299
- execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
300
- // Copy selection to clipboard when mouse is released
301
- // Use copy-pipe-and-cancel with pbcopy to directly copy to system clipboard
302
- // (OSC 52 via set-clipboard doesn't work reliably through ttyd/xterm.js)
303
- execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
304
- execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
305
- }
306
- // Start ttyd to attach to the tmux session
307
- const customIndexPath = findTemplatePath('ttyd-index.html');
308
- const ttydProcess = spawnTtyd({
309
- port: ttydPort,
310
- sessionName,
311
- cwd,
312
- customIndexPath: customIndexPath ?? undefined,
313
- });
314
- return ttydProcess?.pid ?? null;
315
- }
316
- catch (err) {
317
- console.error(`Failed to create tmux session ${sessionName}:`, err.message);
318
- // Cleanup any partial session
319
- killTmuxSession(sessionName);
320
- return null;
321
- }
322
- }
323
- /**
324
- * Generate a short 4-character base64-encoded ID for worktree names
325
- */
326
- function generateShortId() {
327
- const num = Math.floor(Math.random() * 0xFFFFFF);
328
- const bytes = new Uint8Array([num >> 16, (num >> 8) & 0xFF, num & 0xFF]);
329
- return btoa(String.fromCharCode(...bytes))
330
- .replace(/\+/g, '-')
331
- .replace(/\//g, '_')
332
- .replace(/=/g, '')
333
- .substring(0, 4);
334
- }
335
- /**
336
- * Spawn a worktree builder - creates git worktree and starts builder CLI
337
- * Similar to shell spawning but with git worktree isolation
338
- */
339
- function spawnWorktreeBuilder(builderPort, state) {
340
- const shortId = generateShortId();
341
- const builderId = `worktree-${shortId}`;
342
- const branchName = `builder/worktree-${shortId}`;
343
- const worktreePath = path.resolve(projectRoot, '.builders', builderId);
344
- const sessionName = `builder-${builderId}`;
345
- try {
346
- // Ensure .builders directory exists
347
- const buildersDir = path.resolve(projectRoot, '.builders');
348
- if (!fs.existsSync(buildersDir)) {
349
- fs.mkdirSync(buildersDir, { recursive: true });
350
- }
351
- // Create git branch and worktree
352
- execSync(`git branch "${branchName}" HEAD`, { cwd: projectRoot, stdio: 'ignore' });
353
- execSync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
354
- // Get builder command from config or use default shell
355
- const configPath = path.resolve(projectRoot, 'codev', 'config.json');
356
- const defaultShell = process.env.SHELL || 'bash';
357
- let builderCommand = defaultShell;
358
- if (fs.existsSync(configPath)) {
359
- try {
360
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
361
- builderCommand = config?.shell?.builder || defaultShell;
362
- }
363
- catch {
364
- // Use default
365
- }
366
- }
367
- // Create tmux session with builder command
368
- execSync(`tmux new-session -d -s "${sessionName}" -x 200 -y 50 -c "${worktreePath}" "${builderCommand}"`, { cwd: worktreePath, stdio: 'ignore' });
369
- // Hide the tmux status bar (dashboard has its own tabs)
370
- execSync(`tmux set-option -t "${sessionName}" status off`, { stdio: 'ignore' });
371
- // Enable mouse support
372
- execSync(`tmux set-option -t "${sessionName}" -g mouse on`, { stdio: 'ignore' });
373
- execSync(`tmux set-option -t "${sessionName}" -g set-clipboard on`, { stdio: 'ignore' });
374
- execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'ignore' });
375
- // Copy selection to clipboard when mouse is released (pbcopy for macOS)
376
- execSync(`tmux bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
377
- execSync(`tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"`, { stdio: 'ignore' });
378
- // Start ttyd connecting to the tmux session
379
- const customIndexPath = findTemplatePath('ttyd-index.html');
380
- const ttydProcess = spawnTtyd({
381
- port: builderPort,
382
- sessionName,
383
- cwd: worktreePath,
384
- customIndexPath: customIndexPath ?? undefined,
385
- });
386
- const pid = ttydProcess?.pid ?? null;
387
- if (!pid) {
388
- // Cleanup on failure
389
- killTmuxSession(sessionName);
390
- try {
391
- execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
392
- execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
393
- }
394
- catch {
395
- // Best effort cleanup
396
- }
397
- return null;
398
- }
399
- const builder = {
400
- id: builderId,
401
- name: `Worktree ${shortId}`,
402
- port: builderPort,
403
- pid,
404
- status: 'implementing',
405
- phase: 'interactive',
406
- worktree: worktreePath,
407
- branch: branchName,
408
- tmuxSession: sessionName,
409
- type: 'worktree',
410
- };
411
- return { builder, pid };
412
- }
413
- catch (err) {
414
- console.error(`Failed to spawn worktree builder:`, err.message);
415
- // Cleanup any partial state
416
- killTmuxSession(sessionName);
417
- try {
418
- execSync(`git worktree remove "${worktreePath}" --force`, { cwd: projectRoot, stdio: 'ignore' });
419
- execSync(`git branch -D "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
420
- }
421
- catch {
422
- // Best effort cleanup
423
- }
424
- return null;
425
- }
426
- }
427
- // parseJsonBody imported from ../utils/server-utils.js
428
- // Validate path is within project root (prevent path traversal)
429
- // Handles URL-encoded dots (%2e), symlinks, and other encodings
430
- function validatePathWithinProject(filePath) {
431
- // First decode any URL encoding to catch %2e%2e (encoded ..)
432
- let decodedPath;
433
- try {
434
- decodedPath = decodeURIComponent(filePath);
435
- }
436
- catch {
437
- // Invalid encoding
438
- return null;
439
- }
440
- // Resolve to absolute path
441
- const resolvedPath = decodedPath.startsWith('/')
442
- ? path.resolve(decodedPath)
443
- : path.resolve(projectRoot, decodedPath);
444
- // Normalize to remove any .. or . segments
445
- const normalizedPath = path.normalize(resolvedPath);
446
- // First check normalized path (for paths that don't exist yet)
447
- if (!normalizedPath.startsWith(projectRoot + path.sep) && normalizedPath !== projectRoot) {
448
- return null; // Path escapes project root
449
- }
450
- // If file exists, resolve symlinks to prevent symlink-based path traversal
451
- // An attacker could create a symlink within the repo pointing outside
452
- if (fs.existsSync(normalizedPath)) {
453
- try {
454
- const realPath = fs.realpathSync(normalizedPath);
455
- if (!realPath.startsWith(projectRoot + path.sep) && realPath !== projectRoot) {
456
- return null; // Symlink target escapes project root
457
- }
458
- return realPath;
459
- }
460
- catch {
461
- // realpathSync failed (broken symlink, permissions, etc.)
462
- return null;
463
- }
464
- }
465
- return normalizedPath;
466
- }
467
- // Count total tabs for DoS protection
468
- function countTotalTabs(state) {
469
- return state.builders.length + state.utils.length + state.annotations.length;
470
- }
471
- // Find open server script (prefer .ts for dev, .js for compiled)
472
- function getOpenServerPath() {
473
- const tsPath = path.join(__dirname, 'open-server.ts');
474
- const jsPath = path.join(__dirname, 'open-server.js');
475
- if (fs.existsSync(tsPath)) {
476
- return { script: tsPath, useTsx: true };
477
- }
478
- return { script: jsPath, useTsx: false };
479
- }
480
- /**
481
- * Escape a string for safe use in shell commands
482
- * Handles special characters that could cause command injection
483
- */
484
- function escapeShellArg(str) {
485
- // Single-quote the string and escape any single quotes within it
486
- return "'" + str.replace(/'/g, "'\\''") + "'";
487
- }
488
- /**
489
- * Get today's git commits from all branches for the current user
490
- */
491
- async function getGitCommits(projectRoot) {
492
- try {
493
- const { stdout: authorRaw } = await execAsync('git config user.name', { cwd: projectRoot });
494
- const author = authorRaw.trim();
495
- if (!author)
496
- return [];
497
- // Escape author name to prevent command injection
498
- const safeAuthor = escapeShellArg(author);
499
- // Get commits from all branches since midnight
500
- const { stdout: output } = await execAsync(`git log --all --since="midnight" --author=${safeAuthor} --format="%H|%s|%aI|%D"`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
501
- if (!output.trim())
502
- return [];
503
- return output.trim().split('\n').filter(Boolean).map(line => {
504
- const parts = line.split('|');
505
- const hash = parts[0] || '';
506
- const message = parts[1] || '';
507
- const time = parts[2] || '';
508
- const refs = parts.slice(3).join('|'); // refs might contain |
509
- // Extract branch name from refs
510
- let branch = 'unknown';
511
- const headMatch = refs.match(/HEAD -> ([^,]+)/);
512
- const branchMatch = refs.match(/([^,\s]+)$/);
513
- if (headMatch) {
514
- branch = headMatch[1];
515
- }
516
- else if (branchMatch && branchMatch[1]) {
517
- branch = branchMatch[1];
518
- }
519
- return {
520
- hash: hash.slice(0, 7),
521
- message: message.slice(0, 100), // Truncate long messages
522
- time,
523
- branch,
524
- };
525
- });
526
- }
527
- catch (err) {
528
- console.error('Error getting git commits:', err.message);
529
- return [];
530
- }
531
- }
532
- /**
533
- * Get unique files modified today
534
- */
535
- async function getModifiedFiles(projectRoot) {
536
- try {
537
- const { stdout: authorRaw } = await execAsync('git config user.name', { cwd: projectRoot });
538
- const author = authorRaw.trim();
539
- if (!author)
540
- return [];
541
- // Escape author name to prevent command injection
542
- const safeAuthor = escapeShellArg(author);
543
- const { stdout: output } = await execAsync(`git log --all --since="midnight" --author=${safeAuthor} --name-only --format=""`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
544
- if (!output.trim())
545
- return [];
546
- const files = [...new Set(output.trim().split('\n').filter(Boolean))];
547
- return files.sort();
548
- }
549
- catch (err) {
550
- console.error('Error getting modified files:', err.message);
551
- return [];
552
- }
553
- }
554
- /**
555
- * Get GitHub PRs created or merged today via gh CLI
556
- * Combines PRs created today AND PRs merged today (which may have been created earlier)
557
- */
558
- async function getGitHubPRs(projectRoot) {
559
- try {
560
- // Use local time for the date (spec says "today" means local machine time)
561
- const now = new Date();
562
- const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
563
- // Fetch PRs created today AND PRs merged today in parallel
564
- const [createdResult, mergedResult] = await Promise.allSettled([
565
- execAsync(`gh pr list --author "@me" --state all --search "created:>=${today}" --json number,title,state,url`, { cwd: projectRoot, timeout: 15000 }),
566
- execAsync(`gh pr list --author "@me" --state merged --search "merged:>=${today}" --json number,title,state,url`, { cwd: projectRoot, timeout: 15000 }),
567
- ]);
568
- const prsMap = new Map();
569
- // Process PRs created today
570
- if (createdResult.status === 'fulfilled' && createdResult.value.stdout.trim()) {
571
- const prs = JSON.parse(createdResult.value.stdout);
572
- for (const pr of prs) {
573
- prsMap.set(pr.number, {
574
- number: pr.number,
575
- title: pr.title.slice(0, 100),
576
- state: pr.state,
577
- url: pr.url,
578
- });
579
- }
580
- }
581
- // Process PRs merged today (may overlap with created, deduped by Map)
582
- if (mergedResult.status === 'fulfilled' && mergedResult.value.stdout.trim()) {
583
- const prs = JSON.parse(mergedResult.value.stdout);
584
- for (const pr of prs) {
585
- prsMap.set(pr.number, {
586
- number: pr.number,
587
- title: pr.title.slice(0, 100),
588
- state: pr.state,
589
- url: pr.url,
590
- });
591
- }
592
- }
593
- return Array.from(prsMap.values());
594
- }
595
- catch (err) {
596
- // gh CLI might not be available or authenticated
597
- console.error('Error getting GitHub PRs:', err.message);
598
- return [];
599
- }
600
- }
601
- /**
602
- * Get builder activity from state.db for today
603
- * Note: state.json doesn't track timestamps, so we can only report current builders
604
- * without duration. They'll be counted as activity points, not time intervals.
605
- */
606
- function getBuilderActivity() {
607
- try {
608
- const builders = getBuilders();
609
- // Return current builders without time tracking (state.json lacks timestamps)
610
- // Time tracking will rely primarily on git commits
611
- return builders.map(b => ({
612
- id: b.id,
613
- status: b.status || 'unknown',
614
- startTime: '', // Unknown - not tracked in state.json
615
- endTime: undefined,
616
- }));
617
- }
618
- catch (err) {
619
- console.error('Error getting builder activity:', err.message);
620
- return [];
621
- }
622
- }
623
- /**
624
- * Detect project status changes in projectlist.md today
625
- * Handles YAML format inside Markdown fenced code blocks
626
- */
627
- async function getProjectChanges(projectRoot) {
628
- try {
629
- const projectlistPath = path.join(projectRoot, 'codev/projectlist.md');
630
- if (!fs.existsSync(projectlistPath))
631
- return [];
632
- // Get the first commit hash from today that touched projectlist.md
633
- const { stdout: firstCommitOutput } = await execAsync(`git log --since="midnight" --format=%H -- codev/projectlist.md | tail -1`, { cwd: projectRoot });
634
- if (!firstCommitOutput.trim())
635
- return [];
636
- // Get diff of projectlist.md from that commit's parent to HEAD
637
- let diff;
638
- try {
639
- const { stdout } = await execAsync(`git diff ${firstCommitOutput.trim()}^..HEAD -- codev/projectlist.md`, { cwd: projectRoot, maxBuffer: 1024 * 1024 });
640
- diff = stdout;
641
- }
642
- catch {
643
- return [];
644
- }
645
- if (!diff.trim())
646
- return [];
647
- // Parse status changes from diff
648
- // Format is YAML inside Markdown code blocks:
649
- // - id: "0058"
650
- // title: "File Search Autocomplete"
651
- // status: implementing
652
- const changes = [];
653
- const lines = diff.split('\n');
654
- let currentId = '';
655
- let currentTitle = '';
656
- let oldStatus = '';
657
- let newStatus = '';
658
- for (const line of lines) {
659
- // Track current project context from YAML id field
660
- // Match lines like: " - id: \"0058\"" or "+ - id: \"0058\""
661
- const idMatch = line.match(/^[+-]?\s*-\s*id:\s*["']?(\d{4})["']?/);
662
- if (idMatch) {
663
- // If we have a pending status change from previous project, emit it
664
- if (oldStatus && newStatus && currentId) {
665
- changes.push({
666
- id: currentId,
667
- title: currentTitle,
668
- oldStatus,
669
- newStatus,
670
- });
671
- oldStatus = '';
672
- newStatus = '';
673
- }
674
- currentId = idMatch[1];
675
- currentTitle = ''; // Will be filled by title line
676
- }
677
- // Track title (comes after id in YAML)
678
- // Match lines like: " title: \"File Search Autocomplete\""
679
- const titleMatch = line.match(/^[+-]?\s*title:\s*["']?([^"']+)["']?/);
680
- if (titleMatch && currentId) {
681
- currentTitle = titleMatch[1].trim();
682
- }
683
- // Track status changes
684
- // Match lines like: "- status: implementing" or "+ status: implemented"
685
- const statusMatch = line.match(/^([+-])\s*status:\s*(\w+)/);
686
- if (statusMatch) {
687
- const [, modifier, status] = statusMatch;
688
- if (modifier === '-') {
689
- oldStatus = status;
690
- }
691
- else if (modifier === '+') {
692
- newStatus = status;
693
- }
694
- }
695
- }
696
- // Emit final pending change if exists
697
- if (oldStatus && newStatus && currentId) {
698
- changes.push({
699
- id: currentId,
700
- title: currentTitle,
701
- oldStatus,
702
- newStatus,
703
- });
704
- }
705
- return changes;
706
- }
707
- catch (err) {
708
- console.error('Error getting project changes:', err.message);
709
- return [];
710
- }
711
- }
712
- /**
713
- * Merge overlapping time intervals
714
- */
715
- function mergeIntervals(intervals) {
716
- if (intervals.length === 0)
717
- return [];
718
- // Sort by start time
719
- const sorted = [...intervals].sort((a, b) => a.start.getTime() - b.start.getTime());
720
- const merged = [{ ...sorted[0] }];
721
- for (let i = 1; i < sorted.length; i++) {
722
- const last = merged[merged.length - 1];
723
- const current = sorted[i];
724
- // If overlapping or within 2 hours, merge
725
- const gapMs = current.start.getTime() - last.end.getTime();
726
- const twoHoursMs = 2 * 60 * 60 * 1000;
727
- if (gapMs <= twoHoursMs) {
728
- last.end = new Date(Math.max(last.end.getTime(), current.end.getTime()));
729
- }
730
- else {
731
- merged.push({ ...current });
732
- }
733
- }
734
- return merged;
735
- }
736
- /**
737
- * Calculate active time from commits and builder activity
738
- */
739
- function calculateTimeTracking(commits, builders) {
740
- const intervals = [];
741
- const fiveMinutesMs = 5 * 60 * 1000;
742
- // Add commit timestamps (treat each as 5-minute interval)
743
- for (const commit of commits) {
744
- if (commit.time) {
745
- const time = new Date(commit.time);
746
- if (!isNaN(time.getTime())) {
747
- intervals.push({
748
- start: time,
749
- end: new Date(time.getTime() + fiveMinutesMs),
750
- });
751
- }
752
- }
753
- }
754
- // Add builder sessions
755
- for (const builder of builders) {
756
- if (builder.startTime) {
757
- const start = new Date(builder.startTime);
758
- const end = builder.endTime ? new Date(builder.endTime) : new Date();
759
- if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
760
- intervals.push({ start, end });
761
- }
762
- }
763
- }
764
- if (intervals.length === 0) {
765
- return {
766
- activeMinutes: 0,
767
- firstActivity: '',
768
- lastActivity: '',
769
- };
770
- }
771
- const merged = mergeIntervals(intervals);
772
- const totalMinutes = merged.reduce((sum, interval) => sum + (interval.end.getTime() - interval.start.getTime()) / (1000 * 60), 0);
773
- return {
774
- activeMinutes: Math.round(totalMinutes),
775
- firstActivity: merged[0].start.toISOString(),
776
- lastActivity: merged[merged.length - 1].end.toISOString(),
777
- };
778
- }
779
- /**
780
- * Find the consult CLI path
781
- * Returns the path to the consult binary, checking multiple locations
782
- */
783
- function findConsultPath() {
784
- // When running from dist/, check relative paths
785
- // dist/agent-farm/servers/ -> ../../../bin/consult.js
786
- const distPath = path.join(__dirname, '../../../bin/consult.js');
787
- if (fs.existsSync(distPath)) {
788
- return distPath;
789
- }
790
- // When running from src/ with tsx, check src-relative paths
791
- // src/agent-farm/servers/ -> ../../../bin/consult.js (won't exist, it's .ts in src)
792
- // But bin/ is at packages/codev/bin/consult.js, so it should still work
793
- // Fall back to npx consult (works if @cluesmith/codev is installed)
794
- return 'npx consult';
795
- }
796
- /**
797
- * Generate AI summary via consult CLI
798
- */
799
- async function generateAISummary(data) {
800
- // Build prompt with commit messages and file names only (security: no full diffs)
801
- const hours = Math.floor(data.timeTracking.activeMinutes / 60);
802
- const mins = data.timeTracking.activeMinutes % 60;
803
- const prompt = `Summarize this developer's activity today for a standup report.
804
-
805
- Commits (${data.commits.length}):
806
- ${data.commits.slice(0, 20).map(c => `- ${c.message}`).join('\n') || '(none)'}
807
- ${data.commits.length > 20 ? `... and ${data.commits.length - 20} more` : ''}
808
-
809
- PRs: ${data.prs.map(p => `#${p.number} ${p.title} (${p.state})`).join(', ') || 'None'}
810
-
811
- Files modified: ${data.files.length} files
812
- ${data.files.slice(0, 10).join(', ')}${data.files.length > 10 ? ` ... and ${data.files.length - 10} more` : ''}
813
-
814
- Project status changes:
815
- ${data.projectChanges.map(p => `- ${p.id} ${p.title}: ${p.oldStatus} → ${p.newStatus}`).join('\n') || '(none)'}
816
-
817
- Active time: ~${hours}h ${mins}m
818
-
819
- Write a brief, professional summary (2-3 sentences) focusing on accomplishments. Be concise and suitable for a standup or status report.`;
820
- try {
821
- // Use consult CLI to generate summary
822
- const consultCmd = findConsultPath();
823
- const safePrompt = escapeShellArg(prompt);
824
- // Use async exec with timeout
825
- const { stdout } = await execAsync(`${consultCmd} --model gemini general ${safePrompt}`, { timeout: 60000, maxBuffer: 1024 * 1024 });
826
- return stdout.trim();
827
- }
828
- catch (err) {
829
- console.error('AI summary generation failed:', err.message);
830
- return '';
831
- }
832
- }
833
- /**
834
- * Collect all activity data for today
835
- */
836
- async function collectActivitySummary(projectRoot) {
837
- // Collect data from all sources in parallel - these are now truly async
838
- const [commits, files, prs, builders, projectChanges] = await Promise.all([
839
- getGitCommits(projectRoot),
840
- getModifiedFiles(projectRoot),
841
- getGitHubPRs(projectRoot),
842
- Promise.resolve(getBuilderActivity()), // This one is sync (reads from state)
843
- getProjectChanges(projectRoot),
844
- ]);
845
- const timeTracking = calculateTimeTracking(commits, builders);
846
- // Generate AI summary (skip if no activity)
847
- let aiSummary = '';
848
- if (commits.length > 0 || prs.length > 0) {
849
- aiSummary = await generateAISummary({
850
- commits,
851
- prs,
852
- files,
853
- timeTracking,
854
- projectChanges,
855
- });
856
- }
857
- return {
858
- commits,
859
- prs,
860
- builders,
861
- projectChanges,
862
- files,
863
- timeTracking,
864
- aiSummary: aiSummary || undefined,
865
- };
866
- }
867
- // Insecure remote mode - set when bindHost is 0.0.0.0
868
- const insecureRemoteMode = bindHost === '0.0.0.0';
869
- // ============================================================
870
- // Terminal Proxy (Spec 0062 - Secure Remote Access)
871
- // ============================================================
872
- // Create http-proxy instance for terminal proxying
873
- const terminalProxy = httpProxy.createProxyServer({ ws: true });
874
- // Handle proxy errors gracefully
875
- terminalProxy.on('error', (err, req, res) => {
876
- console.error('Terminal proxy error:', err.message);
877
- if (res && 'writeHead' in res && !res.headersSent) {
878
- res.writeHead(502, { 'Content-Type': 'application/json' });
879
- res.end(JSON.stringify({ error: 'Terminal unavailable' }));
880
- }
881
- });
882
- // getPortForTerminal is imported from utils/terminal-ports.ts (Spec 0062)
883
- // Security: Validate request origin (uses base from server-utils with insecureRemoteMode override)
884
- function isRequestAllowed(req) {
885
- // Skip all security checks in insecure remote mode
886
- if (insecureRemoteMode) {
887
- return true;
888
- }
889
- return isRequestAllowedBase(req);
890
- }
891
- // Create server
892
- const server = http.createServer(async (req, res) => {
893
- // Security: Validate Host and Origin headers
894
- if (!isRequestAllowed(req)) {
895
- res.writeHead(403, { 'Content-Type': 'text/plain' });
896
- res.end('Forbidden');
897
- return;
898
- }
899
- // CORS headers
900
- const origin = req.headers.origin;
901
- if (insecureRemoteMode) {
902
- // Allow any origin in insecure remote mode
903
- res.setHeader('Access-Control-Allow-Origin', origin || '*');
904
- }
905
- else if (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:'))) {
906
- res.setHeader('Access-Control-Allow-Origin', origin);
907
- }
908
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
909
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
910
- // Prevent caching of API responses
911
- res.setHeader('Cache-Control', 'no-store');
912
- if (req.method === 'OPTIONS') {
913
- res.writeHead(200);
914
- res.end();
915
- return;
916
- }
917
- const url = new URL(req.url || '/', `http://localhost:${port}`);
918
- try {
919
- // API: Get state
920
- if (req.method === 'GET' && url.pathname === '/api/state') {
921
- const state = loadStateWithCleanup();
922
- res.writeHead(200, { 'Content-Type': 'application/json' });
923
- res.end(JSON.stringify(state));
924
- return;
925
- }
926
- // API: Create file tab (annotation)
927
- if (req.method === 'POST' && url.pathname === '/api/tabs/file') {
928
- const body = await parseJsonBody(req);
929
- const filePath = body.path;
930
- if (!filePath) {
931
- res.writeHead(400, { 'Content-Type': 'text/plain' });
932
- res.end('Missing path');
933
- return;
934
- }
935
- // Validate path is within project root (prevent path traversal)
936
- const fullPath = validatePathWithinProject(filePath);
937
- if (!fullPath) {
938
- res.writeHead(403, { 'Content-Type': 'text/plain' });
939
- res.end('Path must be within project directory');
940
- return;
941
- }
942
- // Check file exists
943
- if (!fs.existsSync(fullPath)) {
944
- res.writeHead(404, { 'Content-Type': 'text/plain' });
945
- res.end(`File not found: ${filePath}`);
946
- return;
947
- }
948
- // Check if already open
949
- const annotations = getAnnotations();
950
- const existing = annotations.find((a) => a.file === fullPath);
951
- if (existing) {
952
- // Verify the process is still running
953
- if (isProcessRunning(existing.pid)) {
954
- res.writeHead(200, { 'Content-Type': 'application/json' });
955
- res.end(JSON.stringify({ id: existing.id, port: existing.port, existing: true }));
956
- return;
957
- }
958
- // Process is dead - clean up stale entry and spawn new one
959
- console.log(`Cleaning up stale annotation for ${fullPath} (pid ${existing.pid} dead)`);
960
- removeAnnotation(existing.id);
961
- }
962
- // DoS protection: check tab limit
963
- const state = loadState();
964
- if (countTotalTabs(state) >= CONFIG.maxTabs) {
965
- res.writeHead(429, { 'Content-Type': 'text/plain' });
966
- res.end(`Tab limit reached (max ${CONFIG.maxTabs}). Close some tabs first.`);
967
- return;
968
- }
969
- // Find available port (pass state to avoid already-allocated ports)
970
- const openPort = await findAvailablePort(CONFIG.openPortStart, state);
971
- // Start open server
972
- const { script: serverScript, useTsx } = getOpenServerPath();
973
- if (!fs.existsSync(serverScript)) {
974
- res.writeHead(500, { 'Content-Type': 'text/plain' });
975
- res.end('Open server not found');
976
- return;
977
- }
978
- // Use tsx for TypeScript files, node for compiled JavaScript
979
- const cmd = useTsx ? 'npx' : 'node';
980
- const args = useTsx
981
- ? ['tsx', serverScript, String(openPort), fullPath]
982
- : [serverScript, String(openPort), fullPath];
983
- const pid = spawnDetached(cmd, args, projectRoot);
984
- if (!pid) {
985
- res.writeHead(500, { 'Content-Type': 'text/plain' });
986
- res.end('Failed to start open server');
987
- return;
988
- }
989
- // Wait for open server to be ready (accepting connections)
990
- const serverReady = await waitForPortReady(openPort, 5000);
991
- if (!serverReady) {
992
- // Server didn't start in time - kill it and report error
993
- try {
994
- process.kill(pid);
995
- }
996
- catch {
997
- // Process may have already died
998
- }
999
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1000
- res.end('Open server failed to start (timeout)');
1001
- return;
1002
- }
1003
- // Create annotation record
1004
- const annotation = {
1005
- id: generateId('A'),
1006
- file: fullPath,
1007
- port: openPort,
1008
- pid,
1009
- parent: { type: 'architect' },
1010
- };
1011
- addAnnotation(annotation);
1012
- res.writeHead(201, { 'Content-Type': 'application/json' });
1013
- res.end(JSON.stringify({ id: annotation.id, port: openPort }));
1014
- return;
1015
- }
1016
- // API: Create builder tab (spawns worktree builder with random ID)
1017
- if (req.method === 'POST' && url.pathname === '/api/tabs/builder') {
1018
- const builderState = loadState();
1019
- // DoS protection: check tab limit
1020
- if (countTotalTabs(builderState) >= CONFIG.maxTabs) {
1021
- res.writeHead(429, { 'Content-Type': 'text/plain' });
1022
- res.end(`Tab limit reached (max ${CONFIG.maxTabs}). Close some tabs first.`);
1023
- return;
1024
- }
1025
- // Find available port for builder
1026
- const builderPort = await findAvailablePort(CONFIG.builderPortStart, builderState);
1027
- // Spawn worktree builder
1028
- const result = spawnWorktreeBuilder(builderPort, builderState);
1029
- if (!result) {
1030
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1031
- res.end('Failed to spawn worktree builder');
1032
- return;
1033
- }
1034
- // Wait for ttyd to be ready
1035
- await new Promise((resolve) => setTimeout(resolve, 500));
1036
- // Save builder to state
1037
- upsertBuilder(result.builder);
1038
- res.writeHead(201, { 'Content-Type': 'application/json' });
1039
- res.end(JSON.stringify({ id: result.builder.id, port: result.builder.port, name: result.builder.name }));
1040
- return;
1041
- }
1042
- // API: Create shell tab (supports worktree parameter for Spec 0057)
1043
- if (req.method === 'POST' && url.pathname === '/api/tabs/shell') {
1044
- const body = await parseJsonBody(req);
1045
- const name = body.name || undefined;
1046
- const command = body.command || undefined;
1047
- const worktree = body.worktree === true;
1048
- const branch = body.branch || undefined;
1049
- // Validate name if provided (prevent command injection)
1050
- if (name && !/^[a-zA-Z0-9_-]+$/.test(name)) {
1051
- res.writeHead(400, { 'Content-Type': 'text/plain' });
1052
- res.end('Invalid name format');
1053
- return;
1054
- }
1055
- // Validate branch name if provided (prevent command injection)
1056
- // Allow: letters, numbers, underscores, hyphens, slashes, dots
1057
- // Reject: control chars, spaces, .., @{, trailing/leading slashes
1058
- if (branch) {
1059
- const invalidPatterns = [
1060
- /[\x00-\x1f\x7f]/, // Control characters
1061
- /\s/, // Whitespace
1062
- /\.\./, // Parent directory traversal
1063
- /@\{/, // Git reflog syntax
1064
- /^\//, // Leading slash
1065
- /\/$/, // Trailing slash
1066
- /\/\//, // Double slash
1067
- /^-/, // Leading hyphen (could be flag)
1068
- ];
1069
- const isInvalid = invalidPatterns.some(p => p.test(branch));
1070
- if (isInvalid) {
1071
- res.writeHead(200, { 'Content-Type': 'application/json' });
1072
- res.end(JSON.stringify({
1073
- success: false,
1074
- error: 'Invalid branch name. Avoid spaces, control characters, .., @{, and leading/trailing slashes.'
1075
- }));
1076
- return;
1077
- }
1078
- }
1079
- const shellState = loadState();
1080
- // DoS protection: check tab limit
1081
- if (countTotalTabs(shellState) >= CONFIG.maxTabs) {
1082
- res.writeHead(429, { 'Content-Type': 'text/plain' });
1083
- res.end(`Tab limit reached (max ${CONFIG.maxTabs}). Close some tabs first.`);
1084
- return;
1085
- }
1086
- // Determine working directory (project root or worktree)
1087
- let cwd = projectRoot;
1088
- let worktreePath;
1089
- if (worktree) {
1090
- // Create worktree for the shell
1091
- const worktreesDir = path.join(projectRoot, '.worktrees');
1092
- if (!fs.existsSync(worktreesDir)) {
1093
- fs.mkdirSync(worktreesDir, { recursive: true });
1094
- }
1095
- // Generate worktree name
1096
- const worktreeName = branch || `temp-${Date.now()}`;
1097
- worktreePath = path.join(worktreesDir, worktreeName);
1098
- // Check if worktree already exists
1099
- if (fs.existsSync(worktreePath)) {
1100
- res.writeHead(200, { 'Content-Type': 'application/json' });
1101
- res.end(JSON.stringify({
1102
- success: false,
1103
- error: `Worktree '${worktreeName}' already exists at ${worktreePath}`
1104
- }));
1105
- return;
1106
- }
1107
- // Create worktree
1108
- try {
1109
- let gitCmd;
1110
- if (branch) {
1111
- // Check if branch already exists
1112
- let branchExists = false;
1113
- try {
1114
- execSync(`git rev-parse --verify "${branch}"`, { cwd: projectRoot, stdio: 'pipe' });
1115
- branchExists = true;
1116
- }
1117
- catch {
1118
- // Branch doesn't exist
1119
- }
1120
- if (branchExists) {
1121
- // Checkout existing branch into worktree
1122
- gitCmd = `git worktree add "${worktreePath}" "${branch}"`;
1123
- }
1124
- else {
1125
- // Create new branch and worktree
1126
- gitCmd = `git worktree add "${worktreePath}" -b "${branch}"`;
1127
- }
1128
- }
1129
- else {
1130
- // Detached HEAD worktree
1131
- gitCmd = `git worktree add "${worktreePath}" --detach`;
1132
- }
1133
- execSync(gitCmd, { cwd: projectRoot, stdio: 'pipe' });
1134
- // Symlink .env from project root into worktree (if it exists)
1135
- const rootEnvPath = path.join(projectRoot, '.env');
1136
- const worktreeEnvPath = path.join(worktreePath, '.env');
1137
- if (fs.existsSync(rootEnvPath) && !fs.existsSync(worktreeEnvPath)) {
1138
- try {
1139
- fs.symlinkSync(rootEnvPath, worktreeEnvPath);
1140
- }
1141
- catch {
1142
- // Non-fatal: continue without .env symlink
1143
- }
1144
- }
1145
- cwd = worktreePath;
1146
- }
1147
- catch (gitError) {
1148
- const errorMsg = gitError instanceof Error
1149
- ? gitError.stderr?.toString() || gitError.message
1150
- : 'Unknown error';
1151
- res.writeHead(200, { 'Content-Type': 'application/json' });
1152
- res.end(JSON.stringify({
1153
- success: false,
1154
- error: `Git worktree creation failed: ${errorMsg}`
1155
- }));
1156
- return;
1157
- }
1158
- }
1159
- // Generate ID and name
1160
- const id = generateId('U');
1161
- const utilName = name || (worktree ? `worktree-${shellState.utils.length + 1}` : `shell-${shellState.utils.length + 1}`);
1162
- const sessionName = `af-shell-${id}`;
1163
- // Get shell command - if command provided, run it then keep shell open
1164
- const shell = process.env.SHELL || '/bin/bash';
1165
- const shellCommand = command
1166
- ? `${shell} -c '${command.replace(/'/g, "'\\''")}; exec ${shell}'`
1167
- : shell;
1168
- // Retry loop for concurrent port allocation race conditions
1169
- const MAX_PORT_RETRIES = 5;
1170
- let utilPort = null;
1171
- let pid = null;
1172
- for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
1173
- // Get fresh state on each attempt to see newly allocated ports
1174
- const currentState = loadState();
1175
- const candidatePort = await findAvailablePort(CONFIG.utilPortStart, currentState);
1176
- // Start tmux session with ttyd attached (use cwd which may be worktree)
1177
- const spawnedPid = spawnTmuxWithTtyd(sessionName, shellCommand, candidatePort, cwd);
1178
- if (!spawnedPid) {
1179
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1180
- res.end('Failed to start shell');
1181
- return;
1182
- }
1183
- // Wait for ttyd to be ready
1184
- await new Promise((resolve) => setTimeout(resolve, 500));
1185
- // Try to add util record - may fail if port was taken by concurrent request
1186
- const util = {
1187
- id,
1188
- name: utilName,
1189
- port: candidatePort,
1190
- pid: spawnedPid,
1191
- tmuxSession: sessionName,
1192
- worktreePath: worktreePath, // Track for cleanup on tab close
1193
- };
1194
- if (tryAddUtil(util)) {
1195
- // Success - port reserved
1196
- utilPort = candidatePort;
1197
- pid = spawnedPid;
1198
- break;
1199
- }
1200
- // Port conflict - kill the spawned process and retry
1201
- console.log(`[info] Port ${candidatePort} conflict, retrying (attempt ${attempt + 1}/${MAX_PORT_RETRIES})`);
1202
- await killProcessGracefully(spawnedPid);
1203
- }
1204
- if (utilPort === null || pid === null) {
1205
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1206
- res.end('Failed to allocate port after multiple retries');
1207
- return;
1208
- }
1209
- res.writeHead(201, { 'Content-Type': 'application/json' });
1210
- res.end(JSON.stringify({ success: true, id, port: utilPort, name: utilName }));
1211
- return;
1212
- }
1213
- // API: Check if tab process is running (Bugfix #132)
1214
- if (req.method === 'GET' && url.pathname.match(/^\/api\/tabs\/[^/]+\/running$/)) {
1215
- const match = url.pathname.match(/^\/api\/tabs\/([^/]+)\/running$/);
1216
- if (!match) {
1217
- res.writeHead(400, { 'Content-Type': 'text/plain' });
1218
- res.end('Invalid tab ID');
1219
- return;
1220
- }
1221
- const tabId = decodeURIComponent(match[1]);
1222
- let running = false;
1223
- let found = false;
1224
- // Check if it's a shell tab
1225
- if (tabId.startsWith('shell-')) {
1226
- const utilId = tabId.replace('shell-', '');
1227
- const tabUtils = getUtils();
1228
- const util = tabUtils.find((u) => u.id === utilId);
1229
- if (util) {
1230
- found = true;
1231
- running = isProcessRunning(util.pid);
1232
- }
1233
- }
1234
- // Check if it's a builder tab
1235
- if (tabId.startsWith('builder-')) {
1236
- const builderId = tabId.replace('builder-', '');
1237
- const builder = getBuilder(builderId);
1238
- if (builder) {
1239
- found = true;
1240
- running = isProcessRunning(builder.pid);
1241
- }
1242
- }
1243
- if (found) {
1244
- res.writeHead(200, { 'Content-Type': 'application/json' });
1245
- res.end(JSON.stringify({ running }));
1246
- }
1247
- else {
1248
- res.writeHead(404, { 'Content-Type': 'application/json' });
1249
- res.end(JSON.stringify({ running: false }));
1250
- }
1251
- return;
1252
- }
1253
- // API: Close tab
1254
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/tabs/')) {
1255
- const tabId = decodeURIComponent(url.pathname.replace('/api/tabs/', ''));
1256
- let found = false;
1257
- // Check if it's a file tab
1258
- if (tabId.startsWith('file-')) {
1259
- const annotationId = tabId.replace('file-', '');
1260
- const tabAnnotations = getAnnotations();
1261
- const annotation = tabAnnotations.find((a) => a.id === annotationId);
1262
- if (annotation) {
1263
- await killProcessGracefully(annotation.pid);
1264
- removeAnnotation(annotationId);
1265
- found = true;
1266
- }
1267
- }
1268
- // Check if it's a builder tab
1269
- if (tabId.startsWith('builder-')) {
1270
- const builderId = tabId.replace('builder-', '');
1271
- const builder = getBuilder(builderId);
1272
- if (builder) {
1273
- await killProcessGracefully(builder.pid);
1274
- removeBuilder(builderId);
1275
- found = true;
1276
- }
1277
- }
1278
- // Check if it's a shell tab
1279
- if (tabId.startsWith('shell-')) {
1280
- const utilId = tabId.replace('shell-', '');
1281
- const tabUtils = getUtils();
1282
- const util = tabUtils.find((u) => u.id === utilId);
1283
- if (util) {
1284
- await killProcessGracefully(util.pid, util.tmuxSession);
1285
- // Note: worktrees are NOT cleaned up on tab close - they may contain useful context
1286
- // Users can manually clean up with `git worktree list` and `git worktree remove`
1287
- removeUtil(utilId);
1288
- found = true;
1289
- }
1290
- }
1291
- if (found) {
1292
- res.writeHead(200, { 'Content-Type': 'application/json' });
1293
- res.end(JSON.stringify({ success: true }));
1294
- }
1295
- else {
1296
- res.writeHead(404, { 'Content-Type': 'text/plain' });
1297
- res.end('Tab not found');
1298
- }
1299
- return;
1300
- }
1301
- // API: Stop all
1302
- if (req.method === 'POST' && url.pathname === '/api/stop') {
1303
- const stopState = loadState();
1304
- // Kill all tmux sessions first
1305
- for (const util of stopState.utils) {
1306
- if (util.tmuxSession) {
1307
- killTmuxSession(util.tmuxSession);
1308
- }
1309
- }
1310
- if (stopState.architect?.tmuxSession) {
1311
- killTmuxSession(stopState.architect.tmuxSession);
1312
- }
1313
- // Kill all processes gracefully
1314
- const pids = [];
1315
- if (stopState.architect) {
1316
- pids.push(stopState.architect.pid);
1317
- }
1318
- for (const builder of stopState.builders) {
1319
- pids.push(builder.pid);
1320
- }
1321
- for (const util of stopState.utils) {
1322
- pids.push(util.pid);
1323
- }
1324
- for (const annotation of stopState.annotations) {
1325
- pids.push(annotation.pid);
1326
- }
1327
- // Kill all processes in parallel
1328
- await Promise.all(pids.map((pid) => killProcessGracefully(pid)));
1329
- // Clear state
1330
- clearState();
1331
- res.writeHead(200, { 'Content-Type': 'application/json' });
1332
- res.end(JSON.stringify({ success: true, killed: pids.length }));
1333
- // Exit after a short delay
1334
- setTimeout(() => process.exit(0), 500);
1335
- return;
1336
- }
1337
- // Open file route - handles file clicks from terminal
1338
- // Returns a small HTML page that messages the dashboard via BroadcastChannel
1339
- if (req.method === 'GET' && url.pathname === '/open-file') {
1340
- const filePath = url.searchParams.get('path');
1341
- const line = url.searchParams.get('line');
1342
- const sourcePort = url.searchParams.get('sourcePort');
1343
- if (!filePath) {
1344
- res.writeHead(400, { 'Content-Type': 'text/plain' });
1345
- res.end('Missing path parameter');
1346
- return;
1347
- }
1348
- // Determine base path for relative path resolution
1349
- // If sourcePort is provided, look up the builder/util to get its worktree
1350
- let basePath = projectRoot;
1351
- if (sourcePort) {
1352
- const portNum = parseInt(sourcePort, 10);
1353
- const builders = getBuilders();
1354
- // Check if it's a builder terminal
1355
- const builder = builders.find((b) => b.port === portNum);
1356
- if (builder && builder.worktree) {
1357
- basePath = builder.worktree;
1358
- }
1359
- // Check if it's a utility terminal (they run in project root, so no change needed)
1360
- // Architect terminal also runs in project root
1361
- }
1362
- // Validate path is within project (or builder worktree)
1363
- // For relative paths, resolve against the determined base path
1364
- let fullPath;
1365
- if (filePath.startsWith('/')) {
1366
- // Absolute path - validate against project root
1367
- fullPath = validatePathWithinProject(filePath);
1368
- }
1369
- else {
1370
- // Relative path - resolve against base path, then validate
1371
- const resolvedPath = path.resolve(basePath, filePath);
1372
- // For builder worktrees, the path is within project root (worktrees are under .builders/)
1373
- fullPath = validatePathWithinProject(resolvedPath);
1374
- }
1375
- if (!fullPath) {
1376
- res.writeHead(403, { 'Content-Type': 'text/plain' });
1377
- res.end('Path must be within project directory');
1378
- return;
1379
- }
1380
- // Check file exists
1381
- if (!fs.existsSync(fullPath)) {
1382
- res.writeHead(404, { 'Content-Type': 'text/plain' });
1383
- res.end(`File not found: ${filePath}`);
1384
- return;
1385
- }
1386
- // HTML-escape the file path for safe display (uses imported escapeHtml from server-utils.js)
1387
- const safeFilePath = escapeHtml(filePath);
1388
- const safeLineDisplay = line ? ':' + escapeHtml(line) : '';
1389
- // Serve a small HTML page that communicates back to dashboard
1390
- // Note: We only use BroadcastChannel, not API call (dashboard handles tab creation)
1391
- const html = `<!DOCTYPE html>
1392
- <html>
1393
- <head>
1394
- <title>Opening file...</title>
1395
- <style>
1396
- body {
1397
- font-family: system-ui;
1398
- background: #1a1a1a;
1399
- color: #ccc;
1400
- display: flex;
1401
- align-items: center;
1402
- justify-content: center;
1403
- height: 100vh;
1404
- margin: 0;
1405
- }
1406
- .message { text-align: center; }
1407
- .path { color: #3b82f6; font-family: monospace; margin: 8px 0; }
1408
- </style>
1409
- </head>
1410
- <body>
1411
- <div class="message">
1412
- <p>Opening file...</p>
1413
- <p class="path">${safeFilePath}${safeLineDisplay}</p>
1414
- </div>
1415
- <script>
1416
- (async function() {
1417
- const path = ${JSON.stringify(fullPath)};
1418
- const line = ${line ? parseInt(line, 10) : 'null'};
1419
-
1420
- // Use BroadcastChannel to message the dashboard
1421
- // Dashboard will handle opening the file tab
1422
- const channel = new BroadcastChannel('agent-farm');
1423
- channel.postMessage({
1424
- type: 'openFile',
1425
- path: path,
1426
- line: line
1427
- });
1428
-
1429
- // Close this window/tab after a short delay
1430
- setTimeout(() => {
1431
- window.close();
1432
- // If window.close() doesn't work (wasn't opened by script),
1433
- // show success message
1434
- document.body.innerHTML = '<div class="message"><p>File opened in dashboard</p><p class="path">You can close this tab</p></div>';
1435
- }, 500);
1436
- })();
1437
- </script>
1438
- </body>
1439
- </html>`;
1440
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1441
- res.end(html);
1442
- return;
1443
- }
1444
- // API: Check if projectlist.md exists (for starter page polling)
1445
- if (req.method === 'GET' && url.pathname === '/api/projectlist-exists') {
1446
- const projectlistPath = path.join(projectRoot, 'codev/projectlist.md');
1447
- const exists = fs.existsSync(projectlistPath);
1448
- res.writeHead(200, { 'Content-Type': 'application/json' });
1449
- res.end(JSON.stringify({ exists }));
1450
- return;
1451
- }
1452
- // Read file contents (for Projects tab to read projectlist.md)
1453
- if (req.method === 'GET' && url.pathname === '/file') {
1454
- const filePath = url.searchParams.get('path');
1455
- if (!filePath) {
1456
- res.writeHead(400, { 'Content-Type': 'text/plain' });
1457
- res.end('Missing path parameter');
1458
- return;
1459
- }
1460
- // Validate path is within project root (prevent path traversal)
1461
- const fullPath = validatePathWithinProject(filePath);
1462
- if (!fullPath) {
1463
- res.writeHead(403, { 'Content-Type': 'text/plain' });
1464
- res.end('Path must be within project directory');
1465
- return;
1466
- }
1467
- // Check file exists
1468
- if (!fs.existsSync(fullPath)) {
1469
- res.writeHead(404, { 'Content-Type': 'text/plain' });
1470
- res.end(`File not found: ${filePath}`);
1471
- return;
1472
- }
1473
- // Read and return file contents
1474
- try {
1475
- const content = fs.readFileSync(fullPath, 'utf-8');
1476
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
1477
- res.end(content);
1478
- }
1479
- catch (err) {
1480
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1481
- res.end('Error reading file: ' + err.message);
1482
- }
1483
- return;
1484
- }
1485
- // API: Get directory tree for file browser (Spec 0055)
1486
- if (req.method === 'GET' && url.pathname === '/api/files') {
1487
- // Directories to exclude from the tree
1488
- const EXCLUDED_DIRS = new Set([
1489
- 'node_modules',
1490
- '.git',
1491
- 'dist',
1492
- '__pycache__',
1493
- '.next',
1494
- '.nuxt',
1495
- '.turbo',
1496
- 'coverage',
1497
- '.nyc_output',
1498
- '.cache',
1499
- '.parcel-cache',
1500
- 'build',
1501
- '.svelte-kit',
1502
- 'vendor',
1503
- '.venv',
1504
- 'venv',
1505
- 'env',
1506
- ]);
1507
- // Recursively build directory tree
1508
- function buildTree(dirPath, relativePath = '') {
1509
- const entries = [];
1510
- try {
1511
- const items = fs.readdirSync(dirPath, { withFileTypes: true });
1512
- for (const item of items) {
1513
- // Skip excluded directories only (allow dotfiles like .github, .eslintrc, etc.)
1514
- if (EXCLUDED_DIRS.has(item.name))
1515
- continue;
1516
- const itemRelPath = relativePath ? `${relativePath}/${item.name}` : item.name;
1517
- const itemFullPath = path.join(dirPath, item.name);
1518
- if (item.isDirectory()) {
1519
- const children = buildTree(itemFullPath, itemRelPath);
1520
- entries.push({
1521
- name: item.name,
1522
- path: itemRelPath,
1523
- type: 'dir',
1524
- children,
1525
- });
1526
- }
1527
- else if (item.isFile()) {
1528
- entries.push({
1529
- name: item.name,
1530
- path: itemRelPath,
1531
- type: 'file',
1532
- });
1533
- }
1534
- }
1535
- }
1536
- catch (err) {
1537
- // Ignore permission errors or inaccessible directories
1538
- console.error(`Error reading directory ${dirPath}:`, err.message);
1539
- }
1540
- // Sort: directories first, then files, alphabetically within each group
1541
- entries.sort((a, b) => {
1542
- if (a.type === 'dir' && b.type === 'file')
1543
- return -1;
1544
- if (a.type === 'file' && b.type === 'dir')
1545
- return 1;
1546
- return a.name.localeCompare(b.name);
1547
- });
1548
- return entries;
1549
- }
1550
- const tree = buildTree(projectRoot);
1551
- res.writeHead(200, { 'Content-Type': 'application/json' });
1552
- res.end(JSON.stringify(tree));
1553
- return;
1554
- }
1555
- // API: Get hash of file tree for change detection (auto-refresh)
1556
- if (req.method === 'GET' && url.pathname === '/api/files/hash') {
1557
- // Build a lightweight hash based on directory mtimes
1558
- // This is faster than building the full tree
1559
- function getTreeHash(dirPath) {
1560
- const EXCLUDED_DIRS = new Set([
1561
- 'node_modules', '.git', 'dist', '__pycache__', '.next',
1562
- '.nuxt', '.turbo', 'coverage', '.nyc_output', '.cache',
1563
- '.parcel-cache', 'build', '.svelte-kit', 'vendor', '.venv', 'venv', 'env',
1564
- ]);
1565
- let hash = '';
1566
- function walk(dir) {
1567
- try {
1568
- const stat = fs.statSync(dir);
1569
- hash += `${dir}:${stat.mtimeMs};`;
1570
- const items = fs.readdirSync(dir, { withFileTypes: true });
1571
- for (const item of items) {
1572
- if (EXCLUDED_DIRS.has(item.name))
1573
- continue;
1574
- if (item.isDirectory()) {
1575
- walk(path.join(dir, item.name));
1576
- }
1577
- else if (item.isFile()) {
1578
- // Include file mtime for change detection
1579
- const fileStat = fs.statSync(path.join(dir, item.name));
1580
- hash += `${item.name}:${fileStat.mtimeMs};`;
1581
- }
1582
- }
1583
- }
1584
- catch {
1585
- // Ignore errors
1586
- }
1587
- }
1588
- walk(dirPath);
1589
- // Simple hash: sum of char codes
1590
- let sum = 0;
1591
- for (let i = 0; i < hash.length; i++) {
1592
- sum = ((sum << 5) - sum + hash.charCodeAt(i)) | 0;
1593
- }
1594
- return sum.toString(16);
1595
- }
1596
- const hash = getTreeHash(projectRoot);
1597
- res.writeHead(200, { 'Content-Type': 'application/json' });
1598
- res.end(JSON.stringify({ hash }));
1599
- return;
1600
- }
1601
- // API: Create a new file (Bugfix #131)
1602
- if (req.method === 'POST' && url.pathname === '/api/files') {
1603
- const body = await parseJsonBody(req);
1604
- const filePath = body.path;
1605
- const content = body.content || '';
1606
- if (!filePath) {
1607
- res.writeHead(400, { 'Content-Type': 'application/json' });
1608
- res.end(JSON.stringify({ error: 'Missing path' }));
1609
- return;
1610
- }
1611
- // Validate path is within project root (prevent path traversal)
1612
- const fullPath = validatePathWithinProject(filePath);
1613
- if (!fullPath) {
1614
- res.writeHead(403, { 'Content-Type': 'application/json' });
1615
- res.end(JSON.stringify({ error: 'Path must be within project directory' }));
1616
- return;
1617
- }
1618
- // Check if file already exists
1619
- if (fs.existsSync(fullPath)) {
1620
- res.writeHead(409, { 'Content-Type': 'application/json' });
1621
- res.end(JSON.stringify({ error: 'File already exists' }));
1622
- return;
1623
- }
1624
- // Additional security: validate parent directories don't symlink outside project
1625
- // Find the deepest existing parent and ensure it's within project
1626
- let checkDir = path.dirname(fullPath);
1627
- while (checkDir !== projectRoot && !fs.existsSync(checkDir)) {
1628
- checkDir = path.dirname(checkDir);
1629
- }
1630
- if (fs.existsSync(checkDir) && checkDir !== projectRoot) {
1631
- try {
1632
- const realParent = fs.realpathSync(checkDir);
1633
- if (!realParent.startsWith(projectRoot + path.sep) && realParent !== projectRoot) {
1634
- res.writeHead(403, { 'Content-Type': 'application/json' });
1635
- res.end(JSON.stringify({ error: 'Path must be within project directory' }));
1636
- return;
1637
- }
1638
- }
1639
- catch {
1640
- res.writeHead(403, { 'Content-Type': 'application/json' });
1641
- res.end(JSON.stringify({ error: 'Cannot resolve path' }));
1642
- return;
1643
- }
1644
- }
1645
- try {
1646
- // Create parent directories if they don't exist
1647
- const parentDir = path.dirname(fullPath);
1648
- if (!fs.existsSync(parentDir)) {
1649
- fs.mkdirSync(parentDir, { recursive: true });
1650
- }
1651
- // Write the file
1652
- fs.writeFileSync(fullPath, content, 'utf-8');
1653
- res.writeHead(201, { 'Content-Type': 'application/json' });
1654
- res.end(JSON.stringify({ success: true, path: filePath }));
1655
- }
1656
- catch (err) {
1657
- console.error('Error creating file:', err.message);
1658
- res.writeHead(500, { 'Content-Type': 'application/json' });
1659
- res.end(JSON.stringify({ error: 'Failed to create file: ' + err.message }));
1660
- }
1661
- return;
1662
- }
1663
- // API: Hot reload check (Spec 0060)
1664
- // Returns modification times for all dashboard CSS/JS files
1665
- if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
1666
- try {
1667
- const dashboardDir = path.join(__dirname, '../../../templates/dashboard');
1668
- const cssDir = path.join(dashboardDir, 'css');
1669
- const jsDir = path.join(dashboardDir, 'js');
1670
- const mtimes = {};
1671
- // Collect CSS file modification times
1672
- if (fs.existsSync(cssDir)) {
1673
- for (const file of fs.readdirSync(cssDir)) {
1674
- if (file.endsWith('.css')) {
1675
- const stat = fs.statSync(path.join(cssDir, file));
1676
- mtimes[`css/${file}`] = stat.mtimeMs;
1677
- }
1678
- }
1679
- }
1680
- // Collect JS file modification times
1681
- if (fs.existsSync(jsDir)) {
1682
- for (const file of fs.readdirSync(jsDir)) {
1683
- if (file.endsWith('.js')) {
1684
- const stat = fs.statSync(path.join(jsDir, file));
1685
- mtimes[`js/${file}`] = stat.mtimeMs;
1686
- }
1687
- }
1688
- }
1689
- res.writeHead(200, { 'Content-Type': 'application/json' });
1690
- res.end(JSON.stringify({ mtimes }));
1691
- }
1692
- catch (err) {
1693
- console.error('Hot reload check error:', err);
1694
- res.writeHead(500, { 'Content-Type': 'application/json' });
1695
- res.end(JSON.stringify({ error: err.message }));
1696
- }
1697
- return;
1698
- }
1699
- // Serve dashboard CSS files
1700
- if (req.method === 'GET' && url.pathname.startsWith('/dashboard/css/')) {
1701
- const filename = url.pathname.replace('/dashboard/css/', '');
1702
- // Validate filename to prevent path traversal
1703
- if (!filename || filename.includes('..') || filename.includes('/') || !filename.endsWith('.css')) {
1704
- res.writeHead(400, { 'Content-Type': 'text/plain' });
1705
- res.end('Invalid filename');
1706
- return;
1707
- }
1708
- const cssPath = path.join(__dirname, '../../../templates/dashboard/css', filename);
1709
- if (fs.existsSync(cssPath)) {
1710
- const content = fs.readFileSync(cssPath, 'utf-8');
1711
- res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
1712
- res.end(content);
1713
- return;
1714
- }
1715
- res.writeHead(404, { 'Content-Type': 'text/plain' });
1716
- res.end('CSS file not found');
1717
- return;
1718
- }
1719
- // Serve dashboard JS files
1720
- if (req.method === 'GET' && url.pathname.startsWith('/dashboard/js/')) {
1721
- const filename = url.pathname.replace('/dashboard/js/', '');
1722
- // Validate filename to prevent path traversal
1723
- if (!filename || filename.includes('..') || filename.includes('/') || !filename.endsWith('.js')) {
1724
- res.writeHead(400, { 'Content-Type': 'text/plain' });
1725
- res.end('Invalid filename');
1726
- return;
1727
- }
1728
- const jsPath = path.join(__dirname, '../../../templates/dashboard/js', filename);
1729
- if (fs.existsSync(jsPath)) {
1730
- const content = fs.readFileSync(jsPath, 'utf-8');
1731
- res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
1732
- res.end(content);
1733
- return;
1734
- }
1735
- res.writeHead(404, { 'Content-Type': 'text/plain' });
1736
- res.end('JS file not found');
1737
- return;
1738
- }
1739
- // Terminal proxy route (Spec 0062 - Secure Remote Access)
1740
- // Routes /terminal/:id to the appropriate ttyd instance
1741
- const terminalMatch = url.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
1742
- if (terminalMatch) {
1743
- const terminalId = terminalMatch[1];
1744
- const terminalPort = getPortForTerminal(terminalId, loadState());
1745
- if (!terminalPort) {
1746
- res.writeHead(404, { 'Content-Type': 'application/json' });
1747
- res.end(JSON.stringify({ error: `Terminal not found: ${terminalId}` }));
1748
- return;
1749
- }
1750
- // Rewrite the URL to strip the /terminal/:id prefix
1751
- req.url = terminalMatch[2] || '/';
1752
- terminalProxy.web(req, res, { target: `http://localhost:${terminalPort}` });
1753
- return;
1754
- }
1755
- // Annotation proxy route (Spec 0062 - Secure Remote Access)
1756
- // Routes /annotation/:id to the appropriate open-server instance
1757
- const annotationMatch = url.pathname.match(/^\/annotation\/([^/]+)(\/.*)?$/);
1758
- if (annotationMatch) {
1759
- const annotationId = annotationMatch[1];
1760
- const annotations = getAnnotations();
1761
- const annotation = annotations.find((a) => a.id === annotationId);
1762
- if (!annotation) {
1763
- res.writeHead(404, { 'Content-Type': 'application/json' });
1764
- res.end(JSON.stringify({ error: `Annotation not found: ${annotationId}` }));
1765
- return;
1766
- }
1767
- // Rewrite the URL to strip the /annotation/:id prefix, preserving query string
1768
- const remainingPath = annotationMatch[2] || '/';
1769
- req.url = url.search ? `${remainingPath}${url.search}` : remainingPath;
1770
- terminalProxy.web(req, res, { target: `http://localhost:${annotation.port}` });
1771
- return;
1772
- }
1773
- // Serve dashboard
1774
- if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
1775
- try {
1776
- let template = fs.readFileSync(templatePath, 'utf-8');
1777
- const state = loadStateWithCleanup();
1778
- // Inject project name into template (HTML-escaped for security)
1779
- const projectName = escapeHtml(getProjectName(projectRoot));
1780
- template = template.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
1781
- // Inject state into template
1782
- const stateJson = JSON.stringify(state);
1783
- template = template.replace('// STATE_INJECTION_POINT', `window.INITIAL_STATE = ${stateJson};`);
1784
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1785
- res.end(template);
1786
- }
1787
- catch (err) {
1788
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1789
- res.end('Error loading dashboard: ' + err.message);
1790
- }
1791
- return;
1792
- }
1793
- // 404 for everything else
1794
- res.writeHead(404, { 'Content-Type': 'text/plain' });
1795
- res.end('Not found');
1796
- }
1797
- catch (err) {
1798
- console.error('Request error:', err);
1799
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1800
- res.end('Internal server error: ' + err.message);
1801
- }
1802
- });
1803
- // WebSocket upgrade handler for terminal proxy (Spec 0062)
1804
- // ttyd uses WebSocket for bidirectional terminal communication
1805
- server.on('upgrade', (req, socket, head) => {
1806
- // Security check
1807
- const host = req.headers.host;
1808
- if (!insecureRemoteMode && host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
1809
- socket.destroy();
1810
- return;
1811
- }
1812
- const reqUrl = new URL(req.url || '/', `http://localhost:${port}`);
1813
- const terminalMatch = reqUrl.pathname.match(/^\/terminal\/([^/]+)(\/.*)?$/);
1814
- if (terminalMatch) {
1815
- const terminalId = terminalMatch[1];
1816
- const terminalPort = getPortForTerminal(terminalId, loadState());
1817
- if (terminalPort) {
1818
- // Rewrite URL to strip /terminal/:id prefix
1819
- req.url = terminalMatch[2] || '/';
1820
- terminalProxy.ws(req, socket, head, { target: `http://localhost:${terminalPort}` });
1821
- }
1822
- else {
1823
- // Terminal not found - close the socket
1824
- socket.destroy();
1825
- }
1826
- }
1827
- // Non-terminal WebSocket requests are ignored (socket will time out)
1828
- });
1829
- // Handle WebSocket proxy errors separately
1830
- terminalProxy.on('error', (err, req, socket) => {
1831
- console.error('WebSocket proxy error:', err.message);
1832
- if (socket && 'destroy' in socket && typeof socket.destroy === 'function' && !socket.destroyed) {
1833
- socket.destroy();
1834
- }
1835
- });
1836
- // Handle server errors (e.g., port already in use)
1837
- server.on('error', (err) => {
1838
- if (err.code === 'EADDRINUSE') {
1839
- console.error(`Error: Port ${port} is already in use.`);
1840
- console.error(`Run 'lsof -i :${port}' to find the process, or use 'af ports cleanup' to clean up orphans.`);
1841
- process.exit(1);
1842
- }
1843
- else {
1844
- console.error(`Server error: ${err.message}`);
1845
- process.exit(1);
1846
- }
1847
- });
1848
- if (bindHost) {
1849
- server.listen(port, bindHost, () => {
1850
- console.log(`Dashboard: http://${bindHost}:${port}`);
1851
- });
1852
- }
1853
- else {
1854
- server.listen(port, () => {
1855
- console.log(`Dashboard: http://localhost:${port}`);
1856
- });
1857
- }
1858
- //# sourceMappingURL=dashboard-server.js.map