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
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// Single source of truth for Vue → MulmoClaude server HTTP calls.
|
|
2
|
+
//
|
|
3
|
+
// Before this module existed there were 56 scattered `fetch("/api/...")`
|
|
4
|
+
// calls across 29 files, each doing its own JSON serialization, its own
|
|
5
|
+
// `!res.ok` check, and its own ad-hoc error extraction. This made any
|
|
6
|
+
// cross-cutting concern — auth headers, error formatting, retry policy,
|
|
7
|
+
// logging — impossible to add without touching every call site.
|
|
8
|
+
//
|
|
9
|
+
// All HTTP traffic from the Vue app should now go through one of:
|
|
10
|
+
//
|
|
11
|
+
// apiGet<T>(path, query?)
|
|
12
|
+
// apiPost<T>(path, body?)
|
|
13
|
+
// apiPut<T>(path, body?)
|
|
14
|
+
// apiDelete<T>(path, body?)
|
|
15
|
+
// apiCall<T>(path, opts) ← generic, for methods not above
|
|
16
|
+
// apiFetchRaw(path, opts) ← when you need the raw Response
|
|
17
|
+
// (binary, streaming, etc.)
|
|
18
|
+
//
|
|
19
|
+
// Return type is a discriminated union `ApiResult<T>`:
|
|
20
|
+
//
|
|
21
|
+
// { ok: true, data: T }
|
|
22
|
+
// { ok: false, error: string, status: number }
|
|
23
|
+
//
|
|
24
|
+
// Callers pattern-match on `result.ok` — no more mixing try/catch with
|
|
25
|
+
// `!res.ok` branches. Network errors and HTTP errors surface through the
|
|
26
|
+
// same `{ ok: false }` shape.
|
|
27
|
+
//
|
|
28
|
+
// Future extension hooks (see #272 for auth token):
|
|
29
|
+
// - setAuthToken() populates a module-level token used by every call
|
|
30
|
+
// - interceptors could go here for logging, retry, metrics
|
|
31
|
+
|
|
32
|
+
import { errorMessage } from "./errors";
|
|
33
|
+
import { hasStringProp } from "./types";
|
|
34
|
+
|
|
35
|
+
// ── Auth token (populated by bootstrap; consumed by every call) ─────
|
|
36
|
+
|
|
37
|
+
let authToken: string | null = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set the bearer token used on every API call. Call once during app
|
|
41
|
+
* bootstrap, typically after reading a `<meta>` tag or window global
|
|
42
|
+
* populated by the server.
|
|
43
|
+
*/
|
|
44
|
+
export function setAuthToken(token: string | null): void {
|
|
45
|
+
authToken = token;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string; status: number };
|
|
51
|
+
|
|
52
|
+
export type ApiQuery = Record<string, string | number | boolean | undefined>;
|
|
53
|
+
|
|
54
|
+
export interface ApiOptions {
|
|
55
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
56
|
+
/** JSON-serialized into the request body. Omit for GET/DELETE. */
|
|
57
|
+
body?: unknown;
|
|
58
|
+
/** Appended as a query string. `undefined` values are dropped. */
|
|
59
|
+
query?: ApiQuery;
|
|
60
|
+
/** AbortSignal — pass through to fetch. */
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
/**
|
|
63
|
+
* Extra headers. Content-Type is set automatically for JSON bodies;
|
|
64
|
+
* Authorization is injected from `authToken`.
|
|
65
|
+
*/
|
|
66
|
+
headers?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Use Parameters<typeof fetch> rather than global DOM lib types so
|
|
70
|
+
// this module doesn't depend on DOM lib being in the ESLint globals.
|
|
71
|
+
type FetchInit = Parameters<typeof fetch>[1];
|
|
72
|
+
type FetchBody = NonNullable<FetchInit>["body"];
|
|
73
|
+
|
|
74
|
+
// ── Internals ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function buildQueryString(query: ApiQuery | undefined): string {
|
|
77
|
+
if (!query) return "";
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
for (const [key, value] of Object.entries(query)) {
|
|
80
|
+
if (value === undefined) continue;
|
|
81
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
82
|
+
}
|
|
83
|
+
return parts.length === 0 ? "" : `?${parts.join("&")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildHeaders(opts: { headers?: Record<string, string> }, hasBody: boolean): Record<string, string> {
|
|
87
|
+
const headers: Record<string, string> = { ...(opts.headers ?? {}) };
|
|
88
|
+
if (hasBody && headers["Content-Type"] === undefined) {
|
|
89
|
+
headers["Content-Type"] = "application/json";
|
|
90
|
+
}
|
|
91
|
+
if (authToken && headers["Authorization"] === undefined) {
|
|
92
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
93
|
+
}
|
|
94
|
+
return headers;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function extractError(res: Response): Promise<{ error: string; status: number }> {
|
|
98
|
+
const status = res.status;
|
|
99
|
+
// Try to parse a `{ error: string }` body first — that's the server's
|
|
100
|
+
// standard error shape. `in` narrowing lets us read `body.error`
|
|
101
|
+
// without any type assertion.
|
|
102
|
+
try {
|
|
103
|
+
const body: unknown = await res.clone().json();
|
|
104
|
+
if (hasStringProp(body, "error")) {
|
|
105
|
+
return { error: body.error, status };
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Body wasn't JSON — fall through.
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
error: res.statusText || `Request failed (${status})`,
|
|
112
|
+
status,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Core call ───────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generic HTTP call. Returns a discriminated union on success vs
|
|
120
|
+
* failure. Network errors are caught and surfaced as
|
|
121
|
+
* `{ ok: false, status: 0 }`. Assumes JSON response bodies (all
|
|
122
|
+
* MulmoClaude `/api/*` endpoints return JSON on success); use
|
|
123
|
+
* `apiFetchRaw` for binary / streaming / non-JSON responses.
|
|
124
|
+
*/
|
|
125
|
+
export async function apiCall<T = unknown>(path: string, opts: ApiOptions = {}): Promise<ApiResult<T>> {
|
|
126
|
+
const method = opts.method ?? "GET";
|
|
127
|
+
const hasBody = opts.body !== undefined;
|
|
128
|
+
const url = `${path}${buildQueryString(opts.query)}`;
|
|
129
|
+
|
|
130
|
+
const init: FetchInit = {
|
|
131
|
+
method,
|
|
132
|
+
headers: buildHeaders(opts, hasBody),
|
|
133
|
+
signal: opts.signal,
|
|
134
|
+
};
|
|
135
|
+
if (hasBody) {
|
|
136
|
+
init.body = JSON.stringify(opts.body);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let res: Response;
|
|
140
|
+
try {
|
|
141
|
+
res = await fetch(url, init);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: errorMessage(err),
|
|
146
|
+
status: 0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const { error, status } = await extractError(res);
|
|
152
|
+
return { ok: false, error, status };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// `res.json()` returns `Promise<any>`, which is assignable to T
|
|
156
|
+
// without a cast.
|
|
157
|
+
try {
|
|
158
|
+
const data: T = await res.json();
|
|
159
|
+
return { ok: true, data };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: `Invalid JSON response: ${errorMessage(err)}`,
|
|
164
|
+
status: res.status,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Convenience verbs ───────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export function apiGet<T = unknown>(path: string, query?: ApiQuery, extra: Omit<ApiOptions, "method" | "body" | "query"> = {}): Promise<ApiResult<T>> {
|
|
172
|
+
return apiCall<T>(path, { ...extra, method: "GET", query });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function apiPost<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
|
|
176
|
+
return apiCall<T>(path, { ...extra, method: "POST", body });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function apiPut<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
|
|
180
|
+
return apiCall<T>(path, { ...extra, method: "PUT", body });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function apiPatch<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
|
|
184
|
+
return apiCall<T>(path, { ...extra, method: "PATCH", body });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function apiDelete<T = unknown>(path: string, body?: unknown, extra: Omit<ApiOptions, "method" | "body"> = {}): Promise<ApiResult<T>> {
|
|
188
|
+
return apiCall<T>(path, { ...extra, method: "DELETE", body });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Raw Response escape hatch ───────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
export interface RawOptions {
|
|
194
|
+
method?: string;
|
|
195
|
+
/** Accepts any value fetch accepts (string / Blob / FormData / …). */
|
|
196
|
+
body?: FetchBody;
|
|
197
|
+
headers?: Record<string, string>;
|
|
198
|
+
signal?: AbortSignal;
|
|
199
|
+
query?: ApiQuery;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Escape hatch for endpoints returning binary / streaming / non-JSON
|
|
204
|
+
* bodies (PDF download, audio blob, SSE, etc.). Auth header is still
|
|
205
|
+
* applied; other handling is the caller's responsibility.
|
|
206
|
+
*
|
|
207
|
+
* Throws on network errors. Does NOT check `res.ok`.
|
|
208
|
+
*/
|
|
209
|
+
export async function apiFetchRaw(path: string, opts: RawOptions = {}): Promise<Response> {
|
|
210
|
+
const url = `${path}${buildQueryString(opts.query)}`;
|
|
211
|
+
const init: FetchInit = {
|
|
212
|
+
method: opts.method ?? "GET",
|
|
213
|
+
headers: buildHeaders(opts, false),
|
|
214
|
+
body: opts.body,
|
|
215
|
+
signal: opts.signal,
|
|
216
|
+
};
|
|
217
|
+
return fetch(url, init);
|
|
218
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Pure helpers for the canvas view mode.
|
|
2
|
+
// The type also lives here, so test files and composables can
|
|
3
|
+
// import it without pulling in a .vue file.
|
|
4
|
+
//
|
|
5
|
+
// To add a new view mode, add it to CANVAS_VIEW below in shortcut order.
|
|
6
|
+
// Everything else (type, set, parser, shortcut) derives automatically.
|
|
7
|
+
|
|
8
|
+
export const CANVAS_VIEW = {
|
|
9
|
+
single: "single",
|
|
10
|
+
stack: "stack",
|
|
11
|
+
files: "files",
|
|
12
|
+
todos: "todos",
|
|
13
|
+
scheduler: "scheduler",
|
|
14
|
+
wiki: "wiki",
|
|
15
|
+
skills: "skills",
|
|
16
|
+
roles: "roles",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
const VIEW_MODES = Object.values(CANVAS_VIEW);
|
|
20
|
+
|
|
21
|
+
export type CanvasViewMode = (typeof CANVAS_VIEW)[keyof typeof CANVAS_VIEW];
|
|
22
|
+
|
|
23
|
+
export function isCanvasViewMode(value: string): value is CanvasViewMode {
|
|
24
|
+
return (VIEW_MODES as readonly string[]).includes(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** All valid view mode values — single source of truth for guards and parsers. */
|
|
28
|
+
export const VALID_VIEW_MODES: ReadonlySet<string> = new Set(VIEW_MODES);
|
|
29
|
+
|
|
30
|
+
export const VIEW_MODE_STORAGE_KEY = "canvas_view_mode";
|
|
31
|
+
|
|
32
|
+
// Parse a value pulled out of localStorage. Anything other than the
|
|
33
|
+
// known modes — including null — falls back to "single".
|
|
34
|
+
export function parseStoredViewMode(stored: string | null): CanvasViewMode {
|
|
35
|
+
if (typeof stored === "string" && isCanvasViewMode(stored)) {
|
|
36
|
+
return stored;
|
|
37
|
+
}
|
|
38
|
+
return CANVAS_VIEW.single;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Map a Cmd/Ctrl + N keyboard shortcut digit to its view mode.
|
|
42
|
+
// Shortcut keys are 1-indexed into the VIEW_MODES array.
|
|
43
|
+
export function viewModeForShortcutKey(key: string): CanvasViewMode | null {
|
|
44
|
+
const index = Number(key) - 1;
|
|
45
|
+
return VIEW_MODES[index] ?? null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Read the bearer auth token that the server embeds into the
|
|
2
|
+
// `<meta name="mulmoclaude-auth" content="…">` tag of index.html (#272).
|
|
3
|
+
// Isolated in this tiny DOM-scoped module so it lives under
|
|
4
|
+
// `src/utils/dom/` where ESLint already configures browser globals —
|
|
5
|
+
// avoids promoting `src/main.ts` into the browser-globals override.
|
|
6
|
+
//
|
|
7
|
+
// Returns `null` when:
|
|
8
|
+
// - the meta tag is missing (placeholder never injected — shouldn't
|
|
9
|
+
// happen in production but guards against it)
|
|
10
|
+
// - the content attribute is empty (server embedded empty = no token
|
|
11
|
+
// available; every subsequent API call will 401, which is the
|
|
12
|
+
// correct dev-time signal)
|
|
13
|
+
|
|
14
|
+
export function readAuthTokenFromMeta(): string | null {
|
|
15
|
+
const meta = document.querySelector('meta[name="mulmoclaude-auth"]');
|
|
16
|
+
if (meta === null) return null;
|
|
17
|
+
const content = meta.getAttribute("content");
|
|
18
|
+
if (content === null || content === "") return null;
|
|
19
|
+
return content;
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Pure helper for "did this click happen outside both the trigger
|
|
2
|
+
// button and the popup body?" — used by every dismiss-on-outside
|
|
3
|
+
// popup. Lifted out so the boolean rule can be unit-tested without
|
|
4
|
+
// a real DOM.
|
|
5
|
+
|
|
6
|
+
export function isClickOutside(target: Node | null, buttonEl: HTMLElement | null, popupEl: HTMLElement | null): boolean {
|
|
7
|
+
if (!target) return false;
|
|
8
|
+
const insideButton = buttonEl?.contains(target) ?? false;
|
|
9
|
+
const insidePopup = popupEl?.contains(target) ?? false;
|
|
10
|
+
return !insideButton && !insidePopup;
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Click handler for rendered markdown / HTML bodies that opens
|
|
2
|
+
// external (cross-origin) http(s) links in a new tab instead of
|
|
3
|
+
// navigating the SPA away from itself.
|
|
4
|
+
//
|
|
5
|
+
// Split into a pure predicate (`isCrossOriginHttpUrl`) that's
|
|
6
|
+
// exhaustively unit-tested, and a thin DOM wrapper
|
|
7
|
+
// (`handleExternalLinkClick`) that reads the click event. Callers
|
|
8
|
+
// invoke the wrapper from their own `@click` handler and check the
|
|
9
|
+
// return value to decide whether to fall through to plugin-specific
|
|
10
|
+
// navigation.
|
|
11
|
+
|
|
12
|
+
// Pure predicate: is `href` an absolute http(s) URL pointing at an
|
|
13
|
+
// origin different from `currentOrigin`? Used by
|
|
14
|
+
// `handleExternalLinkClick` below, and directly by tests.
|
|
15
|
+
//
|
|
16
|
+
// Returns `false` for:
|
|
17
|
+
// - non-http schemes (mailto:, tel:, javascript:, file: …) — the
|
|
18
|
+
// browser's default behaviour is appropriate for those
|
|
19
|
+
// - same-origin URLs (including hash anchors resolved against the
|
|
20
|
+
// current page, which `anchor.href` normalises to a full URL)
|
|
21
|
+
// - malformed input that `URL` can't parse
|
|
22
|
+
export function isCrossOriginHttpUrl(href: string, currentOrigin: string): boolean {
|
|
23
|
+
if (!href.startsWith("http://") && !href.startsWith("https://")) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return new URL(href).origin !== currentOrigin;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// DOM click handler. Invoke from a view's `@click` listener on a
|
|
34
|
+
// rendered-markdown container. If the event targets an external
|
|
35
|
+
// http(s) link, the default navigation is cancelled and the link
|
|
36
|
+
// opens in a new tab with `noopener,noreferrer`; returns `true` so
|
|
37
|
+
// the caller knows the click was consumed. Returns `false` for
|
|
38
|
+
// every other case (not an anchor, internal link, modifier-key
|
|
39
|
+
// click, non-left-button, …) so the caller can continue with its
|
|
40
|
+
// own plugin-specific click handling (e.g. wiki internal links).
|
|
41
|
+
export function handleExternalLinkClick(event: MouseEvent): boolean {
|
|
42
|
+
if (event.button !== 0) return false;
|
|
43
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey) return false;
|
|
44
|
+
const target = event.target as HTMLElement | null;
|
|
45
|
+
if (!target) return false;
|
|
46
|
+
const anchor = target.closest("a");
|
|
47
|
+
if (!anchor) return false;
|
|
48
|
+
// `.href` (DOM property) is always a fully-resolved URL; contrast
|
|
49
|
+
// `getAttribute("href")` which returns the raw attribute string.
|
|
50
|
+
// Using the resolved form gives us reliable origin checks and
|
|
51
|
+
// normalises relative paths away.
|
|
52
|
+
const url = anchor.href;
|
|
53
|
+
if (!isCrossOriginHttpUrl(url, window.location.origin)) return false;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Small DOM helpers shared across components.
|
|
2
|
+
|
|
3
|
+
// Walk a container's descendants and return the first one that
|
|
4
|
+
// has both more vertical content than its visible height AND a
|
|
5
|
+
// CSS overflow that allows scrolling. Used so canvas-level arrow
|
|
6
|
+
// keys can scroll whichever inner element actually owns the
|
|
7
|
+
// scrollbar (e.g. a plugin's view component).
|
|
8
|
+
//
|
|
9
|
+
// Pure in the "no Vue / no module state" sense — it does touch the
|
|
10
|
+
// DOM, so its tests use a synthetic element graph rather than the
|
|
11
|
+
// real DOM.
|
|
12
|
+
export function findScrollableChild(container: HTMLElement): HTMLElement | null {
|
|
13
|
+
const children = container.querySelectorAll("*");
|
|
14
|
+
for (const el of children) {
|
|
15
|
+
const html = el as HTMLElement;
|
|
16
|
+
if (html.scrollHeight > html.clientHeight) {
|
|
17
|
+
const style = getComputedStyle(html);
|
|
18
|
+
if (style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll") {
|
|
19
|
+
return html;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Shared error helpers for the Vue side. Mirrors the server-side
|
|
2
|
+
// `server/utils/errors.ts` so the same helper is available wherever
|
|
3
|
+
// we handle caught exceptions.
|
|
4
|
+
//
|
|
5
|
+
// Use `errorMessage(err)` instead of inlining
|
|
6
|
+
// `err instanceof Error ? err.message : String(err)` — searching for
|
|
7
|
+
// one canonical helper is easier than grepping for the inline form.
|
|
8
|
+
|
|
9
|
+
export function errorMessage(err: unknown): string {
|
|
10
|
+
return err instanceof Error ? err.message : String(err);
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Pure helpers for persisting the FileTree expand/collapse state.
|
|
2
|
+
// Kept Vue-free so the parsing rules are unit-testable in isolation.
|
|
3
|
+
|
|
4
|
+
export const EXPANDED_DIRS_STORAGE_KEY = "files_expanded_dirs";
|
|
5
|
+
|
|
6
|
+
// Default: only the workspace root ("") is expanded — matches the
|
|
7
|
+
// pre-persistence behavior of FileTree.vue, where nested dirs start
|
|
8
|
+
// collapsed so opening Files mode doesn't render the whole tree.
|
|
9
|
+
const DEFAULT_EXPANDED: ReadonlyArray<string> = [""];
|
|
10
|
+
|
|
11
|
+
export function parseStoredExpandedDirs(raw: string | null): Set<string> {
|
|
12
|
+
if (raw === null) return new Set(DEFAULT_EXPANDED);
|
|
13
|
+
try {
|
|
14
|
+
const parsed: unknown = JSON.parse(raw);
|
|
15
|
+
if (!Array.isArray(parsed)) return new Set(DEFAULT_EXPANDED);
|
|
16
|
+
const strings = parsed.filter((v): v is string => typeof v === "string");
|
|
17
|
+
return new Set(strings);
|
|
18
|
+
} catch {
|
|
19
|
+
return new Set(DEFAULT_EXPANDED);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function serializeExpandedDirs(set: Set<string>): string {
|
|
24
|
+
return JSON.stringify([...set]);
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Strip filesystem-hostile chars from a string so it can safely be used
|
|
2
|
+
// as a browser download filename across Windows / macOS / Linux. Not a
|
|
3
|
+
// full slugifier — server-side slugification lives in
|
|
4
|
+
// `server/utils/slug.ts` and is applied before data hits the client.
|
|
5
|
+
// This helper is the last-line defensive escape for plugin views that
|
|
6
|
+
// build a download filename from arbitrary title text.
|
|
7
|
+
const UNSAFE_FILENAME_CHARS = /[/\\:*?"<>|]/g;
|
|
8
|
+
|
|
9
|
+
export function toSafeFilename(name: string, fallback = "download"): string {
|
|
10
|
+
const cleaned = name.replace(UNSAFE_FILENAME_CHARS, "_").trim();
|
|
11
|
+
return cleaned || fallback;
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TreeNode } from "../../types/fileTree";
|
|
2
|
+
import type { FileSortMode } from "../../composables/useFileSortMode";
|
|
3
|
+
|
|
4
|
+
// Sort tree children: directories always come before files; within
|
|
5
|
+
// each group, "name" is locale-aware alphabetical and "recent" is
|
|
6
|
+
// newest-first by modifiedMs (missing mtimes sort last, then tie-break
|
|
7
|
+
// on name so the order is deterministic).
|
|
8
|
+
export function sortChildren(children: readonly TreeNode[], mode: FileSortMode): TreeNode[] {
|
|
9
|
+
const copy = children.slice();
|
|
10
|
+
copy.sort((a, b) => {
|
|
11
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
12
|
+
if (mode === "recent") {
|
|
13
|
+
const am = a.modifiedMs ?? -Infinity;
|
|
14
|
+
const bm = b.modifiedMs ?? -Infinity;
|
|
15
|
+
if (am !== bm) return bm - am;
|
|
16
|
+
}
|
|
17
|
+
return a.name.localeCompare(b.name);
|
|
18
|
+
});
|
|
19
|
+
return copy;
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Synthesize a ToolResultComplete<SchedulerData> from raw scheduler
|
|
2
|
+
// items.json content so FilesView can render it with the scheduler
|
|
3
|
+
// plugin's calendar view. Extracted from FilesView.vue (#507 step 8).
|
|
4
|
+
|
|
5
|
+
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
6
|
+
import type { SchedulerData, ScheduledItem } from "../../plugins/scheduler/index";
|
|
7
|
+
import { WORKSPACE_FILES } from "../../config/workspacePaths";
|
|
8
|
+
import { isRecord } from "../types";
|
|
9
|
+
|
|
10
|
+
function isScheduledItem(value: unknown): value is ScheduledItem {
|
|
11
|
+
if (!isRecord(value)) return false;
|
|
12
|
+
if (typeof value.id !== "string") return false;
|
|
13
|
+
if (typeof value.title !== "string") return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isScheduledItemArray(value: unknown): value is ScheduledItem[] {
|
|
18
|
+
return Array.isArray(value) && value.every(isScheduledItem);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function toSchedulerResult(selectedPath: string | null, rawText: string | null): ToolResultComplete<SchedulerData> | null {
|
|
22
|
+
if (selectedPath !== WORKSPACE_FILES.schedulerItems) return null;
|
|
23
|
+
if (rawText === null) return null;
|
|
24
|
+
let parsed: unknown;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(rawText);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (!isScheduledItemArray(parsed)) return null;
|
|
31
|
+
return {
|
|
32
|
+
uuid: "files-scheduler-preview",
|
|
33
|
+
toolName: "manageScheduler",
|
|
34
|
+
message: WORKSPACE_FILES.schedulerItems,
|
|
35
|
+
title: "Scheduler",
|
|
36
|
+
data: { items: parsed },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Synthesize a ToolResultComplete<TodoData> from raw todos.json
|
|
2
|
+
// content so FilesView can render it with the TodoExplorer.
|
|
3
|
+
// Extracted from FilesView.vue (#507 step 8).
|
|
4
|
+
|
|
5
|
+
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
6
|
+
import type { StatusColumn, TodoData, TodoItem } from "../../plugins/todo/index";
|
|
7
|
+
import { WORKSPACE_FILES } from "../../config/workspacePaths";
|
|
8
|
+
import { isRecord } from "../types";
|
|
9
|
+
|
|
10
|
+
function isTodoItem(value: unknown): value is TodoItem {
|
|
11
|
+
if (!isRecord(value)) return false;
|
|
12
|
+
if (typeof value["id"] !== "string" || typeof value["text"] !== "string") return false;
|
|
13
|
+
if (typeof value["completed"] !== "boolean") return false;
|
|
14
|
+
if (typeof value["createdAt"] !== "number") return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isTodoItemArray(value: unknown): value is TodoItem[] {
|
|
19
|
+
return Array.isArray(value) && value.every(isTodoItem);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toTodoExplorerResult(selectedPath: string | null, rawText: string | null): ToolResultComplete<TodoData> | null {
|
|
23
|
+
if (selectedPath !== WORKSPACE_FILES.todosItems) return null;
|
|
24
|
+
if (rawText === null) return null;
|
|
25
|
+
let parsed: unknown;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(rawText);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const items: TodoItem[] = isTodoItemArray(parsed) ? parsed : [];
|
|
32
|
+
const columns: StatusColumn[] = [];
|
|
33
|
+
return {
|
|
34
|
+
uuid: "files-todo-preview",
|
|
35
|
+
toolName: "manageTodoList",
|
|
36
|
+
message: WORKSPACE_FILES.todosItems,
|
|
37
|
+
title: "Todo",
|
|
38
|
+
data: { items, columns },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Pure date/time formatting helpers for the Vue frontend.
|
|
2
|
+
// All functions are locale-aware on purpose; tests assert
|
|
3
|
+
// structural properties only, not exact strings.
|
|
4
|
+
|
|
5
|
+
/** "Apr 11 06:32" — short month + day + 24h time. */
|
|
6
|
+
export function formatDate(iso: string): string {
|
|
7
|
+
const date = new Date(iso);
|
|
8
|
+
return (
|
|
9
|
+
date.toLocaleDateString(undefined, { month: "short", day: "numeric" }) + " " + date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** "Apr 11 06:32" — same format as formatDate but from epoch ms. */
|
|
14
|
+
export function formatDateTime(epochMs: number): string {
|
|
15
|
+
return new Date(epochMs).toLocaleString(undefined, {
|
|
16
|
+
month: "short",
|
|
17
|
+
day: "numeric",
|
|
18
|
+
hour: "2-digit",
|
|
19
|
+
minute: "2-digit",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** "06:32:15" — locale time string from epoch ms. */
|
|
24
|
+
export function formatTime(epochMs: number): string {
|
|
25
|
+
return new Date(epochMs).toLocaleTimeString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** "06:32" — short HH:MM. Accepts Date, epoch ms, or ISO string. */
|
|
29
|
+
export function formatShortTime(value: Date | number | string): string {
|
|
30
|
+
try {
|
|
31
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
32
|
+
return date.toLocaleTimeString([], {
|
|
33
|
+
hour: "2-digit",
|
|
34
|
+
minute: "2-digit",
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "Apr 11" — short month + day. Accepts Date, epoch ms, or ISO string. */
|
|
42
|
+
export function formatShortDate(value: Date | number | string): string {
|
|
43
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
44
|
+
return date.toLocaleDateString(undefined, {
|
|
45
|
+
month: "short",
|
|
46
|
+
day: "numeric",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when two Dates fall on the same calendar day. */
|
|
51
|
+
export function isSameDay(left: Date, right: Date): boolean {
|
|
52
|
+
return left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True when the given Date is today. */
|
|
56
|
+
export function isToday(date: Date): boolean {
|
|
57
|
+
return isSameDay(date, new Date());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** "14:32" for today, "Apr 16 14:32" for past dates. Works with
|
|
61
|
+
* both epoch ms (number) and ISO strings. */
|
|
62
|
+
export function formatSmartTime(value: number | string): string {
|
|
63
|
+
const date = new Date(value);
|
|
64
|
+
const time = formatShortTime(date);
|
|
65
|
+
if (isToday(date)) return time;
|
|
66
|
+
return `${formatShortDate(date)} ${time}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ONE_MINUTE = 60_000;
|
|
70
|
+
const ONE_HOUR = 3_600_000;
|
|
71
|
+
const ONE_DAY = 86_400_000;
|
|
72
|
+
|
|
73
|
+
/** "just now", "5m ago", "2h ago", "Apr 11" — relative time from ISO string. */
|
|
74
|
+
export function formatRelativeTime(iso: string): string {
|
|
75
|
+
try {
|
|
76
|
+
const date = new Date(iso);
|
|
77
|
+
const diffMs = Date.now() - date.getTime();
|
|
78
|
+
if (diffMs < ONE_MINUTE) return "just now";
|
|
79
|
+
if (diffMs < ONE_HOUR) return `${Math.floor(diffMs / ONE_MINUTE)}m ago`;
|
|
80
|
+
if (diffMs < ONE_DAY) return `${Math.floor(diffMs / ONE_HOUR)}h ago`;
|
|
81
|
+
return formatShortDate(date);
|
|
82
|
+
} catch {
|
|
83
|
+
return iso;
|
|
84
|
+
}
|
|
85
|
+
}
|