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.
- package/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Request-body validators for the mulmo-script PUT endpoints. Split
|
|
2
|
+
// from `mulmo-script.ts` so the pure shape checks can be unit-tested
|
|
3
|
+
// without spinning up Express.
|
|
4
|
+
//
|
|
5
|
+
// The `@mulmocast/types` package exports zod schemas that mirror the
|
|
6
|
+
// canonical MulmoScript / MulmoBeat shapes. Using them here keeps the
|
|
7
|
+
// server and client agreeing on what a "valid script" is — the same
|
|
8
|
+
// schemas back client-side edit-time validation in
|
|
9
|
+
// `src/plugins/presentMulmoScript/View.vue`.
|
|
10
|
+
|
|
11
|
+
import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
|
|
12
|
+
import { isRecord } from "../../utils/types.js";
|
|
13
|
+
|
|
14
|
+
export type ValidationResult<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
15
|
+
|
|
16
|
+
function formatZodIssues(
|
|
17
|
+
// Zod's `$ZodIssue.path` is `PropertyKey[]` (includes `symbol`).
|
|
18
|
+
// Accept the wider type so callers can pass `safeParse().error.issues`
|
|
19
|
+
// directly; stringify any non-string/number segments at format time.
|
|
20
|
+
issues: ReadonlyArray<{ message: string; path: ReadonlyArray<PropertyKey> }>,
|
|
21
|
+
): string {
|
|
22
|
+
if (issues.length === 0) return "invalid shape";
|
|
23
|
+
const head = issues
|
|
24
|
+
.slice(0, 3)
|
|
25
|
+
.map((i) => {
|
|
26
|
+
const pathStr = i.path.length > 0 ? i.path.map((seg) => String(seg)).join(".") : "<root>";
|
|
27
|
+
return `${pathStr}: ${i.message}`;
|
|
28
|
+
})
|
|
29
|
+
.join("; ");
|
|
30
|
+
return issues.length > 3 ? `${head} (+${issues.length - 3} more)` : head;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validate the `update-script` request body. Returns the parsed,
|
|
35
|
+
* schema-conformant script on success, or a human-readable error
|
|
36
|
+
* suitable for sending back as a 400 response.
|
|
37
|
+
*/
|
|
38
|
+
export function validateUpdateScriptBody(body: unknown): ValidationResult<{
|
|
39
|
+
filePath: string;
|
|
40
|
+
script: unknown;
|
|
41
|
+
}> {
|
|
42
|
+
if (!isRecord(body)) {
|
|
43
|
+
return { ok: false, error: "body must be an object" };
|
|
44
|
+
}
|
|
45
|
+
if (typeof body.filePath !== "string" || body.filePath === "") {
|
|
46
|
+
return { ok: false, error: "filePath must be a non-empty string" };
|
|
47
|
+
}
|
|
48
|
+
if (body.script === undefined) {
|
|
49
|
+
return { ok: false, error: "script is required" };
|
|
50
|
+
}
|
|
51
|
+
const parsed = mulmoScriptSchema.safeParse(body.script);
|
|
52
|
+
if (!parsed.success) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: `invalid script: ${formatZodIssues(parsed.error.issues)}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
value: { filePath: body.filePath as string, script: parsed.data },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate the `update-beat` request body. `beatIndex` is allowed
|
|
66
|
+
* to be any non-negative integer; the handler still bounds-checks
|
|
67
|
+
* against the actual script length after reading the file.
|
|
68
|
+
*/
|
|
69
|
+
export function validateUpdateBeatBody(body: unknown): ValidationResult<{
|
|
70
|
+
filePath: string;
|
|
71
|
+
beatIndex: number;
|
|
72
|
+
beat: unknown;
|
|
73
|
+
}> {
|
|
74
|
+
if (!isRecord(body)) {
|
|
75
|
+
return { ok: false, error: "body must be an object" };
|
|
76
|
+
}
|
|
77
|
+
if (typeof body.filePath !== "string" || body.filePath === "") {
|
|
78
|
+
return { ok: false, error: "filePath must be a non-empty string" };
|
|
79
|
+
}
|
|
80
|
+
if (typeof body.beatIndex !== "number" || !Number.isInteger(body.beatIndex) || body.beatIndex < 0) {
|
|
81
|
+
return { ok: false, error: "beatIndex must be a non-negative integer" };
|
|
82
|
+
}
|
|
83
|
+
if (body.beat === undefined) {
|
|
84
|
+
return { ok: false, error: "beat is required" };
|
|
85
|
+
}
|
|
86
|
+
const parsed = mulmoBeatSchema.safeParse(body.beat);
|
|
87
|
+
if (!parsed.success) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: `invalid beat: ${formatZodIssues(parsed.error.issues)}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
value: {
|
|
96
|
+
filePath: body.filePath as string,
|
|
97
|
+
beatIndex: body.beatIndex as number,
|
|
98
|
+
beat: parsed.data,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// PoC push endpoint — proves the server can fire a delayed message
|
|
2
|
+
// simultaneously to every open Web tab (pub-sub) and every bridge
|
|
3
|
+
// (chat-service `pushToBridge`). Stepping stone for the in-app
|
|
4
|
+
// notification center (#144) and external-channel notifications
|
|
5
|
+
// (#142); see plans/feat-notification-push-scaffold.md for the
|
|
6
|
+
// motivation and the production plan.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// curl -X POST http://localhost:3001/api/notifications/test \
|
|
10
|
+
// -H "Authorization: Bearer $(cat ~/mulmoclaude/.session-token)" \
|
|
11
|
+
// -H "Content-Type: application/json" \
|
|
12
|
+
// -d '{"message":"hello","delaySeconds":5}'
|
|
13
|
+
//
|
|
14
|
+
// The route is exported as a factory so the host wiring can inject
|
|
15
|
+
// the pub-sub publisher and the chat-service push handle without
|
|
16
|
+
// this file pulling in either module directly.
|
|
17
|
+
|
|
18
|
+
import { Router, type Request, type Response } from "express";
|
|
19
|
+
import { scheduleTestNotification, type NotificationDeps, type ScheduleNotificationOptions } from "../../events/notifications.js";
|
|
20
|
+
import { log } from "../../system/logger/index.js";
|
|
21
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
22
|
+
|
|
23
|
+
interface TestRequestBody {
|
|
24
|
+
message?: unknown;
|
|
25
|
+
delaySeconds?: unknown;
|
|
26
|
+
transportId?: unknown;
|
|
27
|
+
chatId?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TestResponse {
|
|
31
|
+
firesAt: string;
|
|
32
|
+
delaySeconds: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseBody(body: TestRequestBody): ScheduleNotificationOptions {
|
|
36
|
+
const opts: ScheduleNotificationOptions = {};
|
|
37
|
+
if (typeof body.message === "string" && body.message.length > 0) {
|
|
38
|
+
opts.message = body.message;
|
|
39
|
+
}
|
|
40
|
+
if (typeof body.delaySeconds === "number") {
|
|
41
|
+
opts.delaySeconds = body.delaySeconds;
|
|
42
|
+
}
|
|
43
|
+
if (typeof body.transportId === "string" && body.transportId.length > 0) {
|
|
44
|
+
opts.transportId = body.transportId;
|
|
45
|
+
}
|
|
46
|
+
if (typeof body.chatId === "string" && body.chatId.length > 0) {
|
|
47
|
+
opts.chatId = body.chatId;
|
|
48
|
+
}
|
|
49
|
+
return opts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createNotificationsRouter(deps: NotificationDeps): Router {
|
|
53
|
+
const router = Router();
|
|
54
|
+
router.post(API_ROUTES.notifications.test, (req: Request<object, unknown, TestRequestBody>, res: Response<TestResponse>) => {
|
|
55
|
+
const opts = parseBody(req.body ?? {});
|
|
56
|
+
const scheduled = scheduleTestNotification(opts, deps);
|
|
57
|
+
log.info("notifications", "scheduled test push", {
|
|
58
|
+
delaySeconds: scheduled.delaySeconds,
|
|
59
|
+
firesAt: scheduled.firesAt,
|
|
60
|
+
transportId: opts.transportId,
|
|
61
|
+
chatId: opts.chatId,
|
|
62
|
+
});
|
|
63
|
+
res.status(202).json({
|
|
64
|
+
firesAt: scheduled.firesAt,
|
|
65
|
+
delaySeconds: scheduled.delaySeconds,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
return router;
|
|
69
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Router, Request, Response } from "express";
|
|
4
|
+
import { marked } from "marked";
|
|
5
|
+
import puppeteer from "puppeteer";
|
|
6
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
7
|
+
import { badRequest, serverError } from "../../utils/httpError.js";
|
|
8
|
+
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
9
|
+
import { resolveWithinRoot, readBinarySafeSync } from "../../utils/files/safe.js";
|
|
10
|
+
import { resolveWorkspacePath } from "../../utils/files/workspace-io.js";
|
|
11
|
+
import { log } from "../../system/logger/index.js";
|
|
12
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
13
|
+
|
|
14
|
+
const router = Router();
|
|
15
|
+
|
|
16
|
+
const MARKDOWN_CSS = `
|
|
17
|
+
body {
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
19
|
+
font-size: 13px;
|
|
20
|
+
line-height: 1.6;
|
|
21
|
+
color: #1f2937;
|
|
22
|
+
max-width: 800px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
padding: 32px 48px;
|
|
25
|
+
}
|
|
26
|
+
h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.75rem; color: #111827; }
|
|
27
|
+
h2 { font-size: 1.25rem; font-weight: 600; margin: 1.5rem 0 0.5rem; color: #1f2937; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25rem; }
|
|
28
|
+
h3 { font-size: 1rem; font-weight: 600; margin: 1rem 0 0.4rem; color: #374151; }
|
|
29
|
+
p { margin: 0 0 0.75rem; }
|
|
30
|
+
ul, ol { margin: 0 0 0.75rem 1.5rem; }
|
|
31
|
+
li { margin-bottom: 0.2rem; }
|
|
32
|
+
ul { list-style-type: disc; }
|
|
33
|
+
ol { list-style-type: decimal; }
|
|
34
|
+
code { background: #f3f4f6; padding: 0.1rem 0.3rem; border-radius: 0.25rem; font-size: 0.85em; font-family: monospace; }
|
|
35
|
+
pre { background: #f3f4f6; padding: 0.75rem; border-radius: 0.375rem; overflow-x: auto; margin: 0 0 0.75rem; }
|
|
36
|
+
pre code { background: none; padding: 0; }
|
|
37
|
+
blockquote { border-left: 3px solid #d1d5db; padding-left: 1rem; color: #6b7280; margin: 0.75rem 0; }
|
|
38
|
+
hr { border: none; border-top: 1px solid #e5e7eb; margin: 1.25rem 0; }
|
|
39
|
+
table { border-collapse: collapse; width: 100%; margin: 0 0 0.75rem; font-size: 0.875rem; }
|
|
40
|
+
th, td { border: 1px solid #e5e7eb; padding: 0.5rem 0.75rem; text-align: left; }
|
|
41
|
+
th { background: #f9fafb; font-weight: 600; }
|
|
42
|
+
strong { font-weight: 600; }
|
|
43
|
+
a { color: #2563eb; }
|
|
44
|
+
img { max-width: 100%; height: auto; }
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const MIME_BY_EXT: Record<string, string> = {
|
|
48
|
+
".png": "image/png",
|
|
49
|
+
".jpg": "image/jpeg",
|
|
50
|
+
".jpeg": "image/jpeg",
|
|
51
|
+
".gif": "image/gif",
|
|
52
|
+
".svg": "image/svg+xml",
|
|
53
|
+
".webp": "image/webp",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Realpath of the workspace, resolved once at module load. Used to
|
|
57
|
+
// validate that image paths resolved relative to markdowns/ stay
|
|
58
|
+
// inside the workspace after symlink resolution.
|
|
59
|
+
const workspaceReal = fs.realpathSync(resolveWorkspacePath(""));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inline local images as base64 data URIs so Puppeteer can render them.
|
|
63
|
+
* Markdown files live in workspace/artifacts/documents/ and reference
|
|
64
|
+
* images as "../images/xyz.png" → workspace/artifacts/images/xyz.png.
|
|
65
|
+
*
|
|
66
|
+
* Paths are validated against the workspace root via resolveWithinRoot
|
|
67
|
+
* so an attacker-controlled <img src="../../../etc/passwd"> can't read
|
|
68
|
+
* files outside the workspace.
|
|
69
|
+
*/
|
|
70
|
+
function inlineImages(html: string): string {
|
|
71
|
+
const baseDir = path.join(workspaceReal, WORKSPACE_DIRS.markdowns);
|
|
72
|
+
return html.replace(/(<img\s[^>]*src=")([^"]+)(")/g, (_match, before: string, src: string, after: string) => {
|
|
73
|
+
if (src.startsWith("data:") || src.startsWith("http")) {
|
|
74
|
+
return _match;
|
|
75
|
+
}
|
|
76
|
+
// Resolve the image path relative to markdowns/ but require the
|
|
77
|
+
// final realpath to stay inside the workspace root. markdowns/
|
|
78
|
+
// references like "../images/foo.png" are common so we can't
|
|
79
|
+
// restrict to markdowns/ itself.
|
|
80
|
+
const unsafeAbs = path.resolve(baseDir, src);
|
|
81
|
+
// Make unsafeAbs relative to the workspace for the
|
|
82
|
+
// resolveWithinRoot check (it expects a relative path).
|
|
83
|
+
const relToWorkspace = path.relative(workspaceReal, unsafeAbs);
|
|
84
|
+
if (relToWorkspace.startsWith("..") || path.isAbsolute(relToWorkspace)) {
|
|
85
|
+
log.warn("pdf", "image path escapes workspace", { src });
|
|
86
|
+
return _match;
|
|
87
|
+
}
|
|
88
|
+
const abs = resolveWithinRoot(workspaceReal, relToWorkspace);
|
|
89
|
+
if (!abs) {
|
|
90
|
+
log.warn("pdf", "image path rejected by safe-resolve", { src });
|
|
91
|
+
return _match;
|
|
92
|
+
}
|
|
93
|
+
const buf = readBinarySafeSync(abs);
|
|
94
|
+
if (!buf) {
|
|
95
|
+
log.warn("pdf", "could not read image", { abs });
|
|
96
|
+
return _match;
|
|
97
|
+
}
|
|
98
|
+
const ext = path.extname(abs).toLowerCase();
|
|
99
|
+
const mime = MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
100
|
+
return `${before}data:${mime};base64,${buf.toString("base64")}${after}`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function wrapHtml(body: string, css: string): string {
|
|
105
|
+
return `<!DOCTYPE html>
|
|
106
|
+
<html>
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="utf-8">
|
|
109
|
+
<style>${css}</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>${body}</body>
|
|
112
|
+
</html>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function renderPdf(fullHtml: string, format: "Letter" | "A4" = "Letter"): Promise<Buffer> {
|
|
116
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
117
|
+
try {
|
|
118
|
+
const page = await browser.newPage();
|
|
119
|
+
await page.setContent(fullHtml, { waitUntil: "networkidle0" });
|
|
120
|
+
const pdfBuffer = await page.pdf({
|
|
121
|
+
format,
|
|
122
|
+
margin: { top: "16mm", bottom: "16mm", left: "16mm", right: "16mm" },
|
|
123
|
+
printBackground: true,
|
|
124
|
+
});
|
|
125
|
+
return Buffer.from(pdfBuffer);
|
|
126
|
+
} finally {
|
|
127
|
+
await browser.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sendPdf(res: Response, buffer: Buffer, filename: string): void {
|
|
132
|
+
const safeFilename = filename.endsWith(".pdf") ? filename : `${filename}.pdf`;
|
|
133
|
+
res.setHeader("Content-Type", "application/pdf");
|
|
134
|
+
res.setHeader("Content-Disposition", `attachment; filename="document.pdf"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
|
|
135
|
+
res.send(buffer);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface PdfMarkdownBody {
|
|
139
|
+
markdown: string;
|
|
140
|
+
filename?: string;
|
|
141
|
+
format?: "Letter" | "A4";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
router.post(API_ROUTES.pdf.markdown, async (req: Request<object, unknown, PdfMarkdownBody>, res: Response) => {
|
|
145
|
+
const { markdown, filename = "document.pdf", format = "Letter" } = req.body;
|
|
146
|
+
|
|
147
|
+
if (!markdown) {
|
|
148
|
+
badRequest(res, "markdown is required");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
log.info("pdf", "markdown", { filename, length: markdown.length });
|
|
154
|
+
const html = inlineImages(await marked.parse(markdown));
|
|
155
|
+
const buffer = await renderPdf(wrapHtml(html, MARKDOWN_CSS), format);
|
|
156
|
+
sendPdf(res, buffer, filename);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
log.error("pdf", "generation failed", { error: String(err) });
|
|
159
|
+
serverError(res, `PDF generation failed: ${errorMessage(err)}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export default router;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { Router, Request, Response } from "express";
|
|
3
|
+
import { executeMindMap } from "@gui-chat-plugin/mindmap";
|
|
4
|
+
import { executeSpreadsheet, type SpreadsheetArgs } from "../../../src/plugins/spreadsheet/definition.js";
|
|
5
|
+
import { executeQuiz } from "@mulmochat-plugin/quiz";
|
|
6
|
+
import { executeForm } from "@mulmochat-plugin/form";
|
|
7
|
+
import { executeOpenCanvas } from "../../../src/plugins/canvas/definition.js";
|
|
8
|
+
import { executePresent3D } from "@gui-chat-plugin/present3d";
|
|
9
|
+
import { generateGeminiImageFromPrompt, isGeminiAvailable } from "../../utils/gemini.js";
|
|
10
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
11
|
+
import { badRequest, serverError } from "../../utils/httpError.js";
|
|
12
|
+
import { log } from "../../system/logger/index.js";
|
|
13
|
+
import { saveImage } from "../../utils/files/image-store.js";
|
|
14
|
+
import { saveMarkdown, overwriteMarkdown, isMarkdownPath } from "../../utils/files/markdown-store.js";
|
|
15
|
+
import { saveSpreadsheet, overwriteSpreadsheet, isSpreadsheetPath } from "../../utils/files/spreadsheet-store.js";
|
|
16
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
17
|
+
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
18
|
+
|
|
19
|
+
const router = Router();
|
|
20
|
+
|
|
21
|
+
interface PluginErrorResponse {
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Wraps a plugin's `execute*` invocation in an Express handler. Each
|
|
26
|
+
// plugin route used to inline the same try/catch + 500 response shell;
|
|
27
|
+
// this collapses them to one line per route.
|
|
28
|
+
//
|
|
29
|
+
// The callback receives the Express request and is responsible for
|
|
30
|
+
// pulling whatever it needs out of `req.body` and forwarding it to
|
|
31
|
+
// the plugin's execute function. `req.body` is `any` by Express
|
|
32
|
+
// default and each plugin's execute function does its own runtime
|
|
33
|
+
// validation — matching the behavior of the inline handlers this
|
|
34
|
+
// replaces.
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
function wrapPluginExecute<TBody = any, TResult = unknown>(
|
|
37
|
+
execute: (req: Request<object, unknown, TBody>) => Promise<TResult>,
|
|
38
|
+
): (req: Request<object, unknown, TBody>, res: Response<TResult | PluginErrorResponse>) => Promise<void> {
|
|
39
|
+
return async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await execute(req);
|
|
42
|
+
res.json(result);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.status(500).json({ message: errorMessage(err) });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const IMAGE_PLACEHOLDER = /!\[([^\]]+)\]\(\/?__too_be_replaced_image_path__\)/g;
|
|
50
|
+
|
|
51
|
+
async function generateImageFile(prompt: string): Promise<string | null> {
|
|
52
|
+
if (!isGeminiAvailable()) return null;
|
|
53
|
+
try {
|
|
54
|
+
const { imageData } = await generateGeminiImageFromPrompt(prompt);
|
|
55
|
+
if (imageData) return saveImage(imageData);
|
|
56
|
+
log.warn("present-document", "Gemini returned no image data for prompt", {
|
|
57
|
+
promptPreview: prompt.slice(0, 80),
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// Surface the failure so missing-image symptoms in the canvas
|
|
61
|
+
// are debuggable from the server log instead of vanishing.
|
|
62
|
+
log.warn("present-document", "Gemini image generation failed", {
|
|
63
|
+
error: errorMessage(err),
|
|
64
|
+
promptPreview: prompt.slice(0, 80),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fillImagePlaceholders(markdown: string): Promise<string> {
|
|
71
|
+
const matches = [...markdown.matchAll(IMAGE_PLACEHOLDER)];
|
|
72
|
+
if (matches.length === 0) return markdown;
|
|
73
|
+
// Only attempt generation when Gemini is wired up; otherwise the
|
|
74
|
+
// placeholder still gets stripped below so we don't leak a broken
|
|
75
|
+
// <img src="...__too_be_replaced_image_path__"> into the rendered
|
|
76
|
+
// document.
|
|
77
|
+
const geminiOk = isGeminiAvailable();
|
|
78
|
+
if (!geminiOk) {
|
|
79
|
+
log.warn("present-document", "GEMINI_API_KEY not set — image placeholders will render as text markers", { placeholderCount: matches.length });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const results = await Promise.all(
|
|
83
|
+
matches.map(async (m) => ({
|
|
84
|
+
full: m[0],
|
|
85
|
+
prompt: m[1],
|
|
86
|
+
url: geminiOk ? await generateImageFile(m[1]) : null,
|
|
87
|
+
})),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Surface a single tally line so the operator can see the
|
|
91
|
+
// success rate even when most calls go through. The per-call
|
|
92
|
+
// error already lands at warn from generateImageFile's catch.
|
|
93
|
+
if (geminiOk) {
|
|
94
|
+
const failed = results.filter((r) => !r.url).length;
|
|
95
|
+
if (failed > 0) {
|
|
96
|
+
log.warn("present-document", "image generation had failures", {
|
|
97
|
+
failed,
|
|
98
|
+
total: results.length,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let filled = markdown;
|
|
104
|
+
for (const { full, prompt, url } of results) {
|
|
105
|
+
// On success → real image. On failure / no key → italic text
|
|
106
|
+
// marker so the alt prompt still surfaces but no broken image
|
|
107
|
+
// 404s through the View. The user can re-render later once
|
|
108
|
+
// GEMINI_API_KEY is set.
|
|
109
|
+
filled = filled.replace(
|
|
110
|
+
full,
|
|
111
|
+
// `url` is workspace-relative (e.g. "artifacts/images/xxx.png").
|
|
112
|
+
// The document lives at "artifacts/documents/yyy.md". Compute a
|
|
113
|
+
// relative path from the document's directory so the markdown
|
|
114
|
+
// image reference resolves correctly.
|
|
115
|
+
url ? `})` : `*🖼️ Image: ${prompt}*`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return filled;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// presentDocument — fills image placeholders via Gemini if API key is available
|
|
122
|
+
interface PresentDocumentBody {
|
|
123
|
+
title: string;
|
|
124
|
+
markdown: string;
|
|
125
|
+
filenamePrefix: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface PresentDocumentSuccess {
|
|
129
|
+
message: string;
|
|
130
|
+
title: string;
|
|
131
|
+
data: { markdown: string; filenamePrefix: string };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface PresentDocumentError {
|
|
135
|
+
error: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
router.post(
|
|
139
|
+
API_ROUTES.plugins.presentDocument,
|
|
140
|
+
async (req: Request<object, unknown, PresentDocumentBody>, res: Response<PresentDocumentSuccess | PresentDocumentError>) => {
|
|
141
|
+
const { title, markdown, filenamePrefix } = req.body;
|
|
142
|
+
if (typeof filenamePrefix !== "string" || filenamePrefix.trim().length === 0) {
|
|
143
|
+
badRequest(res, "filenamePrefix is required");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const filledMarkdown = await fillImagePlaceholders(markdown);
|
|
147
|
+
const markdownPath = await saveMarkdown(filledMarkdown, filenamePrefix);
|
|
148
|
+
res.json({
|
|
149
|
+
message: `Document "${title}" is ready.`,
|
|
150
|
+
title,
|
|
151
|
+
data: { markdown: markdownPath, filenamePrefix },
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Update markdown file on disk (user edits in View)
|
|
157
|
+
interface UpdateMarkdownBody {
|
|
158
|
+
markdown: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface UpdateMarkdownResponse {
|
|
162
|
+
path: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface UpdateMarkdownError {
|
|
166
|
+
error: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
router.put(
|
|
170
|
+
API_ROUTES.plugins.updateMarkdown,
|
|
171
|
+
async (req: Request<{ filename: string }, unknown, UpdateMarkdownBody>, res: Response<UpdateMarkdownResponse | UpdateMarkdownError>) => {
|
|
172
|
+
const relativePath = `${WORKSPACE_DIRS.markdowns}/${req.params.filename}`;
|
|
173
|
+
const { markdown } = req.body;
|
|
174
|
+
if (!markdown) {
|
|
175
|
+
badRequest(res, "markdown is required");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!isMarkdownPath(relativePath)) {
|
|
179
|
+
badRequest(res, "invalid markdown path");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await overwriteMarkdown(relativePath, markdown);
|
|
184
|
+
res.json({ path: relativePath });
|
|
185
|
+
} catch (err) {
|
|
186
|
+
serverError(res, errorMessage(err));
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// `null as never` in the calls below: each plugin's `execute*`
|
|
192
|
+
// function expects a client-side context object as its first
|
|
193
|
+
// argument. The server-side bridge has no such context — these
|
|
194
|
+
// functions only touch their second arg (the request body) on this
|
|
195
|
+
// path — so we satisfy the type signature with a never cast rather
|
|
196
|
+
// than fabricating a fake context.
|
|
197
|
+
|
|
198
|
+
// presentSpreadsheet — validate, then save sheets to disk
|
|
199
|
+
router.post(
|
|
200
|
+
API_ROUTES.plugins.presentSpreadsheet,
|
|
201
|
+
wrapPluginExecute<SpreadsheetArgs, unknown>(async (req) => {
|
|
202
|
+
const result = await executeSpreadsheet(req.body);
|
|
203
|
+
if (!Array.isArray(result.data.sheets)) {
|
|
204
|
+
throw new Error("Expected sheets array from executeSpreadsheet");
|
|
205
|
+
}
|
|
206
|
+
const sheetsPath = await saveSpreadsheet(result.data.sheets);
|
|
207
|
+
return { ...result, data: { ...result.data, sheets: sheetsPath } };
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Update spreadsheet file on disk (user edits in View)
|
|
212
|
+
interface UpdateSpreadsheetBody {
|
|
213
|
+
sheets: unknown[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface UpdateSpreadsheetResponse {
|
|
217
|
+
path: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface UpdateSpreadsheetError {
|
|
221
|
+
error: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
router.put(
|
|
225
|
+
API_ROUTES.plugins.updateSpreadsheet,
|
|
226
|
+
async (req: Request<{ filename: string }, unknown, UpdateSpreadsheetBody>, res: Response<UpdateSpreadsheetResponse | UpdateSpreadsheetError>) => {
|
|
227
|
+
const relativePath = `${WORKSPACE_DIRS.spreadsheets}/${req.params.filename}`;
|
|
228
|
+
const { sheets } = req.body;
|
|
229
|
+
if (!Array.isArray(sheets)) {
|
|
230
|
+
badRequest(res, "sheets must be an array");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!isSpreadsheetPath(relativePath)) {
|
|
234
|
+
badRequest(res, "invalid spreadsheet path");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await overwriteSpreadsheet(relativePath, sheets);
|
|
239
|
+
res.json({ path: relativePath });
|
|
240
|
+
} catch (err) {
|
|
241
|
+
serverError(res, errorMessage(err));
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// createMindMap — uses package execute for node layout computation
|
|
247
|
+
router.post(
|
|
248
|
+
API_ROUTES.plugins.mindmap,
|
|
249
|
+
wrapPluginExecute((req) => executeMindMap(null as never, req.body)),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// putQuestions — quiz
|
|
253
|
+
router.post(
|
|
254
|
+
API_ROUTES.plugins.quiz,
|
|
255
|
+
wrapPluginExecute((req) => executeQuiz(null as never, req.body)),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// presentForm — form
|
|
259
|
+
router.post(
|
|
260
|
+
API_ROUTES.plugins.form,
|
|
261
|
+
wrapPluginExecute((req) => executeForm(null as never, req.body)),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// openCanvas — drawing canvas
|
|
265
|
+
router.post(
|
|
266
|
+
API_ROUTES.plugins.canvas,
|
|
267
|
+
wrapPluginExecute(() => executeOpenCanvas()),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// present3d — 3D visualization
|
|
271
|
+
router.post(
|
|
272
|
+
API_ROUTES.plugins.present3d,
|
|
273
|
+
wrapPluginExecute((req) => executePresent3D(null as never, req.body)),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
export default router;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
3
|
+
import { writeWorkspaceText } from "../../utils/files/workspace-io.js";
|
|
4
|
+
import { buildArtifactPath } from "../../utils/files/naming.js";
|
|
5
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
6
|
+
import { badRequest, serverError } from "../../utils/httpError.js";
|
|
7
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
interface PresentHtmlBody {
|
|
12
|
+
html: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PresentHtmlSuccessResponse {
|
|
17
|
+
message: string;
|
|
18
|
+
instructions: string;
|
|
19
|
+
data: { html: string; title?: string; filePath: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PresentHtmlErrorResponse {
|
|
23
|
+
error: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type PresentHtmlResponse = PresentHtmlSuccessResponse | PresentHtmlErrorResponse;
|
|
27
|
+
|
|
28
|
+
router.post(API_ROUTES.html.present, async (req: Request<object, unknown, PresentHtmlBody>, res: Response<PresentHtmlResponse>) => {
|
|
29
|
+
const { html, title } = req.body;
|
|
30
|
+
if (!html) {
|
|
31
|
+
badRequest(res, "html is required");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const filePath = buildArtifactPath(WORKSPACE_DIRS.htmls, title, ".html", "page");
|
|
37
|
+
await writeWorkspaceText(filePath, html);
|
|
38
|
+
res.json({
|
|
39
|
+
message: `Saved HTML to ${filePath}`,
|
|
40
|
+
instructions: "Acknowledge that the HTML page has been presented to the user.",
|
|
41
|
+
data: { html, title, filePath },
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
serverError(res, errorMessage(err));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export default router;
|