@useconductor/conductor 1.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 (504) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.eslintrc.json +23 -0
  4. package/.gitattributes +6 -0
  5. package/.github/FUNDING.yml +15 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +91 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  9. package/.github/ISSUE_TEMPLATE/plugin_request.yml +71 -0
  10. package/.github/README.md +13 -0
  11. package/.github/workflows/README.md +22 -0
  12. package/.github/workflows/auto-release.yml +112 -0
  13. package/.github/workflows/ci.yml +49 -0
  14. package/.github/workflows/claude-code-review.yml +44 -0
  15. package/.github/workflows/claude.yml +36 -0
  16. package/.github/workflows/sync-install.yml +47 -0
  17. package/.mcp.json +9 -0
  18. package/.prettierrc.json +7 -0
  19. package/C.png +0 -0
  20. package/CHANGELOG.md +74 -0
  21. package/CLAUDE.md +118 -0
  22. package/CONTRIBUTING.md +231 -0
  23. package/LICENSE +201 -0
  24. package/README.md +179 -0
  25. package/SECURITY.md +47 -0
  26. package/commands/conductor-setup.md +11 -0
  27. package/commands/conductor-status.md +7 -0
  28. package/dist/ai/base.d.ts +44 -0
  29. package/dist/ai/base.d.ts.map +1 -0
  30. package/dist/ai/base.js +47 -0
  31. package/dist/ai/base.js.map +1 -0
  32. package/dist/ai/claude.d.ts +11 -0
  33. package/dist/ai/claude.d.ts.map +1 -0
  34. package/dist/ai/claude.js +149 -0
  35. package/dist/ai/claude.js.map +1 -0
  36. package/dist/ai/gemini.d.ts +15 -0
  37. package/dist/ai/gemini.d.ts.map +1 -0
  38. package/dist/ai/gemini.js +156 -0
  39. package/dist/ai/gemini.js.map +1 -0
  40. package/dist/ai/maestro.d.ts +22 -0
  41. package/dist/ai/maestro.d.ts.map +1 -0
  42. package/dist/ai/maestro.js +142 -0
  43. package/dist/ai/maestro.js.map +1 -0
  44. package/dist/ai/manager.d.ts +47 -0
  45. package/dist/ai/manager.d.ts.map +1 -0
  46. package/dist/ai/manager.js +450 -0
  47. package/dist/ai/manager.js.map +1 -0
  48. package/dist/ai/ollama.d.ts +16 -0
  49. package/dist/ai/ollama.d.ts.map +1 -0
  50. package/dist/ai/ollama.js +151 -0
  51. package/dist/ai/ollama.js.map +1 -0
  52. package/dist/ai/openai.d.ts +11 -0
  53. package/dist/ai/openai.d.ts.map +1 -0
  54. package/dist/ai/openai.js +132 -0
  55. package/dist/ai/openai.js.map +1 -0
  56. package/dist/ai/openrouter.d.ts +11 -0
  57. package/dist/ai/openrouter.d.ts.map +1 -0
  58. package/dist/ai/openrouter.js +139 -0
  59. package/dist/ai/openrouter.js.map +1 -0
  60. package/dist/bot/slack.d.ts +17 -0
  61. package/dist/bot/slack.d.ts.map +1 -0
  62. package/dist/bot/slack.js +144 -0
  63. package/dist/bot/slack.js.map +1 -0
  64. package/dist/bot/telegram.d.ts +19 -0
  65. package/dist/bot/telegram.d.ts.map +1 -0
  66. package/dist/bot/telegram.js +157 -0
  67. package/dist/bot/telegram.js.map +1 -0
  68. package/dist/cli/commands/ai.d.ts +4 -0
  69. package/dist/cli/commands/ai.d.ts.map +1 -0
  70. package/dist/cli/commands/ai.js +161 -0
  71. package/dist/cli/commands/ai.js.map +1 -0
  72. package/dist/cli/commands/doctor.d.ts +18 -0
  73. package/dist/cli/commands/doctor.d.ts.map +1 -0
  74. package/dist/cli/commands/doctor.js +213 -0
  75. package/dist/cli/commands/doctor.js.map +1 -0
  76. package/dist/cli/commands/init.d.ts +15 -0
  77. package/dist/cli/commands/init.d.ts.map +1 -0
  78. package/dist/cli/commands/init.js +281 -0
  79. package/dist/cli/commands/init.js.map +1 -0
  80. package/dist/cli/commands/install.d.ts +16 -0
  81. package/dist/cli/commands/install.d.ts.map +1 -0
  82. package/dist/cli/commands/install.js +750 -0
  83. package/dist/cli/commands/install.js.map +1 -0
  84. package/dist/cli/commands/lifecycle.d.ts +4 -0
  85. package/dist/cli/commands/lifecycle.d.ts.map +1 -0
  86. package/dist/cli/commands/lifecycle.js +84 -0
  87. package/dist/cli/commands/lifecycle.js.map +1 -0
  88. package/dist/cli/commands/marketplace.d.ts +13 -0
  89. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  90. package/dist/cli/commands/marketplace.js +197 -0
  91. package/dist/cli/commands/marketplace.js.map +1 -0
  92. package/dist/cli/commands/mcp.d.ts +6 -0
  93. package/dist/cli/commands/mcp.d.ts.map +1 -0
  94. package/dist/cli/commands/mcp.js +83 -0
  95. package/dist/cli/commands/mcp.js.map +1 -0
  96. package/dist/cli/commands/onboard.d.ts +10 -0
  97. package/dist/cli/commands/onboard.d.ts.map +1 -0
  98. package/dist/cli/commands/onboard.js +207 -0
  99. package/dist/cli/commands/onboard.js.map +1 -0
  100. package/dist/cli/commands/plugin-create.d.ts +13 -0
  101. package/dist/cli/commands/plugin-create.d.ts.map +1 -0
  102. package/dist/cli/commands/plugin-create.js +122 -0
  103. package/dist/cli/commands/plugin-create.js.map +1 -0
  104. package/dist/cli/commands/plugins.d.ts +5 -0
  105. package/dist/cli/commands/plugins.d.ts.map +1 -0
  106. package/dist/cli/commands/plugins.js +30 -0
  107. package/dist/cli/commands/plugins.js.map +1 -0
  108. package/dist/cli/commands/release.d.ts +13 -0
  109. package/dist/cli/commands/release.d.ts.map +1 -0
  110. package/dist/cli/commands/release.js +243 -0
  111. package/dist/cli/commands/release.js.map +1 -0
  112. package/dist/cli/commands/telegram.d.ts +3 -0
  113. package/dist/cli/commands/telegram.d.ts.map +1 -0
  114. package/dist/cli/commands/telegram.js +20 -0
  115. package/dist/cli/commands/telegram.js.map +1 -0
  116. package/dist/cli/index.d.ts +3 -0
  117. package/dist/cli/index.d.ts.map +1 -0
  118. package/dist/cli/index.js +402 -0
  119. package/dist/cli/index.js.map +1 -0
  120. package/dist/config/oauth.d.ts +8 -0
  121. package/dist/config/oauth.d.ts.map +1 -0
  122. package/dist/config/oauth.js +13 -0
  123. package/dist/config/oauth.js.map +1 -0
  124. package/dist/core/audit.d.ts +91 -0
  125. package/dist/core/audit.d.ts.map +1 -0
  126. package/dist/core/audit.js +233 -0
  127. package/dist/core/audit.js.map +1 -0
  128. package/dist/core/circuit-breaker.d.ts +56 -0
  129. package/dist/core/circuit-breaker.d.ts.map +1 -0
  130. package/dist/core/circuit-breaker.js +107 -0
  131. package/dist/core/circuit-breaker.js.map +1 -0
  132. package/dist/core/conductor.d.ts +44 -0
  133. package/dist/core/conductor.d.ts.map +1 -0
  134. package/dist/core/conductor.js +200 -0
  135. package/dist/core/conductor.js.map +1 -0
  136. package/dist/core/config.d.ts +66 -0
  137. package/dist/core/config.d.ts.map +1 -0
  138. package/dist/core/config.js +86 -0
  139. package/dist/core/config.js.map +1 -0
  140. package/dist/core/database.d.ts +59 -0
  141. package/dist/core/database.d.ts.map +1 -0
  142. package/dist/core/database.js +342 -0
  143. package/dist/core/database.js.map +1 -0
  144. package/dist/core/errors.d.ts +231 -0
  145. package/dist/core/errors.d.ts.map +1 -0
  146. package/dist/core/errors.js +254 -0
  147. package/dist/core/errors.js.map +1 -0
  148. package/dist/core/health.d.ts +72 -0
  149. package/dist/core/health.d.ts.map +1 -0
  150. package/dist/core/health.js +116 -0
  151. package/dist/core/health.js.map +1 -0
  152. package/dist/core/interfaces.d.ts +62 -0
  153. package/dist/core/interfaces.d.ts.map +1 -0
  154. package/dist/core/interfaces.js +8 -0
  155. package/dist/core/interfaces.js.map +1 -0
  156. package/dist/core/logger.d.ts +15 -0
  157. package/dist/core/logger.d.ts.map +1 -0
  158. package/dist/core/logger.js +30 -0
  159. package/dist/core/logger.js.map +1 -0
  160. package/dist/core/rbac.d.ts +132 -0
  161. package/dist/core/rbac.d.ts.map +1 -0
  162. package/dist/core/rbac.js +230 -0
  163. package/dist/core/rbac.js.map +1 -0
  164. package/dist/core/retry.d.ts +22 -0
  165. package/dist/core/retry.d.ts.map +1 -0
  166. package/dist/core/retry.js +41 -0
  167. package/dist/core/retry.js.map +1 -0
  168. package/dist/core/webhooks.d.ts +92 -0
  169. package/dist/core/webhooks.d.ts.map +1 -0
  170. package/dist/core/webhooks.js +176 -0
  171. package/dist/core/webhooks.js.map +1 -0
  172. package/dist/core/zero-config.d.ts +22 -0
  173. package/dist/core/zero-config.d.ts.map +1 -0
  174. package/dist/core/zero-config.js +59 -0
  175. package/dist/core/zero-config.js.map +1 -0
  176. package/dist/dashboard/cli.d.ts +6 -0
  177. package/dist/dashboard/cli.d.ts.map +1 -0
  178. package/dist/dashboard/cli.js +42 -0
  179. package/dist/dashboard/cli.js.map +1 -0
  180. package/dist/dashboard/index.html +3426 -0
  181. package/dist/dashboard/server.d.ts +7 -0
  182. package/dist/dashboard/server.d.ts.map +1 -0
  183. package/dist/dashboard/server.js +1427 -0
  184. package/dist/dashboard/server.js.map +1 -0
  185. package/dist/mcp/server.d.ts +27 -0
  186. package/dist/mcp/server.d.ts.map +1 -0
  187. package/dist/mcp/server.js +380 -0
  188. package/dist/mcp/server.js.map +1 -0
  189. package/dist/mcp/tools/misc.d.ts +15 -0
  190. package/dist/mcp/tools/misc.d.ts.map +1 -0
  191. package/dist/mcp/tools/misc.js +49 -0
  192. package/dist/mcp/tools/misc.js.map +1 -0
  193. package/dist/plugins/builtin/calculator.d.ts +11 -0
  194. package/dist/plugins/builtin/calculator.d.ts.map +1 -0
  195. package/dist/plugins/builtin/calculator.js +166 -0
  196. package/dist/plugins/builtin/calculator.js.map +1 -0
  197. package/dist/plugins/builtin/colors.d.ts +15 -0
  198. package/dist/plugins/builtin/colors.d.ts.map +1 -0
  199. package/dist/plugins/builtin/colors.js +193 -0
  200. package/dist/plugins/builtin/colors.js.map +1 -0
  201. package/dist/plugins/builtin/cron.d.ts +40 -0
  202. package/dist/plugins/builtin/cron.d.ts.map +1 -0
  203. package/dist/plugins/builtin/cron.js +578 -0
  204. package/dist/plugins/builtin/cron.js.map +1 -0
  205. package/dist/plugins/builtin/crypto.d.ts +11 -0
  206. package/dist/plugins/builtin/crypto.d.ts.map +1 -0
  207. package/dist/plugins/builtin/crypto.js +83 -0
  208. package/dist/plugins/builtin/crypto.js.map +1 -0
  209. package/dist/plugins/builtin/database.d.ts +29 -0
  210. package/dist/plugins/builtin/database.d.ts.map +1 -0
  211. package/dist/plugins/builtin/database.js +230 -0
  212. package/dist/plugins/builtin/database.js.map +1 -0
  213. package/dist/plugins/builtin/docker.d.ts +12 -0
  214. package/dist/plugins/builtin/docker.d.ts.map +1 -0
  215. package/dist/plugins/builtin/docker.js +436 -0
  216. package/dist/plugins/builtin/docker.js.map +1 -0
  217. package/dist/plugins/builtin/fun.d.ts +11 -0
  218. package/dist/plugins/builtin/fun.d.ts.map +1 -0
  219. package/dist/plugins/builtin/fun.js +114 -0
  220. package/dist/plugins/builtin/fun.js.map +1 -0
  221. package/dist/plugins/builtin/gcal.d.ts +38 -0
  222. package/dist/plugins/builtin/gcal.d.ts.map +1 -0
  223. package/dist/plugins/builtin/gcal.js +280 -0
  224. package/dist/plugins/builtin/gcal.js.map +1 -0
  225. package/dist/plugins/builtin/gdrive.d.ts +26 -0
  226. package/dist/plugins/builtin/gdrive.d.ts.map +1 -0
  227. package/dist/plugins/builtin/gdrive.js +295 -0
  228. package/dist/plugins/builtin/gdrive.js.map +1 -0
  229. package/dist/plugins/builtin/github-actions.d.ts +38 -0
  230. package/dist/plugins/builtin/github-actions.d.ts.map +1 -0
  231. package/dist/plugins/builtin/github-actions.js +629 -0
  232. package/dist/plugins/builtin/github-actions.js.map +1 -0
  233. package/dist/plugins/builtin/github.d.ts +26 -0
  234. package/dist/plugins/builtin/github.d.ts.map +1 -0
  235. package/dist/plugins/builtin/github.js +800 -0
  236. package/dist/plugins/builtin/github.js.map +1 -0
  237. package/dist/plugins/builtin/gmail.d.ts +50 -0
  238. package/dist/plugins/builtin/gmail.d.ts.map +1 -0
  239. package/dist/plugins/builtin/gmail.js +445 -0
  240. package/dist/plugins/builtin/gmail.js.map +1 -0
  241. package/dist/plugins/builtin/hash.d.ts +11 -0
  242. package/dist/plugins/builtin/hash.d.ts.map +1 -0
  243. package/dist/plugins/builtin/hash.js +95 -0
  244. package/dist/plugins/builtin/hash.js.map +1 -0
  245. package/dist/plugins/builtin/homekit.d.ts +53 -0
  246. package/dist/plugins/builtin/homekit.d.ts.map +1 -0
  247. package/dist/plugins/builtin/homekit.js +341 -0
  248. package/dist/plugins/builtin/homekit.js.map +1 -0
  249. package/dist/plugins/builtin/index.d.ts +4 -0
  250. package/dist/plugins/builtin/index.d.ts.map +1 -0
  251. package/dist/plugins/builtin/index.js +96 -0
  252. package/dist/plugins/builtin/index.js.map +1 -0
  253. package/dist/plugins/builtin/jira.d.ts +50 -0
  254. package/dist/plugins/builtin/jira.d.ts.map +1 -0
  255. package/dist/plugins/builtin/jira.js +353 -0
  256. package/dist/plugins/builtin/jira.js.map +1 -0
  257. package/dist/plugins/builtin/linear.d.ts +35 -0
  258. package/dist/plugins/builtin/linear.d.ts.map +1 -0
  259. package/dist/plugins/builtin/linear.js +397 -0
  260. package/dist/plugins/builtin/linear.js.map +1 -0
  261. package/dist/plugins/builtin/lumen.d.ts +21 -0
  262. package/dist/plugins/builtin/lumen.d.ts.map +1 -0
  263. package/dist/plugins/builtin/lumen.js +404 -0
  264. package/dist/plugins/builtin/lumen.js.map +1 -0
  265. package/dist/plugins/builtin/memory.d.ts +22 -0
  266. package/dist/plugins/builtin/memory.d.ts.map +1 -0
  267. package/dist/plugins/builtin/memory.js +184 -0
  268. package/dist/plugins/builtin/memory.js.map +1 -0
  269. package/dist/plugins/builtin/n8n.d.ts +60 -0
  270. package/dist/plugins/builtin/n8n.d.ts.map +1 -0
  271. package/dist/plugins/builtin/n8n.js +519 -0
  272. package/dist/plugins/builtin/n8n.js.map +1 -0
  273. package/dist/plugins/builtin/network.d.ts +11 -0
  274. package/dist/plugins/builtin/network.d.ts.map +1 -0
  275. package/dist/plugins/builtin/network.js +88 -0
  276. package/dist/plugins/builtin/network.js.map +1 -0
  277. package/dist/plugins/builtin/notes.d.ts +47 -0
  278. package/dist/plugins/builtin/notes.d.ts.map +1 -0
  279. package/dist/plugins/builtin/notes.js +641 -0
  280. package/dist/plugins/builtin/notes.js.map +1 -0
  281. package/dist/plugins/builtin/notion.d.ts +47 -0
  282. package/dist/plugins/builtin/notion.d.ts.map +1 -0
  283. package/dist/plugins/builtin/notion.js +317 -0
  284. package/dist/plugins/builtin/notion.js.map +1 -0
  285. package/dist/plugins/builtin/shell.d.ts +12 -0
  286. package/dist/plugins/builtin/shell.d.ts.map +1 -0
  287. package/dist/plugins/builtin/shell.js +310 -0
  288. package/dist/plugins/builtin/shell.js.map +1 -0
  289. package/dist/plugins/builtin/slack.d.ts +31 -0
  290. package/dist/plugins/builtin/slack.d.ts.map +1 -0
  291. package/dist/plugins/builtin/slack.js +295 -0
  292. package/dist/plugins/builtin/slack.js.map +1 -0
  293. package/dist/plugins/builtin/spotify.d.ts +55 -0
  294. package/dist/plugins/builtin/spotify.d.ts.map +1 -0
  295. package/dist/plugins/builtin/spotify.js +623 -0
  296. package/dist/plugins/builtin/spotify.js.map +1 -0
  297. package/dist/plugins/builtin/stripe.d.ts +35 -0
  298. package/dist/plugins/builtin/stripe.d.ts.map +1 -0
  299. package/dist/plugins/builtin/stripe.js +376 -0
  300. package/dist/plugins/builtin/stripe.js.map +1 -0
  301. package/dist/plugins/builtin/system.d.ts +11 -0
  302. package/dist/plugins/builtin/system.d.ts.map +1 -0
  303. package/dist/plugins/builtin/system.js +91 -0
  304. package/dist/plugins/builtin/system.js.map +1 -0
  305. package/dist/plugins/builtin/text-tools.d.ts +11 -0
  306. package/dist/plugins/builtin/text-tools.d.ts.map +1 -0
  307. package/dist/plugins/builtin/text-tools.js +146 -0
  308. package/dist/plugins/builtin/text-tools.js.map +1 -0
  309. package/dist/plugins/builtin/timezone.d.ts +13 -0
  310. package/dist/plugins/builtin/timezone.d.ts.map +1 -0
  311. package/dist/plugins/builtin/timezone.js +164 -0
  312. package/dist/plugins/builtin/timezone.js.map +1 -0
  313. package/dist/plugins/builtin/todoist.d.ts +49 -0
  314. package/dist/plugins/builtin/todoist.d.ts.map +1 -0
  315. package/dist/plugins/builtin/todoist.js +540 -0
  316. package/dist/plugins/builtin/todoist.js.map +1 -0
  317. package/dist/plugins/builtin/translate.d.ts +11 -0
  318. package/dist/plugins/builtin/translate.d.ts.map +1 -0
  319. package/dist/plugins/builtin/translate.js +42 -0
  320. package/dist/plugins/builtin/translate.js.map +1 -0
  321. package/dist/plugins/builtin/url-tools.d.ts +11 -0
  322. package/dist/plugins/builtin/url-tools.d.ts.map +1 -0
  323. package/dist/plugins/builtin/url-tools.js +70 -0
  324. package/dist/plugins/builtin/url-tools.js.map +1 -0
  325. package/dist/plugins/builtin/vercel.d.ts +55 -0
  326. package/dist/plugins/builtin/vercel.d.ts.map +1 -0
  327. package/dist/plugins/builtin/vercel.js +514 -0
  328. package/dist/plugins/builtin/vercel.js.map +1 -0
  329. package/dist/plugins/builtin/weather.d.ts +13 -0
  330. package/dist/plugins/builtin/weather.d.ts.map +1 -0
  331. package/dist/plugins/builtin/weather.js +103 -0
  332. package/dist/plugins/builtin/weather.js.map +1 -0
  333. package/dist/plugins/builtin/x.d.ts +54 -0
  334. package/dist/plugins/builtin/x.d.ts.map +1 -0
  335. package/dist/plugins/builtin/x.js +402 -0
  336. package/dist/plugins/builtin/x.js.map +1 -0
  337. package/dist/plugins/manager.d.ts +77 -0
  338. package/dist/plugins/manager.d.ts.map +1 -0
  339. package/dist/plugins/manager.js +141 -0
  340. package/dist/plugins/manager.js.map +1 -0
  341. package/dist/plugins/validation.d.ts +18 -0
  342. package/dist/plugins/validation.d.ts.map +1 -0
  343. package/dist/plugins/validation.js +81 -0
  344. package/dist/plugins/validation.js.map +1 -0
  345. package/dist/security/auth.d.ts +23 -0
  346. package/dist/security/auth.d.ts.map +1 -0
  347. package/dist/security/auth.js +56 -0
  348. package/dist/security/auth.js.map +1 -0
  349. package/dist/security/keychain.d.ts +60 -0
  350. package/dist/security/keychain.d.ts.map +1 -0
  351. package/dist/security/keychain.js +213 -0
  352. package/dist/security/keychain.js.map +1 -0
  353. package/dist/utils/google-auth.d.ts +21 -0
  354. package/dist/utils/google-auth.d.ts.map +1 -0
  355. package/dist/utils/google-auth.js +135 -0
  356. package/dist/utils/google-auth.js.map +1 -0
  357. package/dist/utils/retry.d.ts +5 -0
  358. package/dist/utils/retry.d.ts.map +1 -0
  359. package/dist/utils/retry.js +34 -0
  360. package/dist/utils/retry.js.map +1 -0
  361. package/docs/README.md +13 -0
  362. package/docs/api.md +210 -0
  363. package/docs/getting-started.md +100 -0
  364. package/docs/plugins.md +306 -0
  365. package/docs-site/.vitepress/config.ts +59 -0
  366. package/docs-site/README.md +12 -0
  367. package/docs-site/index.md +30 -0
  368. package/eslint.config.js +29 -0
  369. package/install.ps1 +334 -0
  370. package/install.sh +1119 -0
  371. package/local-install.sh +304 -0
  372. package/package.json +90 -0
  373. package/packages/README.md +11 -0
  374. package/packages/plugin-sdk/README.md +12 -0
  375. package/packages/plugin-sdk/package.json +22 -0
  376. package/packages/plugin-sdk/src/README.md +11 -0
  377. package/packages/plugin-sdk/src/index.ts +191 -0
  378. package/sdks/README.md +26 -0
  379. package/sdks/csharp/ConductorClient.cs +65 -0
  380. package/sdks/csharp/README.md +11 -0
  381. package/sdks/go/README.md +11 -0
  382. package/sdks/go/conductor.go +257 -0
  383. package/sdks/java/ConductorClient.java +27 -0
  384. package/sdks/java/README.md +11 -0
  385. package/sdks/php/README.md +11 -0
  386. package/sdks/php/src/Client.php +72 -0
  387. package/sdks/python/README.md +12 -0
  388. package/sdks/python/conductor/__init__.py +227 -0
  389. package/sdks/python/pyproject.toml +30 -0
  390. package/sdks/ruby/README.md +11 -0
  391. package/sdks/ruby/lib/conductor.rb +46 -0
  392. package/sdks/rust/Cargo.toml +14 -0
  393. package/sdks/rust/README.md +11 -0
  394. package/sdks/swift/README.md +11 -0
  395. package/sdks/swift/Sources/Conductor/ConductorClient.swift +65 -0
  396. package/skills/conductor-mcp/SKILL.md +38 -0
  397. package/src/README.md +20 -0
  398. package/src/ai/README.md +18 -0
  399. package/src/ai/base.ts +93 -0
  400. package/src/ai/claude.ts +162 -0
  401. package/src/ai/gemini.ts +188 -0
  402. package/src/ai/maestro.ts +168 -0
  403. package/src/ai/manager.ts +537 -0
  404. package/src/ai/ollama.ts +186 -0
  405. package/src/ai/openai.ts +147 -0
  406. package/src/ai/openrouter.ts +152 -0
  407. package/src/bot/README.md +12 -0
  408. package/src/bot/slack.ts +164 -0
  409. package/src/bot/telegram.ts +185 -0
  410. package/src/cli/README.md +24 -0
  411. package/src/cli/commands/README.md +20 -0
  412. package/src/cli/commands/ai.ts +170 -0
  413. package/src/cli/commands/doctor.ts +221 -0
  414. package/src/cli/commands/init.ts +348 -0
  415. package/src/cli/commands/install.ts +792 -0
  416. package/src/cli/commands/lifecycle.ts +95 -0
  417. package/src/cli/commands/marketplace.ts +253 -0
  418. package/src/cli/commands/mcp.ts +92 -0
  419. package/src/cli/commands/onboard.ts +248 -0
  420. package/src/cli/commands/plugin-create.ts +130 -0
  421. package/src/cli/commands/plugins.ts +36 -0
  422. package/src/cli/commands/release.ts +251 -0
  423. package/src/cli/commands/telegram.ts +25 -0
  424. package/src/cli/index.ts +450 -0
  425. package/src/config/README.md +11 -0
  426. package/src/config/oauth.ts +26 -0
  427. package/src/core/README.md +22 -0
  428. package/src/core/audit.ts +291 -0
  429. package/src/core/circuit-breaker.ts +129 -0
  430. package/src/core/conductor.ts +240 -0
  431. package/src/core/config.ts +149 -0
  432. package/src/core/database.ts +411 -0
  433. package/src/core/errors.ts +275 -0
  434. package/src/core/health.ts +159 -0
  435. package/src/core/interfaces.ts +75 -0
  436. package/src/core/logger.ts +33 -0
  437. package/src/core/rbac.ts +321 -0
  438. package/src/core/retry.ts +61 -0
  439. package/src/core/webhooks.ts +234 -0
  440. package/src/core/zero-config.ts +72 -0
  441. package/src/dashboard/README.md +15 -0
  442. package/src/dashboard/cli.ts +48 -0
  443. package/src/dashboard/index.html +3426 -0
  444. package/src/dashboard/server.ts +1544 -0
  445. package/src/mcp/README.md +20 -0
  446. package/src/mcp/server.ts +475 -0
  447. package/src/mcp/tools/README.md +11 -0
  448. package/src/mcp/tools/misc.ts +61 -0
  449. package/src/plugins/README.md +28 -0
  450. package/src/plugins/builtin/README.md +23 -0
  451. package/src/plugins/builtin/calculator.ts +178 -0
  452. package/src/plugins/builtin/colors.ts +201 -0
  453. package/src/plugins/builtin/cron.ts +649 -0
  454. package/src/plugins/builtin/crypto.ts +85 -0
  455. package/src/plugins/builtin/database.ts +235 -0
  456. package/src/plugins/builtin/docker.ts +426 -0
  457. package/src/plugins/builtin/fun.ts +118 -0
  458. package/src/plugins/builtin/gcal.ts +305 -0
  459. package/src/plugins/builtin/gdrive.ts +326 -0
  460. package/src/plugins/builtin/github-actions.ts +666 -0
  461. package/src/plugins/builtin/github.ts +912 -0
  462. package/src/plugins/builtin/gmail.ts +492 -0
  463. package/src/plugins/builtin/hash.ts +98 -0
  464. package/src/plugins/builtin/homekit.ts +389 -0
  465. package/src/plugins/builtin/index.ts +116 -0
  466. package/src/plugins/builtin/jira.ts +380 -0
  467. package/src/plugins/builtin/linear.ts +448 -0
  468. package/src/plugins/builtin/lumen.ts +497 -0
  469. package/src/plugins/builtin/memory.ts +200 -0
  470. package/src/plugins/builtin/n8n.ts +565 -0
  471. package/src/plugins/builtin/network.ts +92 -0
  472. package/src/plugins/builtin/notes.ts +689 -0
  473. package/src/plugins/builtin/notion.ts +348 -0
  474. package/src/plugins/builtin/shell.ts +334 -0
  475. package/src/plugins/builtin/slack.ts +327 -0
  476. package/src/plugins/builtin/spotify.ts +665 -0
  477. package/src/plugins/builtin/stripe.ts +388 -0
  478. package/src/plugins/builtin/system.ts +93 -0
  479. package/src/plugins/builtin/text-tools.ts +150 -0
  480. package/src/plugins/builtin/timezone.ts +173 -0
  481. package/src/plugins/builtin/todoist.ts +625 -0
  482. package/src/plugins/builtin/translate.ts +47 -0
  483. package/src/plugins/builtin/url-tools.ts +73 -0
  484. package/src/plugins/builtin/vercel.ts +546 -0
  485. package/src/plugins/builtin/weather.ts +112 -0
  486. package/src/plugins/builtin/x.ts +440 -0
  487. package/src/plugins/manager.ts +213 -0
  488. package/src/plugins/validation.ts +94 -0
  489. package/src/security/README.md +12 -0
  490. package/src/security/auth.ts +72 -0
  491. package/src/security/keychain.ts +226 -0
  492. package/src/utils/README.md +12 -0
  493. package/src/utils/google-auth.ts +159 -0
  494. package/src/utils/retry.ts +41 -0
  495. package/test-all.mjs +1256 -0
  496. package/test.mjs +633 -0
  497. package/tests/README.md +19 -0
  498. package/tests/calculator.test.ts +54 -0
  499. package/tests/docker.test.ts +42 -0
  500. package/tests/load.test.ts +129 -0
  501. package/tests/mcp.test.ts +14 -0
  502. package/tests/shell.test.ts +42 -0
  503. package/tsconfig.json +21 -0
  504. package/vitest.config.ts +14 -0
