mulmoclaude 0.1.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 (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Standalone MCP stdio server — spawned by the Claude CLI via --mcp-config.
3
+ * Bridges Claude's tool calls to our server endpoints and pushes ToolResults
4
+ * back to the active frontend SSE stream via the session registry.
5
+ */
6
+
7
+ import type { ToolDefinition } from "gui-chat-protocol";
8
+ import { mcpTools, isMcpToolEnabled } from "./mcp-tools/index.js";
9
+ import { TOOL_ENDPOINTS, PLUGIN_DEFS } from "./plugin-names.js";
10
+ import { errorMessage } from "../utils/errors.js";
11
+ import { isNonEmptyString, isRecord } from "../utils/types.js";
12
+ import { API_ROUTES } from "../../src/config/apiRoutes.js";
13
+ import { env } from "../system/env.js";
14
+ import { extractFetchError } from "../utils/fetch.js";
15
+ import { safeResponseText } from "../utils/http.js";
16
+ import { readTextSafeSync } from "../utils/files/safe.js";
17
+ import { WORKSPACE_PATHS } from "../workspace/paths.js";
18
+
19
+ type JsonRpcId = string | number | null;
20
+
21
+ interface ToolCallParams {
22
+ name: string;
23
+ arguments?: Record<string, unknown>;
24
+ }
25
+
26
+ interface JsonRpcMessage {
27
+ jsonrpc: string;
28
+ id?: JsonRpcId;
29
+ method: string;
30
+ params?: ToolCallParams;
31
+ }
32
+
33
+ const isJsonRpcMessage = (v: unknown): v is JsonRpcMessage => isRecord(v) && "method" in v;
34
+
35
+ const SESSION_ID = env.mcpSessionId;
36
+ const PORT = env.port;
37
+ const PLUGIN_NAMES = env.mcpPluginNames;
38
+ const ROLE_IDS = env.mcpRoleIds;
39
+ const MCP_HOST = env.mcpHost;
40
+ const BASE_URL = `http://${MCP_HOST}:${PORT}`;
41
+
42
+ // Bearer token for /api/* calls back to the parent server (#272).
43
+ // The parent writes it to <workspace>/.session-token at startup; we
44
+ // read once at module load — the token is immutable for the server's
45
+ // lifetime. Same resolution order as bridges/cli/token.ts.
46
+ function readSessionToken(): string {
47
+ const fromEnv = process.env.MULMOCLAUDE_AUTH_TOKEN;
48
+ if (isNonEmptyString(fromEnv)) return fromEnv;
49
+ return readTextSafeSync(WORKSPACE_PATHS.sessionToken)?.trim() ?? "";
50
+ }
51
+ const SESSION_TOKEN = readSessionToken();
52
+ const AUTH_HEADER: Record<string, string> = SESSION_TOKEN ? { Authorization: `Bearer ${SESSION_TOKEN}` } : {};
53
+
54
+ interface ToolDef {
55
+ name: string;
56
+ description: string;
57
+ inputSchema: object;
58
+ endpoint?: string; // absent for tools handled specially (e.g. switchRole)
59
+ }
60
+
61
+ // Combine `description` (one-liner) and `prompt` (detailed usage
62
+ // instructions) into the MCP tool description so Claude CLI sees
63
+ // both. The MCP protocol only has `description` — there's no
64
+ // `prompt` field — so the prompt content must ride along in the
65
+ // description string. The gui-chat-protocol ToolDefinition carries
66
+ // `prompt` separately because the Vue client uses it for different
67
+ // purposes, but the CLI needs it in-band.
68
+ function fromPackage(def: ToolDefinition, endpoint: string): ToolDef {
69
+ const parts = [def.description];
70
+ if (typeof def.prompt === "string" && def.prompt.length > 0) {
71
+ parts.push(def.prompt);
72
+ }
73
+ return {
74
+ name: def.name,
75
+ description: parts.join("\n\n"),
76
+ inputSchema: def.parameters ?? {},
77
+ endpoint,
78
+ };
79
+ }
80
+
81
+ // Pure MCP tools (no GUI) — auto-registered from server/mcp-tools/
82
+ const mcpToolDefs: Record<string, ToolDef> = Object.fromEntries(
83
+ mcpTools.filter(isMcpToolEnabled).map((t) => [
84
+ t.definition.name,
85
+ {
86
+ name: t.definition.name,
87
+ description: t.definition.description,
88
+ inputSchema: t.definition.inputSchema,
89
+ },
90
+ ]),
91
+ );
92
+
93
+ const ALL_TOOLS: Record<string, ToolDef> = {
94
+ ...mcpToolDefs,
95
+ ...Object.fromEntries(PLUGIN_DEFS.map((def) => [def.name, fromPackage(def, TOOL_ENDPOINTS[def.name])])),
96
+ switchRole: {
97
+ name: "switchRole",
98
+ description: "Switch to a different AI role, resetting the conversation context. Use when the user's request is better served by another role.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ roleId: {
103
+ type: "string",
104
+ enum: ROLE_IDS,
105
+ description: "The ID of the role to switch to.",
106
+ },
107
+ },
108
+ required: ["roleId"],
109
+ },
110
+ },
111
+ };
112
+
113
+ const tools = PLUGIN_NAMES.map((name) => ALL_TOOLS[name]).filter(Boolean);
114
+
115
+ function respond(msg: unknown): void {
116
+ process.stdout.write(JSON.stringify(msg) + "\n");
117
+ }
118
+
119
+ // All bridge calls go to the same backend on the same session, so
120
+ // every fetch was duplicating the same headers, method, and
121
+ // stringify boilerplate. `postJson` captures BASE_URL + SESSION_ID
122
+ // once and lets handleToolCall focus on what it's calling, not how.
123
+ //
124
+ // `path` is the absolute server path (e.g. /api/internal/tool-result)
125
+ // — the session query string is appended automatically.
126
+ //
127
+ // Both network errors and HTTP failures (4xx/5xx) are converted into
128
+ // a descriptive Error by default, so the outer catch in handleToolCall
129
+ // reports them as the failed tool call instead of a silent success.
130
+ // Pass `allowHttpError: true` for callers that want to inspect the
131
+ // response themselves (e.g. /api/mcp-tools/* which has its own
132
+ // status-aware result handling).
133
+ interface PostJsonOpts {
134
+ allowHttpError?: boolean;
135
+ }
136
+
137
+ async function postJson(path: string, body: unknown, opts: PostJsonOpts = {}): Promise<Response> {
138
+ // SESSION_ID comes from the parent process env so it's effectively
139
+ // trusted, but encode it anyway — defense in depth against future
140
+ // callers passing unexpected characters (`&`, `#`, newlines, etc.).
141
+ // The path arg is used as-is because all current call sites pass
142
+ // hardcoded literals.
143
+ let res: Response;
144
+ try {
145
+ res = await fetch(`${BASE_URL}${path}?session=${encodeURIComponent(SESSION_ID)}`, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json", ...AUTH_HEADER },
148
+ body: JSON.stringify(body),
149
+ });
150
+ } catch (err) {
151
+ throw new Error(`Network error calling ${path}: ${errorMessage(err)}`);
152
+ }
153
+ if (!opts.allowHttpError && !res.ok) {
154
+ const errBody = await safeResponseText(res, 500);
155
+ const detail = errBody ? `: ${errBody}` : "";
156
+ throw new Error(`HTTP ${res.status} calling ${path}${detail}`);
157
+ }
158
+ return res;
159
+ }
160
+
161
+ // Bridge for the manageSkills tool. Routes by `action`:
162
+ // - "list" (default): GET /api/skills, push the list as a ToolResult
163
+ // - "save" : POST /api/skills with { name, description, body }
164
+ // - "delete" : DELETE /api/skills/:name
165
+ // In every case, after a successful mutation we re-fetch the list and
166
+ // push it so the canvas reflects the new state immediately.
167
+ async function handleManageSkills(args: Record<string, unknown>): Promise<string> {
168
+ const action = typeof args.action === "string" ? args.action : "list";
169
+ if (action === "save") return handleManageSkillsSave(args);
170
+ if (action === "update") return handleManageSkillsUpdate(args);
171
+ if (action === "delete") return handleManageSkillsDelete(args);
172
+ return handleManageSkillsList();
173
+ }
174
+
175
+ async function fetchSkillsList(): Promise<{ name: string }[]> {
176
+ const url = `${BASE_URL}/api/skills?session=${encodeURIComponent(SESSION_ID)}`;
177
+ let res: Response;
178
+ try {
179
+ res = await fetch(url, { headers: AUTH_HEADER });
180
+ } catch (err) {
181
+ throw new Error(`Network error calling /api/skills: ${errorMessage(err)}`);
182
+ }
183
+ if (!res.ok) {
184
+ const body = await safeResponseText(res);
185
+ throw new Error(`HTTP ${res.status} calling /api/skills: ${body}`);
186
+ }
187
+ const body: { skills: { name: string }[] } = await res.json();
188
+ return body.skills;
189
+ }
190
+
191
+ async function pushSkillsListResult(message: string): Promise<void> {
192
+ const skills = await fetchSkillsList();
193
+ await postJson(API_ROUTES.agent.internal.toolResult, {
194
+ toolName: "manageSkills",
195
+ uuid: crypto.randomUUID(),
196
+ title: "Skills",
197
+ message,
198
+ data: { skills },
199
+ });
200
+ }
201
+
202
+ async function handleManageSkillsList(): Promise<string> {
203
+ const skills = await fetchSkillsList();
204
+ const suffix = skills.length === 1 ? "" : "s";
205
+ await postJson(API_ROUTES.agent.internal.toolResult, {
206
+ toolName: "manageSkills",
207
+ uuid: crypto.randomUUID(),
208
+ title: "Skills",
209
+ message: `Found ${skills.length} skill${suffix}.`,
210
+ data: { skills },
211
+ });
212
+ return `Listed ${skills.length} skill${suffix}`;
213
+ }
214
+
215
+ async function handleManageSkillsSave(args: Record<string, unknown>): Promise<string> {
216
+ // Normalize name once up front so log / result messages below never
217
+ // interpolate an accidental object / number into `/${name}`.
218
+ const name = String(args.name ?? "");
219
+ const res = await postJson(
220
+ API_ROUTES.skills.create,
221
+ {
222
+ name,
223
+ description: args.description,
224
+ body: args.body,
225
+ },
226
+ { allowHttpError: true },
227
+ );
228
+ if (!res.ok) {
229
+ return "Error: " + (await extractFetchError(res));
230
+ }
231
+ await pushSkillsListResult(`Saved skill "${name}".`);
232
+ return `Saved skill ${name}. Run with /${name}.`;
233
+ }
234
+
235
+ async function handleManageSkillsUpdate(args: Record<string, unknown>): Promise<string> {
236
+ const name = String(args.name ?? "");
237
+ const url = `${BASE_URL}/api/skills/${encodeURIComponent(name)}?session=${encodeURIComponent(SESSION_ID)}`;
238
+ let res: Response;
239
+ try {
240
+ res = await fetch(url, {
241
+ method: "PUT",
242
+ headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
243
+ body: JSON.stringify({
244
+ description: args.description,
245
+ body: args.body,
246
+ }),
247
+ });
248
+ } catch (err) {
249
+ throw new Error(`Network error calling PUT /api/skills/${name}: ${errorMessage(err)}`);
250
+ }
251
+ if (!res.ok) {
252
+ return "Error: " + (await extractFetchError(res));
253
+ }
254
+ await pushSkillsListResult(`Updated skill "${name}".`);
255
+ return `Updated skill ${name}. The changes take effect in new sessions.`;
256
+ }
257
+
258
+ async function handleManageSkillsDelete(args: Record<string, unknown>): Promise<string> {
259
+ const name = String(args.name ?? "");
260
+ const url = `/api/skills/${encodeURIComponent(name)}?session=${encodeURIComponent(SESSION_ID)}`;
261
+ let res: Response;
262
+ try {
263
+ res = await fetch(`${BASE_URL}${url}`, {
264
+ method: "DELETE",
265
+ headers: AUTH_HEADER,
266
+ });
267
+ } catch (err) {
268
+ throw new Error(`Network error calling DELETE ${url}: ${errorMessage(err)}`);
269
+ }
270
+ if (!res.ok) {
271
+ return "Error: " + (await extractFetchError(res));
272
+ }
273
+ await pushSkillsListResult(`Deleted skill "${name}".`);
274
+ return `Deleted skill ${name}.`;
275
+ }
276
+
277
+ async function handleToolCall(name: string, args: Record<string, unknown>): Promise<string> {
278
+ if (name === "switchRole") {
279
+ await postJson(API_ROUTES.agent.internal.switchRole, {
280
+ roleId: args.roleId,
281
+ });
282
+ return `Switching to ${args.roleId} role`;
283
+ }
284
+
285
+ if (name === "manageRoles") {
286
+ const res = await postJson(API_ROUTES.roles.manage, args);
287
+ const result = await res.json();
288
+
289
+ // For the list action, push a visual canvas result so the viewer renders
290
+ if (args.action === "list" && result.success) {
291
+ await postJson(API_ROUTES.agent.internal.toolResult, {
292
+ toolName: "manageRoles",
293
+ uuid: crypto.randomUUID(),
294
+ ...result,
295
+ });
296
+ }
297
+
298
+ return result.message ?? (result.error ? `Error: ${result.error}` : "Done");
299
+ }
300
+
301
+ if (name === "manageSkills") return handleManageSkills(args);
302
+
303
+ // Pure MCP tools — call via /api/mcp-tools/:tool, return text directly
304
+ // (no frontend push). Opt out of postJson's HTTP error throw because
305
+ // we want to surface the JSON error body to the caller as a string.
306
+ const mcpTool = mcpTools.find((t) => t.definition.name === name);
307
+ if (mcpTool) {
308
+ const res = await postJson(`/api/mcp-tools/${name}`, args, {
309
+ allowHttpError: true,
310
+ });
311
+ const json = await res.json();
312
+ if (!res.ok) return `Error: ${json.error ?? res.status}`;
313
+ return typeof json.result === "string" ? json.result : JSON.stringify(json.result);
314
+ }
315
+
316
+ const tool = tools.find((t) => t.name === name);
317
+ if (!tool) throw new Error(`Unknown tool: ${name}`);
318
+
319
+ const res = await postJson(tool.endpoint!, args);
320
+ const result = await res.json();
321
+
322
+ // Push visual ToolResult to the frontend via the session
323
+ await postJson(API_ROUTES.agent.internal.toolResult, {
324
+ toolName: name,
325
+ uuid: crypto.randomUUID(),
326
+ ...result,
327
+ });
328
+
329
+ const parts = [result.message, result.instructions].filter(Boolean);
330
+ return parts.length > 0 ? parts.join("\n") : "Done";
331
+ }
332
+
333
+ let buffer = "";
334
+
335
+ process.stdin.on("data", (chunk: Buffer) => {
336
+ buffer += chunk.toString();
337
+ const lines = buffer.split("\n");
338
+ buffer = lines.pop() ?? "";
339
+
340
+ for (const line of lines) {
341
+ if (!line.trim()) continue;
342
+ let msg: unknown;
343
+ try {
344
+ msg = JSON.parse(line);
345
+ } catch {
346
+ continue;
347
+ }
348
+ if (!isJsonRpcMessage(msg)) continue;
349
+
350
+ const { id, method, params } = msg;
351
+
352
+ if (method === "initialize") {
353
+ respond({
354
+ jsonrpc: "2.0",
355
+ id,
356
+ result: {
357
+ protocolVersion: "2024-11-05",
358
+ capabilities: { tools: {} },
359
+ serverInfo: { name: "mulmoclaude", version: "1.0.0" },
360
+ },
361
+ });
362
+ } else if (method === "tools/list") {
363
+ respond({
364
+ jsonrpc: "2.0",
365
+ id,
366
+ result: {
367
+ tools: tools.map((t) => ({
368
+ name: t.name,
369
+ description: t.description,
370
+ inputSchema: t.inputSchema,
371
+ })),
372
+ },
373
+ });
374
+ } else if (method === "tools/call") {
375
+ if (!params?.name) {
376
+ respond({
377
+ jsonrpc: "2.0",
378
+ id,
379
+ error: {
380
+ code: -32602,
381
+ message: "Invalid params: tools/call requires params.name",
382
+ },
383
+ });
384
+ continue;
385
+ }
386
+ const toolArgs = params.arguments ?? {};
387
+ handleToolCall(params.name, toolArgs)
388
+ .then((text) => {
389
+ respond({
390
+ jsonrpc: "2.0",
391
+ id,
392
+ result: { content: [{ type: "text", text }] },
393
+ });
394
+ })
395
+ .catch((err: unknown) => {
396
+ respond({
397
+ jsonrpc: "2.0",
398
+ id,
399
+ result: {
400
+ content: [{ type: "text", text: String(err) }],
401
+ isError: true,
402
+ },
403
+ });
404
+ });
405
+ } else if (method === "ping") {
406
+ respond({ jsonrpc: "2.0", id, result: {} });
407
+ }
408
+ // notifications/initialized and other notifications: no response needed
409
+ }
410
+ });
411
+
412
+ process.stdin.on("end", () => process.exit(0));
@@ -0,0 +1,63 @@
1
+ import { Router, Request, Response } from "express";
2
+ import { readXPost, searchX } from "./x.js";
3
+ import { errorMessage } from "../../utils/errors.js";
4
+ import { notFound, sendError, serverError } from "../../utils/httpError.js";
5
+ import { API_ROUTES } from "../../../src/config/apiRoutes.js";
6
+
7
+ export interface McpTool {
8
+ definition: {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: object;
12
+ };
13
+ requiredEnv?: string[];
14
+ prompt?: string;
15
+ handler: (args: Record<string, unknown>) => Promise<string>;
16
+ }
17
+
18
+ export const mcpTools: McpTool[] = [readXPost, searchX];
19
+
20
+ const toolMap = new Map(mcpTools.map((t) => [t.definition.name, t]));
21
+
22
+ export function isMcpToolEnabled(tool: McpTool): boolean {
23
+ return (tool.requiredEnv ?? []).every((key) => !!process.env[key]);
24
+ }
25
+
26
+ // Express router ──────────────────────────────────────────────────────────────
27
+
28
+ export const mcpToolsRouter = Router();
29
+
30
+ interface McpToolParams {
31
+ tool: string;
32
+ }
33
+
34
+ // GET /api/mcp-tools — returns { name, enabled, requiredEnv } for each tool (used by the role builder UI)
35
+ mcpToolsRouter.get(API_ROUTES.mcpTools.list, (_req: Request, res: Response) => {
36
+ res.json(
37
+ mcpTools.map((t) => ({
38
+ name: t.definition.name,
39
+ enabled: isMcpToolEnabled(t),
40
+ requiredEnv: t.requiredEnv ?? [],
41
+ prompt: t.prompt,
42
+ })),
43
+ );
44
+ });
45
+
46
+ // POST /api/mcp-tools/:tool — dispatches to the right handler
47
+ mcpToolsRouter.post(API_ROUTES.mcpTools.invoke, async (req: Request<McpToolParams, unknown, Record<string, unknown>>, res: Response) => {
48
+ const tool = toolMap.get(req.params.tool);
49
+ if (!tool) {
50
+ notFound(res, `Unknown MCP tool: ${req.params.tool}`);
51
+ return;
52
+ }
53
+ if (!isMcpToolEnabled(tool)) {
54
+ sendError(res, 503, `Tool ${req.params.tool} is not configured.`);
55
+ return;
56
+ }
57
+ try {
58
+ const result = await tool.handler(req.body);
59
+ res.json({ result });
60
+ } catch (err) {
61
+ serverError(res, errorMessage(err));
62
+ }
63
+ });
@@ -0,0 +1,188 @@
1
+ import { errorMessage } from "../../utils/errors.js";
2
+ import { safeResponseText } from "../../utils/http.js";
3
+ import { env } from "../../system/env.js";
4
+
5
+ const X_API_BASE = "https://api.twitter.com/2";
6
+ const TWEET_FIELDS = "tweet.fields=created_at,author_id,public_metrics,entities";
7
+ const EXPANSIONS = "expansions=author_id";
8
+ const USER_FIELDS = "user.fields=name,username";
9
+
10
+ interface XUser {
11
+ id: string;
12
+ name: string;
13
+ username: string;
14
+ }
15
+
16
+ interface XTweet {
17
+ id: string;
18
+ text: string;
19
+ author_id?: string;
20
+ created_at?: string;
21
+ public_metrics?: {
22
+ like_count: number;
23
+ retweet_count: number;
24
+ reply_count: number;
25
+ };
26
+ }
27
+
28
+ interface XApiResponse {
29
+ data?: XTweet | XTweet[];
30
+ includes?: { users?: XUser[] };
31
+ errors?: { detail: string }[];
32
+ meta?: { result_count: number };
33
+ }
34
+
35
+ async function fetchX(path: string): Promise<XApiResponse> {
36
+ const token = env.xBearerToken;
37
+ if (!token) throw new Error("X_BEARER_TOKEN is not configured in .env");
38
+
39
+ let response: Response;
40
+ try {
41
+ response = await fetch(`${X_API_BASE}${path}`, {
42
+ headers: { Authorization: `Bearer ${token}` },
43
+ });
44
+ } catch (err) {
45
+ throw new Error(`Network error calling X API: ${errorMessage(err)}`);
46
+ }
47
+
48
+ if (response.status === 401) throw new Error("X API error 401: Invalid or expired Bearer Token.");
49
+ if (response.status === 429) throw new Error("X API error 429: Rate limit exceeded. Please wait before retrying.");
50
+ if (!response.ok) {
51
+ const body = await safeResponseText(response);
52
+ throw new Error(`X API error ${response.status}: ${body}`);
53
+ }
54
+
55
+ return response.json() as Promise<XApiResponse>;
56
+ }
57
+
58
+ function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
59
+ const date = tweet.created_at ? new Date(tweet.created_at).toISOString().split("T")[0] : "";
60
+ const dateSuffix = date ? ` · ${date}` : "";
61
+ const byline = author ? `@${author.username} (${author.name})${dateSuffix}` : date;
62
+ const metrics = tweet.public_metrics
63
+ ? `Likes: ${tweet.public_metrics.like_count} | Retweets: ${tweet.public_metrics.retweet_count} | Replies: ${tweet.public_metrics.reply_count}`
64
+ : "";
65
+ const link = url ?? "";
66
+ return [byline, "", tweet.text, "", metrics, link]
67
+ .filter((l) => l !== undefined)
68
+ .join("\n")
69
+ .trimEnd();
70
+ }
71
+
72
+ // ── readXPost ──────────────────────────────────────────────────────────────────
73
+
74
+ export const readXPost = {
75
+ definition: {
76
+ name: "readXPost",
77
+ description: "Fetch the content of a single X (Twitter) post by URL or tweet ID. Returns the author, text, and engagement metrics.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ url: {
82
+ type: "string",
83
+ description: "Full X post URL (https://x.com/user/status/ID) or bare tweet ID.",
84
+ },
85
+ },
86
+ required: ["url"],
87
+ },
88
+ },
89
+
90
+ requiredEnv: ["X_BEARER_TOKEN"],
91
+
92
+ prompt: "Use the readXPost tool whenever the user shares a URL from x.com or twitter.com.",
93
+
94
+ async handler(args: Record<string, unknown>): Promise<string> {
95
+ const url = String(args.url ?? "");
96
+ const match = url.match(/status\/(\d+)/);
97
+ const tweetId = match ? match[1] : /^\d+$/.test(url) ? url : null;
98
+ if (!tweetId) return `Could not extract a tweet ID from: ${url}. Provide a full x.com URL or a numeric tweet ID.`;
99
+
100
+ let data: XApiResponse;
101
+ try {
102
+ data = await fetchX(`/tweets/${tweetId}?${TWEET_FIELDS}&${EXPANSIONS}&${USER_FIELDS}`);
103
+ } catch (err) {
104
+ return errorMessage(err);
105
+ }
106
+
107
+ if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
108
+
109
+ const tweet = data.data as XTweet | undefined;
110
+ if (!tweet) return "Tweet not found.";
111
+
112
+ const author = data.includes?.users?.find((u) => u.id === tweet.author_id);
113
+ const canonicalUrl = author ? `https://x.com/${author.username}/status/${tweet.id}` : undefined;
114
+ return formatTweet(tweet, author, canonicalUrl);
115
+ },
116
+ };
117
+
118
+ // ── searchX ───────────────────────────────────────────────────────────────────
119
+
120
+ export const searchX = {
121
+ definition: {
122
+ name: "searchX",
123
+ description: "Search recent X (Twitter) posts by keyword or query. Returns up to max_results posts (default 10, max 100).",
124
+ inputSchema: {
125
+ type: "object",
126
+ properties: {
127
+ query: {
128
+ type: "string",
129
+ description: "X search query. Supports operators like from:user, #hashtag, -excludeword.",
130
+ },
131
+ max_results: {
132
+ type: "number",
133
+ description: "Number of results to return (10–100). Defaults to 10.",
134
+ },
135
+ sort_order: {
136
+ type: "string",
137
+ enum: ["recency", "relevancy"],
138
+ description: "'recency' = latest tweets first (default). 'relevancy' = most relevant (Top) first.",
139
+ },
140
+ },
141
+ required: ["query"],
142
+ },
143
+ },
144
+
145
+ requiredEnv: ["X_BEARER_TOKEN"],
146
+
147
+ prompt: "Use the searchX tool to find recent posts on X by keyword or topic.",
148
+
149
+ async handler(args: Record<string, unknown>): Promise<string> {
150
+ const query = String(args.query ?? "").trim();
151
+ if (!query) return "A search query is required.";
152
+
153
+ const maxResults = Math.min(100, Math.max(10, Number(args.max_results ?? 10)));
154
+
155
+ let data: XApiResponse;
156
+ try {
157
+ const sortOrder = args.sort_order === "relevancy" ? "relevancy" : "recency";
158
+ const params = new URLSearchParams({
159
+ query,
160
+ max_results: String(maxResults),
161
+ sort_order: sortOrder,
162
+ });
163
+ params.append("tweet.fields", "created_at,author_id,public_metrics");
164
+ params.append("expansions", "author_id");
165
+ params.append("user.fields", "name,username");
166
+ data = await fetchX(`/tweets/search/recent?${params.toString()}`);
167
+ } catch (err) {
168
+ return errorMessage(err);
169
+ }
170
+
171
+ if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
172
+
173
+ const tweets = Array.isArray(data.data) ? data.data : [];
174
+ if (tweets.length === 0) return `No recent posts found for: "${query}"`;
175
+
176
+ const users = data.includes?.users ?? [];
177
+ const userMap = new Map(users.map((u) => [u.id, u]));
178
+
179
+ const lines: string[] = [`Search: "${query}" — ${tweets.length} result${tweets.length !== 1 ? "s" : ""}`, ""];
180
+ tweets.forEach((tweet, i) => {
181
+ const author = tweet.author_id ? userMap.get(tweet.author_id) : undefined;
182
+ lines.push(`${i + 1}. ${formatTweet(tweet, author)}`);
183
+ lines.push("");
184
+ });
185
+
186
+ return lines.join("\n").trimEnd();
187
+ },
188
+ };