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