@@ -0,0 +1,1544 @@
1
+ import express, { Request, Response, NextFunction } from 'express';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import fs from 'fs/promises';
5
+ import crypto from 'crypto';
6
+ import os from 'os';
7
+ import { execFile } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import rateLimit from 'express-rate-limit';
10
+ import { ConfigManager } from '../core/config.js';
11
+ import { DatabaseManager } from '../core/database.js';
12
+ import { Keychain } from '../security/keychain.js';
13
+ import type { Conductor } from '../core/conductor.js';
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ // ── Constants ────────────────────────────────────────────────────────────────
21
+
22
+ const ALL_PLUGINS: readonly string[] = [
23
+ 'calculator',
24
+ 'colors',
25
+ 'cron',
26
+ 'crypto',
27
+ 'fun',
28
+ 'gcal',
29
+ 'gdrive',
30
+ 'github',
31
+ 'github-actions',
32
+ 'gmail',
33
+ 'hash',
34
+ 'homekit',
35
+ 'memory',
36
+ 'n8n',
37
+ 'network',
38
+ 'notes',
39
+ 'notion',
40
+ 'slack',
41
+ 'spotify',
42
+ 'system',
43
+ 'text-tools',
44
+ 'timezone',
45
+ 'todoist',
46
+ 'translate',
47
+ 'url-tools',
48
+ 'vercel',
49
+ 'weather',
50
+ 'x',
51
+ ] as const;
52
+
53
+ const PLUGIN_REQUIRED_CREDS: Record<string, { service: string; key: string }[]> = {
54
+ github: [{ service: 'github', key: 'token' }],
55
+ 'github-actions': [{ service: 'github', key: 'token' }],
56
+ gmail: [{ service: 'google', key: 'access_token' }],
57
+ gcal: [{ service: 'google', key: 'access_token' }],
58
+ gdrive: [{ service: 'google', key: 'access_token' }],
59
+ notion: [{ service: 'notion', key: 'api_key' }],
60
+ spotify: [{ service: 'spotify', key: 'client_id' }],
61
+ n8n: [{ service: 'n8n', key: 'api_key' }],
62
+ vercel: [{ service: 'vercel', key: 'token' }],
63
+ weather: [{ service: 'weather', key: 'api_key' }],
64
+ x: [{ service: 'x', key: 'api_key' }],
65
+ homekit: [{ service: 'homekit', key: 'base_url' }],
66
+ slack: [{ service: 'slack', key: 'bot_token' }],
67
+ todoist: [{ service: 'todoist', key: 'api_token' }],
68
+ };
69
+
70
+ interface CredentialEntry {
71
+ service: string;
72
+ key: string;
73
+ }
74
+
75
+ const KNOWN_CREDENTIALS: CredentialEntry[] = [
76
+ { service: 'conductor', key: 'api_key' },
77
+ { service: 'claude', key: 'api_key' },
78
+ { service: 'openai', key: 'api_key' },
79
+ { service: 'gemini', key: 'api_key' },
80
+ { service: 'github', key: 'token' },
81
+ { service: 'telegram', key: 'bot_token' },
82
+ { service: 'spotify', key: 'client_id' },
83
+ { service: 'spotify', key: 'client_secret' },
84
+ { service: 'notion', key: 'api_key' },
85
+ { service: 'n8n', key: 'api_key' },
86
+ { service: 'vercel', key: 'token' },
87
+ { service: 'weather', key: 'api_key' },
88
+ { service: 'x', key: 'api_key' },
89
+ { service: 'google', key: 'access_token' },
90
+ { service: 'slack', key: 'bot_token' },
91
+ { service: 'todoist', key: 'api_token' },
92
+ ];
93
+
94
+ // Bundled Google OAuth app — users never need to create their own
95
+ const GOOGLE_CLIENT_ID = '529105409300-vmtlgnvcpfohtd7ha9o98fkel6ldjmin.apps.googleusercontent.com';
96
+ const GOOGLE_CLIENT_SECRET: string | undefined = process.env.GOOGLE_CLIENT_SECRET;
97
+ const GOOGLE_REDIRECT_URI = 'http://localhost:4242/api/auth/google/callback';
98
+
99
+ // ── Public types ──────────────────────────────────────────────────────────────
100
+
101
+ export interface DashboardServer {
102
+ port: number;
103
+ close(): Promise<void>;
104
+ }
105
+
106
+ // ── Main export ───────────────────────────────────────────────────────────────
107
+
108
+ /** Generate or load the persistent dashboard session token. */
109
+ async function getDashboardToken(configDir: string): Promise<string> {
110
+ const tokenPath = path.join(configDir, 'dashboard.token');
111
+ try {
112
+ const existing = (await fs.readFile(tokenPath, 'utf-8')).trim();
113
+ if (existing.length >= 32) return existing;
114
+ } catch {
115
+ /* not yet created */
116
+ }
117
+
118
+ const token = crypto.randomBytes(24).toString('hex');
119
+ await fs.mkdir(configDir, { recursive: true });
120
+ await fs.writeFile(tokenPath, token, { mode: 0o600 });
121
+ return token;
122
+ }
123
+
124
+ /** In-memory log buffer for SSE streaming */
125
+ const logBuffer: Array<{ level: string; message: string; timestamp: string }> = [];
126
+ const maxLogBuffer = 500;
127
+ const sseClients: Set<Response> = new Set();
128
+
129
+ function interceptLogs(): void {
130
+ const origLog = console.log.bind(console);
131
+ const origError = console.error.bind(console);
132
+ const origWarn = console.warn.bind(console);
133
+
134
+ function pushLog(level: string, args: unknown[]): void {
135
+ const message = args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
136
+ const entry = { level, message, timestamp: new Date().toISOString() };
137
+ logBuffer.push(entry);
138
+ if (logBuffer.length > maxLogBuffer) logBuffer.shift();
139
+ const data = `data: ${JSON.stringify(entry)}\n\n`;
140
+ for (const client of sseClients) {
141
+ try {
142
+ client.write(data);
143
+ } catch {
144
+ sseClients.delete(client);
145
+ }
146
+ }
147
+ }
148
+
149
+ console.log = (...args: unknown[]) => {
150
+ origLog(...args);
151
+ pushLog('info', args);
152
+ };
153
+ console.error = (...args: unknown[]) => {
154
+ origError(...args);
155
+ pushLog('error', args);
156
+ };
157
+ console.warn = (...args: unknown[]) => {
158
+ origWarn(...args);
159
+ pushLog('warn', args);
160
+ };
161
+ }
162
+
163
+ export async function startDashboard(port = 4242, conductorInstance?: Conductor): Promise<DashboardServer> {
164
+ interceptLogs();
165
+
166
+ const config = new ConfigManager();
167
+ await config.initialize();
168
+ const keychain = new Keychain(config.getConfigDir());
169
+
170
+ // Initialize database for conversations endpoint
171
+ const db = new DatabaseManager(config.getConfigDir());
172
+ try {
173
+ await db.initialize();
174
+ } catch {
175
+ // DB may not exist yet — conversations endpoint will return empty
176
+ }
177
+
178
+ // Generate / load session token
179
+ const dashboardToken = await getDashboardToken(config.getConfigDir());
180
+
181
+ // Auto-store bundled Google OAuth creds so the rest of Conductor can use them
182
+ const existingOAuth = config.get<{ clientId?: string }>('oauth.google');
183
+ if (!existingOAuth?.clientId && GOOGLE_CLIENT_SECRET) {
184
+ await config.set('oauth.google', {
185
+ clientId: GOOGLE_CLIENT_ID,
186
+ clientSecret: GOOGLE_CLIENT_SECRET,
187
+ redirectUri: GOOGLE_REDIRECT_URI,
188
+ });
189
+ }
190
+
191
+ const app = express();
192
+ app.use(express.json());
193
+
194
+ // CORS — allow both localhost and 127.0.0.1
195
+ app.use((_req: Request, res: Response, next: NextFunction): void => {
196
+ const origin = (_req.headers.origin as string) || '';
197
+ const allowed = ['http://localhost:4242', 'http://127.0.0.1:4242'];
198
+ if (allowed.includes(origin)) {
199
+ res.setHeader('Access-Control-Allow-Origin', origin);
200
+ } else {
201
+ res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:4242');
202
+ }
203
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS');
204
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
205
+ next();
206
+ });
207
+ app.options('/{*path}', (_req: Request, res: Response): void => {
208
+ res.sendStatus(204);
209
+ });
210
+
211
+ // ── Rate limiting ─────────────────────────────────────────────────────────
212
+ app.use(
213
+ '/api',
214
+ rateLimit({
215
+ windowMs: 60 * 1000, // 1 minute
216
+ max: 120, // 120 requests per minute per IP
217
+ standardHeaders: true,
218
+ legacyHeaders: false,
219
+ message: { error: 'COND-RATE-001: Too many requests. Slow down.' },
220
+ }),
221
+ );
222
+
223
+ // ── Authentication middleware for /api/* routes ───────────────────────────
224
+ // The Google OAuth callback must remain open (browser redirect from google.com)
225
+ app.use('/api', (req: Request, res: Response, next: NextFunction): void => {
226
+ if (req.path === '/auth/google/callback') {
227
+ next();
228
+ return;
229
+ }
230
+ // Accept token from Authorization header OR ?token= query param (needed for EventSource / SSE)
231
+ const authHeader = req.headers['authorization'];
232
+ const queryToken = (req.query as Record<string, string>).token;
233
+ const rawToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : (queryToken ?? '');
234
+ if (!rawToken) {
235
+ res.status(401).json({
236
+ error:
237
+ 'COND-AUTH-001: Unauthorized — include Authorization: Bearer <token>. Generate a token with: conductor dashboard token',
238
+ });
239
+ return;
240
+ }
241
+ const provided = rawToken;
242
+ // Constant-time comparison to prevent timing attacks
243
+ try {
244
+ const tokenBuf = Buffer.from(dashboardToken, 'utf-8');
245
+ const providedBuf = Buffer.from(provided, 'utf-8');
246
+ if (tokenBuf.length !== providedBuf.length || !crypto.timingSafeEqual(tokenBuf, providedBuf)) {
247
+ res
248
+ .status(401)
249
+ .json({ error: 'COND-AUTH-002: Invalid token. Generate a new token with: conductor dashboard token' });
250
+ return;
251
+ }
252
+ } catch {
253
+ res
254
+ .status(401)
255
+ .json({ error: 'COND-AUTH-003: Invalid token format. Generate a new token with: conductor dashboard token' });
256
+ return;
257
+ }
258
+ next();
259
+ });
260
+
261
+ // ── Static ────────────────────────────────────────────────────────────────
262
+ // Inject dashboard token as a meta tag so the JS can read it
263
+ app.get('/', async (_req: Request, res: Response): Promise<void> => {
264
+ try {
265
+ const htmlPath = path.join(__dirname, 'index.html');
266
+ let html = await fs.readFile(htmlPath, 'utf-8');
267
+ // Inject meta tag with the token right before </head>
268
+ // Replace the placeholder meta tag that's already in the HTML template
269
+ html = html.replace(
270
+ '<meta name="dashboard-token" content="">',
271
+ `<meta name="dashboard-token" content="${dashboardToken}">`,
272
+ );
273
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
274
+ res.send(html);
275
+ } catch {
276
+ res.status(500).send('Dashboard HTML not found. Run: npm run build');
277
+ }
278
+ });
279
+
280
+ // ── Status ────────────────────────────────────────────────────────────────
281
+ app.get('/api/status', async (_req: Request, res: Response): Promise<void> => {
282
+ let version = 'unknown';
283
+ try {
284
+ const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
285
+ version = (JSON.parse(await fs.readFile(pkgPath, 'utf-8')) as { version?: string }).version ?? 'unknown';
286
+ } catch {
287
+ /* ignore */
288
+ }
289
+ res.json({ version, configDir: config.getConfigDir(), nodeVersion: process.version, platform: process.platform });
290
+ });
291
+
292
+ // ── Config ────────────────────────────────────────────────────────────────
293
+ app.get('/api/config', (_req: Request, res: Response): void => {
294
+ res.json(config.getConfig());
295
+ });
296
+
297
+ app.post('/api/config', async (req: Request, res: Response): Promise<void> => {
298
+ const body = req.body as { key?: string; value?: unknown };
299
+ if (typeof body.key !== 'string' || body.key.trim() === '') {
300
+ res.status(400).json({ error: '`key` must be a non-empty string' });
301
+ return;
302
+ }
303
+ await config.set(body.key, body.value);
304
+ res.json({ ok: true });
305
+ });
306
+
307
+ // ── Plugins ───────────────────────────────────────────────────────────────
308
+ app.get('/api/plugins', (_req: Request, res: Response): void => {
309
+ const installed = config.get<string[]>('plugins.installed') ?? [];
310
+ const enabled = config.get<string[]>('plugins.enabled') ?? [];
311
+ res.json({ installed, enabled, all: ALL_PLUGINS, requiredCreds: PLUGIN_REQUIRED_CREDS });
312
+ });
313
+
314
+ app.post('/api/plugins/toggle', async (req: Request, res: Response): Promise<void> => {
315
+ const body = req.body as { plugin?: string; enabled?: boolean };
316
+ if (typeof body.plugin !== 'string' || body.plugin.trim() === '') {
317
+ res.status(400).json({ error: '`plugin` must be a non-empty string' });
318
+ return;
319
+ }
320
+ if (typeof body.enabled !== 'boolean') {
321
+ res.status(400).json({ error: '`enabled` must be a boolean' });
322
+ return;
323
+ }
324
+
325
+ if (body.enabled && PLUGIN_REQUIRED_CREDS[body.plugin]) {
326
+ const missing: string[] = [];
327
+ for (const { service, key } of PLUGIN_REQUIRED_CREDS[body.plugin]) {
328
+ if (!(await keychain.has(service, key))) missing.push(`${service} / ${key}`);
329
+ }
330
+ if (missing.length > 0) {
331
+ res.status(400).json({ error: `Missing credentials: ${missing.join(', ')}`, missingCreds: missing });
332
+ return;
333
+ }
334
+ }
335
+
336
+ const current = config.get<string[]>('plugins.enabled') ?? [];
337
+ const updated = body.enabled
338
+ ? current.includes(body.plugin)
339
+ ? current
340
+ : [...current, body.plugin]
341
+ : current.filter((p: string) => p !== body.plugin);
342
+
343
+ await config.set('plugins.enabled', updated);
344
+ res.json({ ok: true, enabled: updated });
345
+ });
346
+
347
+ // ── Credentials ───────────────────────────────────────────────────────────
348
+ app.get('/api/credentials', async (_req: Request, res: Response): Promise<void> => {
349
+ const result = await Promise.all(
350
+ KNOWN_CREDENTIALS.map(async ({ service, key }) => ({
351
+ service,
352
+ key,
353
+ hasValue: await keychain.has(service, key),
354
+ })),
355
+ );
356
+ res.json(result);
357
+ });
358
+
359
+ app.post('/api/credentials', async (req: Request, res: Response): Promise<void> => {
360
+ const body = req.body as { service?: string; key?: string; value?: string };
361
+ if (!body.service || !body.key || !body.value) {
362
+ res.status(400).json({ error: '`service`, `key`, and `value` are all required' });
363
+ return;
364
+ }
365
+ await keychain.set(body.service, body.key, body.value);
366
+ res.json({ ok: true });
367
+ });
368
+
369
+ app.delete('/api/credentials/:service/:key', async (req: Request, res: Response): Promise<void> => {
370
+ const { service, key } = req.params as { service: string; key: string };
371
+ await keychain.delete(service, key);
372
+ res.json({ ok: true });
373
+ });
374
+
375
+ // ── Google OAuth ──────────────────────────────────────────────────────────
376
+ app.get('/api/auth/google/status', async (_req: Request, res: Response): Promise<void> => {
377
+ const connected = await keychain.has('google', 'access_token');
378
+ res.json({ connected });
379
+ });
380
+
381
+ app.get('/api/auth/google/url', async (_req: Request, res: Response): Promise<void> => {
382
+ if (!GOOGLE_CLIENT_SECRET) {
383
+ res.status(503).json({ error: 'Google OAuth is not configured — set GOOGLE_CLIENT_SECRET env var' });
384
+ return;
385
+ }
386
+ try {
387
+ const { google } = await import('googleapis');
388
+ const oauth2Client = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI);
389
+ const url = oauth2Client.generateAuthUrl({
390
+ access_type: 'offline',
391
+ prompt: 'consent',
392
+ scope: [
393
+ 'https://www.googleapis.com/auth/userinfo.email',
394
+ 'https://www.googleapis.com/auth/userinfo.profile',
395
+ 'https://www.googleapis.com/auth/gmail.modify',
396
+ 'https://www.googleapis.com/auth/calendar',
397
+ 'https://www.googleapis.com/auth/drive.file',
398
+ ],
399
+ });
400
+ res.json({ url });
401
+ } catch (e: unknown) {
402
+ res.status(500).json({ error: (e as Error).message });
403
+ }
404
+ });
405
+
406
+ app.get('/api/auth/google/callback', async (req: Request, res: Response): Promise<void> => {
407
+ if (!GOOGLE_CLIENT_SECRET) {
408
+ res.status(503).json({ error: 'Google OAuth is not configured — set GOOGLE_CLIENT_SECRET env var' });
409
+ return;
410
+ }
411
+ const code = (req.query as Record<string, string>).code;
412
+ if (!code) {
413
+ res.status(400).send('<h2>Missing code</h2>');
414
+ return;
415
+ }
416
+ try {
417
+ const { google } = await import('googleapis');
418
+ const oauth2Client = new google.auth.OAuth2(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI);
419
+ const { tokens } = await oauth2Client.getToken(code);
420
+ if (tokens.access_token) await keychain.set('google', 'access_token', tokens.access_token);
421
+ if (tokens.refresh_token) await keychain.set('google', 'refresh_token', tokens.refresh_token);
422
+ res.send(`<!DOCTYPE html><html><head><title>Connected</title></head>
423
+ <body style="font-family:system-ui;text-align:center;padding:60px;background:#0d0d0d;color:#e8e8e8">
424
+ <h2 style="color:#22c55e;margin-bottom:12px">✓ Google Connected</h2>
425
+ <p style="color:#888">Gmail, Calendar, and Drive are ready.</p>
426
+ <p style="color:#555;font-size:12px;margin-top:8px">This tab will close automatically.</p>
427
+ <script>setTimeout(()=>{window.close();},1800);</script>
428
+ </body></html>`);
429
+ } catch (e: unknown) {
430
+ res.status(500).send(`<h2 style="color:#ef4444">Auth failed: ${(e as Error).message}</h2>`);
431
+ }
432
+ });
433
+
434
+ app.delete('/api/auth/google', async (_req: Request, res: Response): Promise<void> => {
435
+ await keychain.delete('google', 'access_token');
436
+ await keychain.delete('google', 'refresh_token');
437
+ res.json({ ok: true });
438
+ });
439
+
440
+ // ── Activity log ──────────────────────────────────────────────────────────
441
+ app.get('/api/activity', async (_req: Request, res: Response): Promise<void> => {
442
+ try {
443
+ const logsDir = path.join(config.getConfigDir(), 'logs');
444
+ let entries: unknown[] = [];
445
+ try {
446
+ const files = await fs.readdir(logsDir);
447
+ for (const f of files.filter((f: string) => f.endsWith('.json')).slice(-5)) {
448
+ try {
449
+ const raw = await fs.readFile(path.join(logsDir, f), 'utf-8');
450
+ const parsed = JSON.parse(raw);
451
+ if (Array.isArray(parsed)) entries.push(...parsed);
452
+ else entries.push(parsed);
453
+ } catch {
454
+ /* skip bad file */
455
+ }
456
+ }
457
+ } catch {
458
+ /* no logs dir */
459
+ }
460
+ res.json({ entries: entries.slice(-20) });
461
+ } catch {
462
+ res.json({ entries: [] });
463
+ }
464
+ });
465
+
466
+ // ── System Control ────────────────────────────────────────────────────────
467
+
468
+ // Safe command runner using execFile (no shell interpretation)
469
+ async function runCmd(cmd: string, timeoutMs = 30000): Promise<{ stdout: string; stderr: string; exitCode: number }> {
470
+ // Whitelist of allowed dashboard commands
471
+ const allowedPrefixes = [
472
+ 'ps ',
473
+ 'tasklist',
474
+ 'open ',
475
+ 'xdg-open',
476
+ 'screencapture',
477
+ 'scrot',
478
+ 'pbpaste',
479
+ 'xclip',
480
+ 'xsel',
481
+ 'ifconfig',
482
+ 'ip ',
483
+ 'netstat',
484
+ 'ss ',
485
+ 'lsof',
486
+ 'docker ',
487
+ 'crontab',
488
+ 'git ',
489
+ ];
490
+ const trimmed = cmd.trim();
491
+ const isAllowed = allowedPrefixes.some((p) => trimmed.startsWith(p));
492
+ if (!isAllowed) {
493
+ return { stdout: '', stderr: `Command not allowed in dashboard: ${trimmed}`, exitCode: 1 };
494
+ }
495
+ const [executable, ...args] = trimmed.split(/\s+/);
496
+ try {
497
+ const { stdout, stderr } = await execFileAsync(executable, args, {
498
+ timeout: timeoutMs,
499
+ maxBuffer: 10 * 1024 * 1024,
500
+ });
501
+ return { stdout: (stdout ?? '').trim(), stderr: (stderr ?? '').trim(), exitCode: 0 };
502
+ } catch (err: unknown) {
503
+ if (err && typeof err === 'object' && 'code' in err) {
504
+ const e = err as { code?: number; stdout?: string; stderr?: string };
505
+ return { stdout: (e.stdout ?? '').trim(), stderr: (e.stderr ?? '').trim(), exitCode: e.code ?? 1 };
506
+ }
507
+ return { stdout: '', stderr: String(err), exitCode: 1 };
508
+ }
509
+ }
510
+
511
+ // GET /api/system/info
512
+ app.get('/api/system/info', async (_req: Request, res: Response): Promise<void> => {
513
+ const cpus = os.cpus();
514
+ res.json({
515
+ platform: os.platform(),
516
+ arch: os.arch(),
517
+ hostname: os.hostname(),
518
+ uptime: os.uptime(),
519
+ memory: {
520
+ total: os.totalmem(),
521
+ free: os.freemem(),
522
+ used: os.totalmem() - os.freemem(),
523
+ },
524
+ cpu: {
525
+ model: cpus[0]?.model ?? 'unknown',
526
+ cores: cpus.length,
527
+ usage: null, // point-in-time snapshot not meaningful without sampling interval
528
+ },
529
+ });
530
+ });
531
+
532
+ // POST /api/system/shell — REMOVED for security
533
+ // Shell access through a web dashboard is an unacceptable attack surface.
534
+ // Use the CLI directly for shell operations.
535
+ app.post('/api/system/shell', async (_req: Request, res: Response): Promise<void> => {
536
+ res.status(410).json({ error: 'Shell endpoint has been removed for security. Use the CLI directly.' });
537
+ });
538
+
539
+ // GET /api/system/processes — REMOVED (relied on shell execution)
540
+ app.get('/api/system/processes', async (_req: Request, res: Response): Promise<void> => {
541
+ res.status(410).json({ error: 'Process listing endpoint has been removed (relied on shell execution).' });
542
+ });
543
+
544
+ // POST /api/system/open
545
+ app.post('/api/system/open', async (req: Request, res: Response): Promise<void> => {
546
+ const body = req.body as { path?: string };
547
+ if (!body.path || typeof body.path !== 'string' || body.path.trim() === '') {
548
+ res.status(400).json({ error: '`path` is required' });
549
+ return;
550
+ }
551
+ const platform = os.platform();
552
+ let opener: string;
553
+ if (platform === 'darwin') opener = 'open';
554
+ else if (platform === 'win32') opener = 'start ""';
555
+ else opener = 'xdg-open';
556
+ const result = await runCmd(`${opener} ${JSON.stringify(body.path.trim())}`);
557
+ res.json(result);
558
+ });
559
+
560
+ // POST /api/system/notify
561
+ app.post('/api/system/notify', async (req: Request, res: Response): Promise<void> => {
562
+ const body = req.body as { title?: string; message?: string };
563
+ if (!body.title || !body.message) {
564
+ res.status(400).json({ error: '`title` and `message` are required' });
565
+ return;
566
+ }
567
+ const platform = os.platform();
568
+ let cmd: string;
569
+ if (platform === 'darwin') {
570
+ const t = body.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
571
+ const m = body.message.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
572
+ cmd = `osascript -e 'display notification "${m}" with title "${t}"'`;
573
+ } else {
574
+ cmd = `notify-send ${JSON.stringify(body.title)} ${JSON.stringify(body.message)}`;
575
+ }
576
+ const result = await runCmd(cmd);
577
+ res.json(result);
578
+ });
579
+
580
+ // GET /api/system/clipboard
581
+ app.get('/api/system/clipboard', async (_req: Request, res: Response): Promise<void> => {
582
+ const platform = os.platform();
583
+ const cmd = platform === 'darwin' ? 'pbpaste' : 'xclip -o';
584
+ const result = await runCmd(cmd);
585
+ res.json({ text: result.stdout, ...result });
586
+ });
587
+
588
+ // POST /api/system/clipboard
589
+ app.post('/api/system/clipboard', async (req: Request, res: Response): Promise<void> => {
590
+ const body = req.body as { text?: string };
591
+ if (typeof body.text !== 'string') {
592
+ res.status(400).json({ error: '`text` is required' });
593
+ return;
594
+ }
595
+ const platform = os.platform();
596
+ const cmd = platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
597
+ const result = await runCmd(`printf '%s' ${JSON.stringify(body.text)} | ${cmd}`);
598
+ res.json(result);
599
+ });
600
+
601
+ // GET /api/system/screenshot
602
+ app.get('/api/system/screenshot', async (_req: Request, res: Response): Promise<void> => {
603
+ const platform = os.platform();
604
+ const tmpFile = '/tmp/conductor-screenshot.png';
605
+ let captureResult: { stdout: string; stderr: string; exitCode: number };
606
+ if (platform === 'darwin') {
607
+ captureResult = await runCmd(`screencapture -x -t png ${tmpFile}`);
608
+ } else {
609
+ captureResult = await runCmd(`scrot ${tmpFile}`);
610
+ }
611
+ if (captureResult.exitCode !== 0) {
612
+ res.status(500).json({ error: 'Screenshot failed', ...captureResult });
613
+ return;
614
+ }
615
+ try {
616
+ const imgBuf = await fs.readFile(tmpFile);
617
+ const image = imgBuf.toString('base64');
618
+ await fs.unlink(tmpFile).catch(() => {
619
+ /* best-effort cleanup */
620
+ });
621
+ res.json({ image, mimeType: 'image/png' });
622
+ } catch (e: unknown) {
623
+ res.status(500).json({ error: (e as Error).message });
624
+ }
625
+ });
626
+
627
+ // POST /api/system/type
628
+ app.post('/api/system/type', async (req: Request, res: Response): Promise<void> => {
629
+ const body = req.body as { text?: string };
630
+ if (typeof body.text !== 'string' || body.text === '') {
631
+ res.status(400).json({ error: '`text` is required' });
632
+ return;
633
+ }
634
+ const platform = os.platform();
635
+ let cmd: string;
636
+ if (platform === 'darwin') {
637
+ const escaped = body.text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
638
+ cmd = `osascript -e 'tell application "System Events" to keystroke "${escaped}"'`;
639
+ } else {
640
+ cmd = `xdotool type ${JSON.stringify(body.text)}`;
641
+ }
642
+ const result = await runCmd(cmd);
643
+ res.json(result);
644
+ });
645
+
646
+ // GET /api/system/windows
647
+ app.get('/api/system/windows', async (_req: Request, res: Response): Promise<void> => {
648
+ const platform = os.platform();
649
+ let cmd: string;
650
+ if (platform === 'darwin') {
651
+ cmd = `osascript -e 'tell application "System Events" to get name of every process whose background only is false'`;
652
+ } else if (platform === 'win32') {
653
+ cmd = `powershell -Command "Get-Process | Where-Object {$_.MainWindowTitle -ne ''} | Select-Object -ExpandProperty MainWindowTitle"`;
654
+ } else {
655
+ cmd = `wmctrl -l 2>/dev/null || xdotool search --onlyvisible --name "" getwindowname 2>/dev/null || echo ''`;
656
+ }
657
+ const result = await runCmd(cmd);
658
+ const apps = result.stdout
659
+ .split(/,\s*|\n/)
660
+ .map((s: string) => s.trim())
661
+ .filter((s: string) => s.length > 0);
662
+ res.json({ apps, raw: result.stdout });
663
+ });
664
+
665
+ // ── File System Routes ────────────────────────────────────────────────────
666
+
667
+ const ALLOWED_TEXT_EXTENSIONS = new Set([
668
+ '.txt',
669
+ '.md',
670
+ '.json',
671
+ '.ts',
672
+ '.js',
673
+ '.py',
674
+ '.sh',
675
+ '.yaml',
676
+ '.yml',
677
+ '.toml',
678
+ '.env',
679
+ '.log',
680
+ '.csv',
681
+ '.html',
682
+ '.css',
683
+ '.xml',
684
+ '.sql',
685
+ '.go',
686
+ '.rs',
687
+ '.rb',
688
+ '.php',
689
+ '.java',
690
+ '.c',
691
+ '.cpp',
692
+ '.h',
693
+ ]);
694
+
695
+ function isSafePath(rawPath: string, mustBeUnder?: string): { safe: boolean; resolved: string } {
696
+ const resolved = path.resolve(rawPath);
697
+ if (rawPath.includes('..') || resolved !== path.normalize(resolved)) {
698
+ return { safe: false, resolved };
699
+ }
700
+ if (mustBeUnder) {
701
+ const base = path.resolve(mustBeUnder);
702
+ if (!resolved.startsWith(base + path.sep) && resolved !== base) {
703
+ return { safe: false, resolved };
704
+ }
705
+ }
706
+ return { safe: true, resolved };
707
+ }
708
+
709
+ // GET /api/fs/list
710
+ app.get('/api/fs/list', async (req: Request, res: Response): Promise<void> => {
711
+ let rawPath = (req.query as Record<string, string>).path;
712
+ // Normalize: missing, empty, or '~' → home dir; relative → join with home dir
713
+ if (!rawPath || rawPath === '~') {
714
+ rawPath = os.homedir();
715
+ } else if (!path.isAbsolute(rawPath)) {
716
+ rawPath = path.join(os.homedir(), rawPath);
717
+ }
718
+ const { safe, resolved } = isSafePath(rawPath);
719
+ if (!safe) {
720
+ res.status(400).json({ error: 'Invalid path — traversal not allowed' });
721
+ return;
722
+ }
723
+ try {
724
+ const names = await fs.readdir(resolved);
725
+ const capped = names.slice(0, 200);
726
+ const entries = await Promise.all(
727
+ capped.map(async (name) => {
728
+ try {
729
+ const stat = await fs.stat(path.join(resolved, name));
730
+ return {
731
+ name,
732
+ type: stat.isDirectory() ? 'dir' : 'file',
733
+ size: stat.size,
734
+ modified: stat.mtime.toISOString(),
735
+ permissions: (stat.mode & 0o777).toString(8).padStart(3, '0'),
736
+ };
737
+ } catch {
738
+ return { name, type: 'unknown', size: 0, modified: null, permissions: '000' };
739
+ }
740
+ }),
741
+ );
742
+ res.json({ entries, path: resolved });
743
+ } catch (e: unknown) {
744
+ res.status(500).json({ error: (e as Error).message });
745
+ }
746
+ });
747
+
748
+ // GET /api/fs/read
749
+ app.get('/api/fs/read', async (req: Request, res: Response): Promise<void> => {
750
+ const rawPath = (req.query as Record<string, string>).path;
751
+ if (!rawPath) {
752
+ res.status(400).json({ error: '`path` query parameter is required' });
753
+ return;
754
+ }
755
+ const { safe, resolved } = isSafePath(rawPath);
756
+ if (!safe) {
757
+ res.status(400).json({ error: 'Invalid path — traversal not allowed' });
758
+ return;
759
+ }
760
+ const ext = path.extname(resolved).toLowerCase();
761
+ if (!ALLOWED_TEXT_EXTENSIONS.has(ext)) {
762
+ res.status(400).json({ error: `File extension '${ext}' is not allowed` });
763
+ return;
764
+ }
765
+ try {
766
+ const stat = await fs.stat(resolved);
767
+ if (stat.size > 1024 * 1024) {
768
+ res.status(400).json({ error: 'File exceeds 1MB limit' });
769
+ return;
770
+ }
771
+ const content = await fs.readFile(resolved, 'utf-8');
772
+ res.json({ content, size: stat.size, encoding: 'utf-8' });
773
+ } catch (e: unknown) {
774
+ res.status(500).json({ error: (e as Error).message });
775
+ }
776
+ });
777
+
778
+ // POST /api/fs/write
779
+ app.post('/api/fs/write', async (req: Request, res: Response): Promise<void> => {
780
+ const body = req.body as { path?: string; content?: string };
781
+ if (!body.path || typeof body.path !== 'string') {
782
+ res.status(400).json({ error: '`path` is required' });
783
+ return;
784
+ }
785
+ if (typeof body.content !== 'string') {
786
+ res.status(400).json({ error: '`content` is required' });
787
+ return;
788
+ }
789
+ const { safe, resolved } = isSafePath(body.path, os.homedir());
790
+ if (!safe) {
791
+ res.status(400).json({ error: 'Invalid path — traversal above home directory not allowed' });
792
+ return;
793
+ }
794
+ const contentBytes = Buffer.byteLength(body.content, 'utf-8');
795
+ if (contentBytes > 512 * 1024) {
796
+ res.status(400).json({ error: 'Content exceeds 512KB limit' });
797
+ return;
798
+ }
799
+ try {
800
+ await fs.writeFile(resolved, body.content, 'utf-8');
801
+ res.json({ ok: true, bytesWritten: contentBytes });
802
+ } catch (e: unknown) {
803
+ res.status(500).json({ error: (e as Error).message });
804
+ }
805
+ });
806
+
807
+ // POST /api/fs/delete
808
+ app.post('/api/fs/delete', async (req: Request, res: Response): Promise<void> => {
809
+ const body = req.body as { path?: string };
810
+ if (!body.path || typeof body.path !== 'string') {
811
+ res.status(400).json({ error: '`path` is required' });
812
+ return;
813
+ }
814
+ const { safe, resolved } = isSafePath(body.path, os.homedir());
815
+ if (!safe) {
816
+ res.status(400).json({ error: 'Invalid path — traversal above home directory not allowed' });
817
+ return;
818
+ }
819
+ try {
820
+ const stat = await fs.stat(resolved);
821
+ if (stat.isDirectory()) {
822
+ await fs.rmdir(resolved); // only succeeds on empty dirs
823
+ } else {
824
+ await fs.unlink(resolved);
825
+ }
826
+ res.json({ ok: true });
827
+ } catch (e: unknown) {
828
+ res.status(500).json({ error: (e as Error).message });
829
+ }
830
+ });
831
+
832
+ // ── Enhanced Process Manager ──────────────────────────────────────────────
833
+
834
+ // GET /api/system/processes/detail
835
+ app.get('/api/system/processes/detail', async (_req: Request, res: Response): Promise<void> => {
836
+ const platform = os.platform();
837
+ const cmd = platform === 'win32' ? 'tasklist /FO CSV /NH' : 'ps aux';
838
+ const result = await runCmd(cmd);
839
+ const lines = result.stdout.split('\n').filter((l: string) => l.trim().length > 0);
840
+ const dataLines = platform === 'win32' ? lines : lines.slice(1); // skip header on unix
841
+ const processes = dataLines.slice(0, 30).map((line: string) => {
842
+ if (platform === 'win32') {
843
+ const parts = line.split(',').map((p: string) => p.replace(/"/g, '').trim());
844
+ return { pid: parseInt(parts[1] ?? '0', 10), user: 'N/A', cpu: 'N/A', mem: 'N/A', command: parts[0] ?? line };
845
+ }
846
+ // ps aux format: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
847
+ const parts = line.trim().split(/\s+/);
848
+ return {
849
+ pid: parseInt(parts[1] ?? '0', 10),
850
+ user: parts[0] ?? '',
851
+ cpu: parts[2] ?? '',
852
+ mem: parts[3] ?? '',
853
+ command: parts.slice(10).join(' '),
854
+ };
855
+ });
856
+ res.json({ processes });
857
+ });
858
+
859
+ // POST /api/system/processes/kill
860
+ app.post('/api/system/processes/kill', (req: Request, res: Response): void => {
861
+ const body = req.body as { pid?: number };
862
+ if (typeof body.pid !== 'number' || !Number.isInteger(body.pid)) {
863
+ res.status(400).json({ error: '`pid` must be an integer' });
864
+ return;
865
+ }
866
+ if (body.pid <= 1000) {
867
+ res.status(400).json({ error: 'Refusing to kill system process (pid <= 1000)' });
868
+ return;
869
+ }
870
+ try {
871
+ process.kill(body.pid, 'SIGTERM');
872
+ res.json({ ok: true });
873
+ } catch (e: unknown) {
874
+ res.status(500).json({ error: (e as Error).message });
875
+ }
876
+ });
877
+
878
+ // ── Real-time System Metrics ───────────────────────────────────────────────
879
+
880
+ // GET /api/system/metrics
881
+ app.get('/api/system/metrics', (_req: Request, res: Response): void => {
882
+ const total = os.totalmem();
883
+ const free = os.freemem();
884
+ const used = total - free;
885
+ res.json({
886
+ loadAvg: os.loadavg(),
887
+ memory: {
888
+ total,
889
+ free,
890
+ used,
891
+ usedPercent: Math.round((used / total) * 10000) / 100,
892
+ },
893
+ uptime: os.uptime(),
894
+ platform: os.platform(),
895
+ });
896
+ });
897
+
898
+ // ── Network Connections ───────────────────────────────────────────────────
899
+
900
+ // GET /api/system/network
901
+ app.get('/api/system/network', async (_req: Request, res: Response): Promise<void> => {
902
+ const platform = os.platform();
903
+ let connCmd: string;
904
+ let ifaceCmd: string;
905
+ if (platform === 'darwin') {
906
+ connCmd = 'netstat -an | grep ESTABLISHED | head -20';
907
+ ifaceCmd = 'ifconfig';
908
+ } else {
909
+ connCmd = 'ss -tuln | head -20';
910
+ ifaceCmd = 'ip addr';
911
+ }
912
+ const [connResult, ifaceResult] = await Promise.all([runCmd(connCmd), runCmd(ifaceCmd)]);
913
+ const connections = connResult.stdout
914
+ .split('\n')
915
+ .map((l: string) => l.trim())
916
+ .filter((l: string) => l.length > 0);
917
+ res.json({ connections, interfaces: ifaceResult.stdout });
918
+ });
919
+
920
+ // ── Environment Variables ─────────────────────────────────────────────────
921
+
922
+ // GET /api/system/env
923
+ app.get('/api/system/env', (_req: Request, res: Response): void => {
924
+ const SAFE_KEYS = new Set(['PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'NODE_ENV', 'PORT']);
925
+ const env: Record<string, string> = {};
926
+ for (const [k, v] of Object.entries(process.env)) {
927
+ if (v !== undefined && (SAFE_KEYS.has(k) || k.startsWith('CONDUCTOR_'))) {
928
+ env[k] = v;
929
+ }
930
+ }
931
+ res.json({ env });
932
+ });
933
+
934
+ // ── Git Status ────────────────────────────────────────────────────────────
935
+
936
+ // GET /api/git/status
937
+ app.get('/api/git/status', async (req: Request, res: Response): Promise<void> => {
938
+ const rawPath = (req.query as Record<string, string>).path;
939
+ if (!rawPath) {
940
+ res.status(400).json({ error: '`path` query parameter is required' });
941
+ return;
942
+ }
943
+ const { safe, resolved } = isSafePath(rawPath);
944
+ if (!safe) {
945
+ res.status(400).json({ error: 'Invalid path — traversal not allowed' });
946
+ return;
947
+ }
948
+ const [statusResult, logResult] = await Promise.all([
949
+ runCmd(`git -C ${JSON.stringify(resolved)} status --porcelain -b 2>&1`),
950
+ runCmd(`git -C ${JSON.stringify(resolved)} log --oneline -10 2>&1`),
951
+ ]);
952
+
953
+ const isRepo = statusResult.exitCode === 0;
954
+ let branch = '';
955
+ let statusText = statusResult.stdout;
956
+
957
+ if (isRepo) {
958
+ // First line of --porcelain -b is "## <branch>..." or "## HEAD (no branch)"
959
+ const lines = statusResult.stdout.split('\n');
960
+ const branchLine = lines[0] ?? '';
961
+ const branchMatch = branchLine.match(/^## ([^.]+)/);
962
+ branch = branchMatch ? branchMatch[1].trim() : '';
963
+ statusText = lines.slice(1).join('\n');
964
+ }
965
+
966
+ const recentCommits = isRepo ? logResult.stdout.split('\n').filter((l: string) => l.trim().length > 0) : [];
967
+
968
+ res.json({ branch, status: statusText, recentCommits, isRepo });
969
+ });
970
+
971
+ // ── Docker ────────────────────────────────────────────────────────────────
972
+
973
+ // GET /api/docker/containers
974
+ app.get('/api/docker/containers', async (_req: Request, res: Response): Promise<void> => {
975
+ const result = await runCmd('docker ps --format "{{json .}}" 2>&1');
976
+ if (
977
+ result.exitCode !== 0 ||
978
+ result.stdout.includes('command not found') ||
979
+ result.stdout.includes('Cannot connect')
980
+ ) {
981
+ res.json({ available: false, containers: [] });
982
+ return;
983
+ }
984
+ const containers = result.stdout
985
+ .split('\n')
986
+ .filter((l: string) => l.trim().startsWith('{'))
987
+ .map((l: string) => {
988
+ try {
989
+ return JSON.parse(l) as unknown;
990
+ } catch {
991
+ return null;
992
+ }
993
+ })
994
+ .filter((c): c is NonNullable<typeof c> => c !== null);
995
+ res.json({ available: true, containers });
996
+ });
997
+
998
+ // POST /api/docker/containers/:id/action
999
+ app.post('/api/docker/containers/:id/action', async (req: Request, res: Response): Promise<void> => {
1000
+ const { id } = req.params as { id: string };
1001
+ const body = req.body as { action?: string };
1002
+ const allowed = new Set(['start', 'stop', 'restart']);
1003
+ if (!body.action || !allowed.has(body.action)) {
1004
+ res.status(400).json({ error: "`action` must be 'start', 'stop', or 'restart'" });
1005
+ return;
1006
+ }
1007
+ // Validate container id: alphanumeric, dashes, underscores only
1008
+ if (!/^[a-zA-Z0-9_\-]+$/.test(id)) {
1009
+ res.status(400).json({ error: 'Invalid container id' });
1010
+ return;
1011
+ }
1012
+ const result = await runCmd(`docker ${body.action} ${id} 2>&1`);
1013
+ if (result.exitCode !== 0) {
1014
+ res.status(500).json({ error: result.stdout || result.stderr });
1015
+ return;
1016
+ }
1017
+ res.json({ ok: true });
1018
+ });
1019
+
1020
+ // ── Notes & Memory ────────────────────────────────────────────────────────
1021
+
1022
+ interface Note {
1023
+ id: string;
1024
+ title: string;
1025
+ content: string;
1026
+ created: string;
1027
+ updated: string;
1028
+ }
1029
+
1030
+ const notesDir = path.join(os.homedir(), '.conductor');
1031
+ const notesFile = path.join(notesDir, 'notes.json');
1032
+
1033
+ async function readNotes(): Promise<Note[]> {
1034
+ try {
1035
+ const raw = await fs.readFile(notesFile, 'utf-8');
1036
+ return JSON.parse(raw) as Note[];
1037
+ } catch {
1038
+ return [];
1039
+ }
1040
+ }
1041
+
1042
+ async function writeNotes(notes: Note[]): Promise<void> {
1043
+ await fs.mkdir(notesDir, { recursive: true });
1044
+ await fs.writeFile(notesFile, JSON.stringify(notes, null, 2), 'utf-8');
1045
+ }
1046
+
1047
+ // GET /api/notes
1048
+ app.get('/api/notes', async (_req: Request, res: Response): Promise<void> => {
1049
+ const notes = await readNotes();
1050
+ res.json({ notes });
1051
+ });
1052
+
1053
+ // POST /api/notes
1054
+ app.post('/api/notes', async (req: Request, res: Response): Promise<void> => {
1055
+ const body = req.body as { title?: string; content?: string };
1056
+ if (typeof body.title !== 'string' || body.title.trim() === '') {
1057
+ res.status(400).json({ error: '`title` is required' });
1058
+ return;
1059
+ }
1060
+ if (typeof body.content !== 'string') {
1061
+ res.status(400).json({ error: '`content` is required' });
1062
+ return;
1063
+ }
1064
+ const notes = await readNotes();
1065
+ const now = new Date().toISOString();
1066
+ const note: Note = {
1067
+ id: crypto.randomBytes(8).toString('hex'),
1068
+ title: body.title.trim(),
1069
+ content: body.content,
1070
+ created: now,
1071
+ updated: now,
1072
+ };
1073
+ notes.push(note);
1074
+ await writeNotes(notes);
1075
+ res.json({ ok: true, note });
1076
+ });
1077
+
1078
+ // PUT /api/notes/:id — update title and/or content
1079
+ app.put('/api/notes/:id', async (req: Request, res: Response): Promise<void> => {
1080
+ const { id } = req.params as { id: string };
1081
+ const body = req.body as { title?: string; content?: string };
1082
+ const notes = await readNotes();
1083
+ const idx = notes.findIndex((n) => n.id === id);
1084
+ if (idx === -1) {
1085
+ res.status(404).json({ error: 'Note not found' });
1086
+ return;
1087
+ }
1088
+ if (typeof body.title === 'string') notes[idx].title = body.title.trim() || notes[idx].title;
1089
+ if (typeof body.content === 'string') notes[idx].content = body.content;
1090
+ notes[idx].updated = new Date().toISOString();
1091
+ await writeNotes(notes);
1092
+ res.json({ ok: true, note: notes[idx] });
1093
+ });
1094
+
1095
+ // DELETE /api/notes/:id
1096
+ app.delete('/api/notes/:id', async (req: Request, res: Response): Promise<void> => {
1097
+ const { id } = req.params as { id: string };
1098
+ const notes = await readNotes();
1099
+ const filtered = notes.filter((n) => n.id !== id);
1100
+ if (filtered.length === notes.length) {
1101
+ res.status(404).json({ error: 'Note not found' });
1102
+ return;
1103
+ }
1104
+ await writeNotes(filtered);
1105
+ res.json({ ok: true });
1106
+ });
1107
+
1108
+ // ── Cron Jobs ─────────────────────────────────────────────────────────────
1109
+
1110
+ // GET /api/cron
1111
+ app.get('/api/cron', async (_req: Request, res: Response): Promise<void> => {
1112
+ const result = await runCmd('crontab -l 2>/dev/null');
1113
+ const raw = result.stdout;
1114
+ const entries = raw
1115
+ .split('\n')
1116
+ .map((l: string) => l.trim())
1117
+ .filter((l: string) => l.length > 0 && !l.startsWith('#'));
1118
+ res.json({ entries, raw });
1119
+ });
1120
+
1121
+ // ── Todoist proxy ─────────────────────────────────────────────────────────
1122
+ //
1123
+ // All routes forward to https://api.todoist.com/api/v1 using the token
1124
+ // stored in the keychain. The token is never sent to the browser.
1125
+
1126
+ async function todoistProxy(
1127
+ token: string,
1128
+ path: string,
1129
+ options: RequestInit = {},
1130
+ ): Promise<{ status: number; body: unknown }> {
1131
+ let todoistRes: globalThis.Response;
1132
+ try {
1133
+ const url = `https://api.todoist.com/api/v1${path}`;
1134
+ const { headers: extraHeaders, ...restOptions } = options;
1135
+ todoistRes = await fetch(url, {
1136
+ ...restOptions,
1137
+ headers: {
1138
+ Authorization: `Bearer ${token}`,
1139
+ 'Content-Type': 'application/json',
1140
+ ...((extraHeaders as Record<string, string> | undefined) ?? {}),
1141
+ },
1142
+ });
1143
+ } catch (fetchErr: unknown) {
1144
+ const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
1145
+ console.error('[todoist-proxy] fetch error:', msg);
1146
+ return { status: 502, body: { error: `Failed to reach Todoist: ${msg}` } };
1147
+ }
1148
+
1149
+ if (todoistRes.status === 204) {
1150
+ return { status: 200, body: { ok: true } };
1151
+ }
1152
+
1153
+ const rawText = await todoistRes.text().catch(() => '');
1154
+
1155
+ if (!todoistRes.ok) {
1156
+ console.error(`[todoist-proxy] Todoist ${todoistRes.status} for ${path}:`, rawText.slice(0, 200));
1157
+ return {
1158
+ status: todoistRes.status,
1159
+ body: { error: `Todoist error ${todoistRes.status}: ${rawText.slice(0, 120)}` },
1160
+ };
1161
+ }
1162
+
1163
+ if (!rawText) return { status: 200, body: [] };
1164
+
1165
+ try {
1166
+ const data = JSON.parse(rawText);
1167
+ // API v1 wraps list responses in { results: [], next_cursor }
1168
+ if (data !== null && typeof data === 'object' && Array.isArray((data as Record<string, unknown>).results)) {
1169
+ return { status: 200, body: (data as Record<string, unknown>).results };
1170
+ }
1171
+ return { status: 200, body: data };
1172
+ } catch {
1173
+ console.error('[todoist-proxy] JSON parse failed, raw:', rawText.slice(0, 200));
1174
+ return { status: 502, body: { error: `Todoist returned non-JSON: ${rawText.slice(0, 80)}` } };
1175
+ }
1176
+ }
1177
+
1178
+ // GET /api/todoist/status
1179
+ app.get('/api/todoist/status', async (_req: Request, res: Response): Promise<void> => {
1180
+ const configured = await keychain.has('todoist', 'api_token');
1181
+ res.json({ configured });
1182
+ });
1183
+
1184
+ // GET /api/todoist/projects
1185
+ app.get('/api/todoist/projects', async (_req: Request, res: Response): Promise<void> => {
1186
+ const token = await keychain.get('todoist', 'api_token');
1187
+ if (!token) {
1188
+ res.status(400).json({ error: 'Todoist not configured' });
1189
+ return;
1190
+ }
1191
+
1192
+ const { status, body } = await todoistProxy(token, '/projects');
1193
+ res.status(status).json(body);
1194
+ });
1195
+
1196
+ // GET /api/todoist/tasks — supports ?project_id, ?label, ?filter
1197
+ app.get('/api/todoist/tasks', async (req: Request, res: Response): Promise<void> => {
1198
+ const token = await keychain.get('todoist', 'api_token');
1199
+ if (!token) {
1200
+ res.status(400).json({ error: 'Todoist not configured' });
1201
+ return;
1202
+ }
1203
+
1204
+ const query = req.query as Record<string, string>;
1205
+ const params = new URLSearchParams();
1206
+ for (const key of ['project_id', 'label', 'filter'] as const) {
1207
+ if (query[key]) params.set(key, query[key]);
1208
+ }
1209
+ const qs = params.toString() ? `?${params.toString()}` : '';
1210
+
1211
+ const { status, body } = await todoistProxy(token, `/tasks${qs}`);
1212
+ res.status(status).json(body);
1213
+ });
1214
+
1215
+ // POST /api/todoist/tasks — create a task
1216
+ app.post('/api/todoist/tasks', async (req: Request, res: Response): Promise<void> => {
1217
+ const token = await keychain.get('todoist', 'api_token');
1218
+ if (!token) {
1219
+ res.status(400).json({ error: 'Todoist not configured' });
1220
+ return;
1221
+ }
1222
+
1223
+ const { status, body } = await todoistProxy(token, '/tasks', {
1224
+ method: 'POST',
1225
+ body: JSON.stringify(req.body),
1226
+ });
1227
+ res.status(status).json(body);
1228
+ });
1229
+
1230
+ // POST /api/todoist/tasks/:id — update a task
1231
+ app.post('/api/todoist/tasks/:id', async (req: Request, res: Response): Promise<void> => {
1232
+ const token = await keychain.get('todoist', 'api_token');
1233
+ if (!token) {
1234
+ res.status(400).json({ error: 'Todoist not configured' });
1235
+ return;
1236
+ }
1237
+
1238
+ const { id } = req.params as { id: string };
1239
+ const { status, body } = await todoistProxy(token, `/tasks/${id}`, {
1240
+ method: 'POST',
1241
+ body: JSON.stringify(req.body),
1242
+ });
1243
+ res.status(status).json(body);
1244
+ });
1245
+
1246
+ // POST /api/todoist/tasks/:id/close — complete a task
1247
+ app.post('/api/todoist/tasks/:id/close', async (req: Request, res: Response): Promise<void> => {
1248
+ const token = await keychain.get('todoist', 'api_token');
1249
+ if (!token) {
1250
+ res.status(400).json({ error: 'Todoist not configured' });
1251
+ return;
1252
+ }
1253
+
1254
+ const { id } = req.params as { id: string };
1255
+ const { status, body } = await todoistProxy(token, `/tasks/${id}/close`, {
1256
+ method: 'POST',
1257
+ });
1258
+ // Todoist returns 204 on success — normalise to a JSON ok response for the
1259
+ // frontend so it doesn't have to special-case empty bodies.
1260
+ if (status === 204) {
1261
+ res.json({ ok: true });
1262
+ return;
1263
+ }
1264
+ res.status(status).json(body);
1265
+ });
1266
+
1267
+ // DELETE /api/todoist/tasks/:id — delete a task
1268
+ app.delete('/api/todoist/tasks/:id', async (req: Request, res: Response): Promise<void> => {
1269
+ const token = await keychain.get('todoist', 'api_token');
1270
+ if (!token) {
1271
+ res.status(400).json({ error: 'Todoist not configured' });
1272
+ return;
1273
+ }
1274
+
1275
+ const { id } = req.params as { id: string };
1276
+ const { status, body } = await todoistProxy(token, `/tasks/${id}`, {
1277
+ method: 'DELETE',
1278
+ });
1279
+ if (status === 204) {
1280
+ res.json({ ok: true });
1281
+ return;
1282
+ }
1283
+ res.status(status).json(body);
1284
+ });
1285
+
1286
+ // ── Conversations ─────────────────────────────────────────────────────────
1287
+ app.get('/api/conversations', async (_req: Request, res: Response): Promise<void> => {
1288
+ try {
1289
+ const messages = await db.getRecentMessages(200);
1290
+ res.json({ messages: messages.reverse() });
1291
+ } catch {
1292
+ res.json({ messages: [] });
1293
+ }
1294
+ });
1295
+
1296
+ // ── Live Log Stream (SSE) ─────────────────────────────────────────────────
1297
+ app.get('/api/logs/stream', (req: Request, res: Response): void => {
1298
+ res.setHeader('Content-Type', 'text/event-stream');
1299
+ res.setHeader('Cache-Control', 'no-cache');
1300
+ res.setHeader('Connection', 'keep-alive');
1301
+ res.flushHeaders();
1302
+
1303
+ // Send buffered logs first
1304
+ for (const entry of logBuffer) {
1305
+ res.write(`data: ${JSON.stringify(entry)}\n\n`);
1306
+ }
1307
+
1308
+ sseClients.add(res);
1309
+
1310
+ req.on('close', () => {
1311
+ sseClients.delete(res);
1312
+ });
1313
+ });
1314
+
1315
+ // ── Chat (AI) ─────────────────────────────────────────────────────────────
1316
+
1317
+ const PLUGIN_CATALOG: Record<string, { desc: string; category: string; requiresAuth: boolean; authLabel?: string }> =
1318
+ {
1319
+ calculator: {
1320
+ desc: 'Evaluate mathematical expressions and unit conversions',
1321
+ category: 'Utilities',
1322
+ requiresAuth: false,
1323
+ },
1324
+ colors: {
1325
+ desc: 'Convert and manipulate colors (hex, rgb, hsl, name)',
1326
+ category: 'Utilities',
1327
+ requiresAuth: false,
1328
+ },
1329
+ cron: { desc: 'Manage and inspect system cron jobs', category: 'System', requiresAuth: false },
1330
+ crypto: { desc: 'Encrypt, decrypt, and generate cryptographic keys', category: 'Security', requiresAuth: false },
1331
+ fun: { desc: 'Jokes, trivia, dice rolls, and random fun', category: 'Utilities', requiresAuth: false },
1332
+ gcal: {
1333
+ desc: 'Read, create, and manage Google Calendar events',
1334
+ category: 'Google',
1335
+ requiresAuth: true,
1336
+ authLabel: 'Google OAuth',
1337
+ },
1338
+ gdrive: {
1339
+ desc: 'List, read, and upload files to Google Drive',
1340
+ category: 'Google',
1341
+ requiresAuth: true,
1342
+ authLabel: 'Google OAuth',
1343
+ },
1344
+ github: {
1345
+ desc: 'Manage repos, issues, PRs, and gists on GitHub',
1346
+ category: 'Developer',
1347
+ requiresAuth: true,
1348
+ authLabel: 'GitHub Token',
1349
+ },
1350
+ 'github-actions': {
1351
+ desc: 'Trigger and monitor GitHub Actions CI/CD workflows',
1352
+ category: 'Developer',
1353
+ requiresAuth: true,
1354
+ authLabel: 'GitHub Token',
1355
+ },
1356
+ gmail: {
1357
+ desc: 'Read, search, send, and label Gmail messages',
1358
+ category: 'Google',
1359
+ requiresAuth: true,
1360
+ authLabel: 'Google OAuth',
1361
+ },
1362
+ hash: { desc: 'Compute MD5, SHA-1, SHA-256, and bcrypt hashes', category: 'Security', requiresAuth: false },
1363
+ homekit: {
1364
+ desc: 'Control HomeKit smart home devices and accessories',
1365
+ category: 'Smart Home',
1366
+ requiresAuth: true,
1367
+ authLabel: 'HomeKit Bridge URL',
1368
+ },
1369
+ memory: { desc: 'Store and recall information across conversations', category: 'AI', requiresAuth: false },
1370
+ n8n: {
1371
+ desc: 'Trigger n8n automation workflows via webhook',
1372
+ category: 'Automation',
1373
+ requiresAuth: true,
1374
+ authLabel: 'n8n API Key',
1375
+ },
1376
+ network: { desc: 'DNS lookup, ping, port scan, and IP geolocation', category: 'System', requiresAuth: false },
1377
+ notes: { desc: 'Create, read, update, and delete personal notes', category: 'Productivity', requiresAuth: false },
1378
+ notion: {
1379
+ desc: 'Query, create, and update Notion databases and pages',
1380
+ category: 'Productivity',
1381
+ requiresAuth: true,
1382
+ authLabel: 'Notion API Key',
1383
+ },
1384
+ slack: {
1385
+ desc: 'Send messages and read channels in Slack workspaces',
1386
+ category: 'Communication',
1387
+ requiresAuth: true,
1388
+ authLabel: 'Slack Bot Token',
1389
+ },
1390
+ spotify: {
1391
+ desc: 'Control Spotify playback and browse music catalog',
1392
+ category: 'Entertainment',
1393
+ requiresAuth: true,
1394
+ authLabel: 'Spotify OAuth',
1395
+ },
1396
+ system: {
1397
+ desc: 'CPU, memory, disk stats, processes, and shell commands',
1398
+ category: 'System',
1399
+ requiresAuth: false,
1400
+ },
1401
+ 'text-tools': {
1402
+ desc: 'Transform text: case, trim, word count, slugify, base64',
1403
+ category: 'Utilities',
1404
+ requiresAuth: false,
1405
+ },
1406
+ timezone: { desc: 'Convert times between timezones worldwide', category: 'Utilities', requiresAuth: false },
1407
+ todoist: {
1408
+ desc: 'Manage Todoist tasks, projects, and priorities',
1409
+ category: 'Productivity',
1410
+ requiresAuth: true,
1411
+ authLabel: 'Todoist API Token',
1412
+ },
1413
+ translate: { desc: 'Translate text between 100+ languages', category: 'Utilities', requiresAuth: false },
1414
+ 'url-tools': {
1415
+ desc: 'Parse, encode, decode, and expand shortened URLs',
1416
+ category: 'Utilities',
1417
+ requiresAuth: false,
1418
+ },
1419
+ vercel: {
1420
+ desc: 'Manage Vercel deployments, projects, and domains',
1421
+ category: 'Developer',
1422
+ requiresAuth: true,
1423
+ authLabel: 'Vercel Token',
1424
+ },
1425
+ weather: {
1426
+ desc: 'Current conditions and forecasts for any location',
1427
+ category: 'Utilities',
1428
+ requiresAuth: true,
1429
+ authLabel: 'Weather API Key',
1430
+ },
1431
+ x: {
1432
+ desc: 'Post tweets and read your X/Twitter timeline',
1433
+ category: 'Social',
1434
+ requiresAuth: true,
1435
+ authLabel: 'X API Key',
1436
+ },
1437
+ };
1438
+
1439
+ let _chatConductorInstance: Conductor | null = conductorInstance ?? null;
1440
+
1441
+ async function getChatConductor(): Promise<Conductor> {
1442
+ if (_chatConductorInstance) return _chatConductorInstance;
1443
+ const { Conductor: ConductorClass } = await import('../core/conductor.js');
1444
+ _chatConductorInstance = new ConductorClass(undefined, { quiet: true });
1445
+ await _chatConductorInstance.initialize();
1446
+ return _chatConductorInstance;
1447
+ }
1448
+
1449
+ // POST /api/chat
1450
+ app.post('/api/chat', async (req: Request, res: Response): Promise<void> => {
1451
+ const body = req.body as { message?: string; userId?: string };
1452
+ if (!body.message || typeof body.message !== 'string' || body.message.trim() === '') {
1453
+ res.status(400).json({ error: '`message` is required' });
1454
+ return;
1455
+ }
1456
+
1457
+ const userId = body.userId && typeof body.userId === 'string' ? body.userId.trim() : 'dashboard-user';
1458
+
1459
+ try {
1460
+ const c = await getChatConductor();
1461
+ const ai = c.getAIManager();
1462
+
1463
+ const result = await ai.handleConversation(userId, body.message.trim());
1464
+
1465
+ // Extract tool calls from the current turn: walk backwards from the end,
1466
+ // collect tool messages until we hit the user message we just added.
1467
+ const history = await c.getDatabase().getHistory(userId, 60);
1468
+ const toolCalls: Array<{ tool: string; success: boolean }> = [];
1469
+ for (let i = history.length - 1; i >= 0; i--) {
1470
+ const msg = history[i];
1471
+ if (msg.role === 'user') break; // stop at the user message for this turn
1472
+ if (msg.role === 'tool' && msg.name) {
1473
+ toolCalls.unshift({ tool: msg.name, success: !String(msg.content ?? '').startsWith('Error') });
1474
+ }
1475
+ }
1476
+
1477
+ const providerName = c.getConfig().get<string>('ai.provider') ?? 'unknown';
1478
+ const modelName = c.getConfig().get<string>('ai.model') ?? '';
1479
+
1480
+ res.json({
1481
+ response: result.text,
1482
+ toolCalls,
1483
+ approvalRequired: result.approvalRequired ?? null,
1484
+ provider: providerName,
1485
+ model: modelName,
1486
+ });
1487
+ } catch (e: unknown) {
1488
+ res.status(500).json({ error: (e as Error).message });
1489
+ }
1490
+ });
1491
+
1492
+ // GET /api/chat/history
1493
+ app.get('/api/chat/history', async (req: Request, res: Response): Promise<void> => {
1494
+ const userId = ((req.query as Record<string, string>).userId ?? 'dashboard-user').trim();
1495
+ try {
1496
+ const c = await getChatConductor();
1497
+ const messages = await c.getDatabase().getHistory(userId, 100);
1498
+ res.json({ messages });
1499
+ } catch (e: unknown) {
1500
+ res.status(500).json({ error: (e as Error).message });
1501
+ }
1502
+ });
1503
+
1504
+ // DELETE /api/chat/history
1505
+ app.delete('/api/chat/history', async (req: Request, res: Response): Promise<void> => {
1506
+ const userId = ((req.query as Record<string, string>).userId ?? 'dashboard-user').trim();
1507
+ try {
1508
+ const c = await getChatConductor();
1509
+ await c.getDatabase().clearHistory(userId);
1510
+ res.json({ ok: true });
1511
+ } catch (e: unknown) {
1512
+ res.status(500).json({ error: (e as Error).message });
1513
+ }
1514
+ });
1515
+
1516
+ // GET /api/marketplace
1517
+ app.get('/api/marketplace', (_req: Request, res: Response): void => {
1518
+ const installedPlugins = config.get<string[]>('plugins.installed') ?? [];
1519
+ const enabledPlugins = config.get<string[]>('plugins.enabled') ?? [];
1520
+
1521
+ const plugins = Object.entries(PLUGIN_CATALOG).map(([name, meta]) => ({
1522
+ name,
1523
+ ...meta,
1524
+ installed: installedPlugins.includes(name) || ALL_PLUGINS.includes(name as never),
1525
+ enabled: enabledPlugins.includes(name),
1526
+ requiredCreds: PLUGIN_REQUIRED_CREDS[name] ?? [],
1527
+ }));
1528
+
1529
+ res.json({ plugins });
1530
+ });
1531
+
1532
+ // ── Start — bind to 127.0.0.1 only (not 0.0.0.0) ────────────────────────
1533
+ return new Promise<DashboardServer>((resolve, reject) => {
1534
+ const server = app.listen(port, '127.0.0.1', () => {
1535
+ process.stderr.write(`Dashboard running at http://127.0.0.1:${port}\n`);
1536
+ process.stderr.write(`Dashboard token stored at ${path.join(config.getConfigDir(), 'dashboard.token')}\n`);
1537
+ resolve({
1538
+ port,
1539
+ close: (): Promise<void> => new Promise<void>((res, rej) => server.close((err) => (err ? rej(err) : res()))),
1540
+ });
1541
+ });
1542
+ server.on('error', reject);
1543
+ });
1544
+ }