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,294 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { readdir, stat } from "fs/promises";
|
|
4
|
+
import { readTextSafe } from "../../utils/files/safe.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { workspacePath } from "../../workspace/workspace.js";
|
|
7
|
+
import { WORKSPACE_PATHS } from "../../workspace/paths.js";
|
|
8
|
+
import { readSessionMeta as readSessionMetaIO, readSessionJsonl, sessionJsonlAbsPath, sessionMetaAbsPath } from "../../utils/files/session-io.js";
|
|
9
|
+
import { readManifest } from "../../workspace/chat-index/indexer.js";
|
|
10
|
+
import { resolveWithinRoot } from "../../utils/files/safe.js";
|
|
11
|
+
import type { ChatIndexEntry } from "../../workspace/chat-index/types.js";
|
|
12
|
+
import { markRead, getSession } from "../../events/session-store/index.js";
|
|
13
|
+
import { notFound } from "../../utils/httpError.js";
|
|
14
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
15
|
+
import { EVENT_TYPES } from "../../../src/types/events.js";
|
|
16
|
+
import type { SessionOrigin } from "../../../src/types/session.js";
|
|
17
|
+
import { env } from "../../system/env.js";
|
|
18
|
+
import { ONE_DAY_MS } from "../../utils/time.js";
|
|
19
|
+
import { encodeCursor, parseCursor, sessionChangeMs } from "./sessionsCursor.js";
|
|
20
|
+
|
|
21
|
+
interface SessionMeta {
|
|
22
|
+
roleId: string;
|
|
23
|
+
startedAt: string;
|
|
24
|
+
firstUserMessage?: string;
|
|
25
|
+
hasUnread?: boolean;
|
|
26
|
+
origin?: SessionOrigin;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readSessionMeta(__chatDir: string, id: string): Promise<SessionMeta | null> {
|
|
30
|
+
// Try new-style .json meta first
|
|
31
|
+
const meta = await readSessionMetaIO(id);
|
|
32
|
+
if (meta?.roleId && meta?.startedAt) {
|
|
33
|
+
return meta as SessionMeta;
|
|
34
|
+
}
|
|
35
|
+
// Legacy: read first line of .jsonl
|
|
36
|
+
const jsonl = await readSessionJsonl(id);
|
|
37
|
+
if (jsonl) {
|
|
38
|
+
const first = jsonl.split("\n").find(Boolean);
|
|
39
|
+
if (first) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(first);
|
|
42
|
+
if (parsed.roleId && parsed.startedAt) return parsed;
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SessionSummary {
|
|
52
|
+
id: string;
|
|
53
|
+
roleId: string;
|
|
54
|
+
startedAt: string;
|
|
55
|
+
// ISO timestamp of the jsonl file's most recent mtime — i.e. the
|
|
56
|
+
// last time the session had an event appended. Clients sort the
|
|
57
|
+
// sidebar history list by this so active sessions float to the top.
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
preview: string;
|
|
60
|
+
// Populated when the chat indexer has produced a summary for this
|
|
61
|
+
// session. The frontend renders `summary` as a smaller second line
|
|
62
|
+
// under the preview in the history popup. See #123.
|
|
63
|
+
summary?: string;
|
|
64
|
+
keywords?: string[];
|
|
65
|
+
// Where this session originated (#486). Missing = "human".
|
|
66
|
+
origin?: SessionOrigin;
|
|
67
|
+
// Live state from the in-memory session store. Absent when the
|
|
68
|
+
// session has no active entry in the store (i.e. idle / historical).
|
|
69
|
+
isRunning?: boolean;
|
|
70
|
+
hasUnread?: boolean;
|
|
71
|
+
statusMessage?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Public response envelope for GET /api/sessions (issue #205).
|
|
75
|
+
//
|
|
76
|
+
// `cursor` — opaque string clients echo back as `?since=` on the
|
|
77
|
+
// next call to receive only sessions that have changed.
|
|
78
|
+
// `deletedIds` — always `[]` for now (no session-delete code path
|
|
79
|
+
// exists yet). Kept in the shape so the client already
|
|
80
|
+
// merges it; when deletion lands, populating this will
|
|
81
|
+
// be a server-only change.
|
|
82
|
+
interface SessionsResponse {
|
|
83
|
+
sessions: SessionSummary[];
|
|
84
|
+
cursor: string;
|
|
85
|
+
deletedIds: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface SessionsQuery {
|
|
89
|
+
since?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const router = Router();
|
|
93
|
+
|
|
94
|
+
// Sessions older than this are excluded from the listing. Set
|
|
95
|
+
// SESSIONS_LIST_WINDOW_DAYS to override (0 = no cutoff).
|
|
96
|
+
const WINDOW_MS = env.sessionsListWindowDays * ONE_DAY_MS;
|
|
97
|
+
|
|
98
|
+
// Read the full session list off disk. Each row carries its
|
|
99
|
+
// `changeMs` — the later of the jsonl mtime and the chat-index
|
|
100
|
+
// `indexedAt` — so the handler can filter against `?since=` and
|
|
101
|
+
// compute the new cursor without re-statting anything.
|
|
102
|
+
export async function loadAllSessions(): Promise<{ summary: SessionSummary; changeMs: number }[]> {
|
|
103
|
+
const chatDir = WORKSPACE_PATHS.chat;
|
|
104
|
+
const manifest = await readManifest(workspacePath);
|
|
105
|
+
const indexById = new Map<string, ChatIndexEntry>(manifest.entries.map((e) => [e.id, e]));
|
|
106
|
+
const cutoff = WINDOW_MS > 0 ? Date.now() - WINDOW_MS : 0;
|
|
107
|
+
|
|
108
|
+
const files = (await readdir(chatDir)).filter((f) => f.endsWith(".jsonl"));
|
|
109
|
+
const rows = await Promise.all(
|
|
110
|
+
files.map(async (file) => {
|
|
111
|
+
const id = file.replace(".jsonl", "");
|
|
112
|
+
try {
|
|
113
|
+
// stat only — no readFile on .jsonl content
|
|
114
|
+
const fileStat = await stat(sessionJsonlAbsPath(id));
|
|
115
|
+
if (cutoff > 0 && fileStat.mtimeMs < cutoff) return null;
|
|
116
|
+
|
|
117
|
+
const meta = await readSessionMeta(chatDir, id);
|
|
118
|
+
if (!meta) return null;
|
|
119
|
+
|
|
120
|
+
// The meta sidecar bumps its mtime on hasUnread / origin
|
|
121
|
+
// writes — feed it into changeMs so cursor-based refetches
|
|
122
|
+
// pick up drains of background generations (which only touch
|
|
123
|
+
// meta, not the jsonl). Missing stat (brand-new session
|
|
124
|
+
// before its first meta write) contributes 0.
|
|
125
|
+
const metaMtimeMs = await stat(sessionMetaAbsPath(id))
|
|
126
|
+
.then((s) => s.mtimeMs)
|
|
127
|
+
.catch(() => 0);
|
|
128
|
+
|
|
129
|
+
const indexEntry = indexById.get(id);
|
|
130
|
+
// Prefer AI title → meta.firstUserMessage → empty.
|
|
131
|
+
// `summary` and `keywords` are spread conditionally
|
|
132
|
+
// to respect the server tsconfig's
|
|
133
|
+
// exactOptionalPropertyTypes.
|
|
134
|
+
const preview = indexEntry?.title ?? meta.firstUserMessage ?? "";
|
|
135
|
+
|
|
136
|
+
const live = getSession(id);
|
|
137
|
+
const summary: SessionSummary = {
|
|
138
|
+
id,
|
|
139
|
+
roleId: meta.roleId,
|
|
140
|
+
startedAt: meta.startedAt,
|
|
141
|
+
updatedAt: new Date(fileStat.mtimeMs).toISOString(),
|
|
142
|
+
preview,
|
|
143
|
+
hasUnread: live?.hasUnread ?? meta.hasUnread ?? false,
|
|
144
|
+
};
|
|
145
|
+
if (meta.origin) summary.origin = meta.origin;
|
|
146
|
+
if (indexEntry?.summary !== undefined) summary.summary = indexEntry.summary;
|
|
147
|
+
if (indexEntry?.keywords !== undefined) summary.keywords = indexEntry.keywords;
|
|
148
|
+
if (live) {
|
|
149
|
+
// Background generations (image/audio/movie) keep the session
|
|
150
|
+
// "busy" even when the agent turn has ended, so the sidebar
|
|
151
|
+
// indicator stays lit across view navigation.
|
|
152
|
+
summary.isRunning = live.isRunning || Object.keys(live.pendingGenerations).length > 0;
|
|
153
|
+
summary.statusMessage = live.statusMessage;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
summary,
|
|
157
|
+
changeMs: sessionChangeMs(fileStat.mtimeMs, indexEntry?.indexedAt, metaMtimeMs),
|
|
158
|
+
};
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
return rows.filter((r): r is { summary: SessionSummary; changeMs: number } => r !== null);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
router.get(API_ROUTES.sessions.list, async (req: Request<object, SessionsResponse, object, SessionsQuery>, res: Response<SessionsResponse>) => {
|
|
168
|
+
try {
|
|
169
|
+
const sinceMs = parseCursor(req.query.since);
|
|
170
|
+
const rows = await loadAllSessions();
|
|
171
|
+
|
|
172
|
+
// Cursor = max(changeMs) across every visible session, regardless
|
|
173
|
+
// of whether it's in the diff. Echoing the same cursor back on an
|
|
174
|
+
// empty diff (nothing changed since `?since=`) is fine; the
|
|
175
|
+
// client no-ops.
|
|
176
|
+
const maxChangeMs = rows.reduce((acc, r) => Math.max(acc, r.changeMs), 0);
|
|
177
|
+
|
|
178
|
+
const filtered = sinceMs > 0 ? rows.filter((r) => r.changeMs > sinceMs) : rows;
|
|
179
|
+
|
|
180
|
+
const sessions = filtered.map((r) => r.summary);
|
|
181
|
+
sessions.sort((a, b) => {
|
|
182
|
+
const byUpdated = new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
183
|
+
if (byUpdated !== 0) return byUpdated;
|
|
184
|
+
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
res.json({
|
|
188
|
+
sessions,
|
|
189
|
+
cursor: encodeCursor(maxChangeMs),
|
|
190
|
+
// No session-delete code path exists today — issue #205 picked
|
|
191
|
+
// approach A (tombstones) so the client already merges this
|
|
192
|
+
// field; populating it becomes a server-only change when
|
|
193
|
+
// deletion lands.
|
|
194
|
+
deletedIds: [],
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
res.json({ sessions: [], cursor: encodeCursor(0), deletedIds: [] });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
interface SessionIdParams {
|
|
202
|
+
id: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface SessionErrorResponse {
|
|
206
|
+
error: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
router.get(API_ROUTES.sessions.detail, async (req: Request<SessionIdParams>, res: Response<unknown[] | SessionErrorResponse>) => {
|
|
210
|
+
const { id } = req.params;
|
|
211
|
+
const chatDir = WORKSPACE_PATHS.chat;
|
|
212
|
+
try {
|
|
213
|
+
const meta = await readSessionMeta(chatDir, id);
|
|
214
|
+
const content = await readSessionJsonl(id);
|
|
215
|
+
if (!content) {
|
|
216
|
+
notFound(res, `Session ${id} not found`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const entries = (
|
|
220
|
+
await Promise.all(
|
|
221
|
+
content
|
|
222
|
+
.split("\n")
|
|
223
|
+
.filter(Boolean)
|
|
224
|
+
.map(async (line) => {
|
|
225
|
+
try {
|
|
226
|
+
const entry = JSON.parse(line);
|
|
227
|
+
// Skip legacy metadata entries now stored in .json
|
|
228
|
+
if (entry.type === EVENT_TYPES.sessionMeta || entry.type === EVENT_TYPES.claudeSessionId) return null;
|
|
229
|
+
// For presentMulmoScript results, re-read the script from disk
|
|
230
|
+
if (
|
|
231
|
+
entry.source === "tool" &&
|
|
232
|
+
entry.type === EVENT_TYPES.toolResult &&
|
|
233
|
+
entry.result?.toolName === "presentMulmoScript" &&
|
|
234
|
+
entry.result?.data?.filePath
|
|
235
|
+
) {
|
|
236
|
+
try {
|
|
237
|
+
// Realpath-based traversal check defeats symlink
|
|
238
|
+
// escapes — see resolveWithinRoot in utils/fs.ts.
|
|
239
|
+
// Resolve the stories dir's realpath so the
|
|
240
|
+
// boundary check works even when stories/ itself
|
|
241
|
+
// is a legitimate symlink to another disk.
|
|
242
|
+
const storiesDir = path.resolve(WORKSPACE_PATHS.stories);
|
|
243
|
+
let storiesReal: string;
|
|
244
|
+
try {
|
|
245
|
+
storiesReal = fs.realpathSync(storiesDir);
|
|
246
|
+
} catch {
|
|
247
|
+
return entry;
|
|
248
|
+
}
|
|
249
|
+
const scriptRelPath: string = entry.result.data.filePath;
|
|
250
|
+
if (path.isAbsolute(scriptRelPath)) return entry;
|
|
251
|
+
// Strip optional "stories/" prefix so the
|
|
252
|
+
// remainder is relative to storiesReal.
|
|
253
|
+
const relFromStories = scriptRelPath.startsWith("stories/") ? scriptRelPath.slice("stories/".length) : scriptRelPath;
|
|
254
|
+
const scriptPath = resolveWithinRoot(storiesReal, relFromStories);
|
|
255
|
+
if (!scriptPath) return entry;
|
|
256
|
+
const scriptJson = (await readTextSafe(scriptPath)) ?? "";
|
|
257
|
+
return {
|
|
258
|
+
...entry,
|
|
259
|
+
result: {
|
|
260
|
+
...entry.result,
|
|
261
|
+
data: {
|
|
262
|
+
...entry.result.data,
|
|
263
|
+
script: JSON.parse(scriptJson),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
} catch {
|
|
268
|
+
// file missing — return original entry
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return entry;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
)
|
|
277
|
+
).filter(Boolean);
|
|
278
|
+
// Prepend metadata as session_meta entry for the frontend
|
|
279
|
+
const result = meta ? [{ type: EVENT_TYPES.sessionMeta, ...meta }, ...entries] : entries;
|
|
280
|
+
res.json(result);
|
|
281
|
+
} catch {
|
|
282
|
+
notFound(res, "Session not found");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Mark a session as read (clears the hasUnread flag in the session store).
|
|
287
|
+
// Awaits persistence so the response only arrives after the disk write
|
|
288
|
+
// completes — prevents the client from refetching stale hasUnread values.
|
|
289
|
+
router.post(API_ROUTES.sessions.markRead, async (req: Request<SessionIdParams>, res: Response<{ ok: boolean }>) => {
|
|
290
|
+
await markRead(req.params.id);
|
|
291
|
+
res.json({ ok: true });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export default router;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Cursor logic for `GET /api/sessions?since=<cursor>` (issue #205).
|
|
2
|
+
//
|
|
3
|
+
// Kept separate from `sessions.ts` so the pure logic can be unit
|
|
4
|
+
// tested without an Express harness.
|
|
5
|
+
//
|
|
6
|
+
// The cursor is deliberately opaque to the client: today it encodes
|
|
7
|
+
// the max "change timestamp" (ms since epoch) as `"v1:<ms>"`, where a
|
|
8
|
+
// session's change timestamp is `max(jsonlMtimeMs, indexedAtMs)`. We
|
|
9
|
+
// prefix with `v1:` so a future encoding change (e.g. adding a
|
|
10
|
+
// deletion generation counter for approach A when deletion lands)
|
|
11
|
+
// can bump the prefix without clients caring — they always echo back
|
|
12
|
+
// whatever the server handed them.
|
|
13
|
+
|
|
14
|
+
const CURSOR_PREFIX = "v1:";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encode a change timestamp (ms) as an opaque cursor string.
|
|
18
|
+
*
|
|
19
|
+
* `changeMs <= 0` is allowed and yields `"v1:0"` — that's the
|
|
20
|
+
* "beginning of time" cursor a client will never hold but which we
|
|
21
|
+
* fall back to when an incoming cursor is malformed.
|
|
22
|
+
*/
|
|
23
|
+
export function encodeCursor(changeMs: number): string {
|
|
24
|
+
const ms = Number.isFinite(changeMs) && changeMs > 0 ? Math.floor(changeMs) : 0;
|
|
25
|
+
return `${CURSOR_PREFIX}${ms}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse an incoming `?since=` cursor back to the ms timestamp it
|
|
30
|
+
* encodes. Anything the client sends that we don't recognise — old
|
|
31
|
+
* format, truncated, typo, empty — returns 0 so the client gets a
|
|
32
|
+
* full resend instead of a broken sidebar. This is intentionally
|
|
33
|
+
* forgiving; the failure mode is "downloads slightly more than
|
|
34
|
+
* needed once" which is the behaviour clients had pre-#205 anyway.
|
|
35
|
+
*/
|
|
36
|
+
export function parseCursor(raw: unknown): number {
|
|
37
|
+
if (typeof raw !== "string") return 0;
|
|
38
|
+
if (!raw.startsWith(CURSOR_PREFIX)) return 0;
|
|
39
|
+
const n = Number(raw.slice(CURSOR_PREFIX.length));
|
|
40
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute the per-session "change timestamp" in ms — the latest of:
|
|
45
|
+
* - jsonl mtime (new user/assistant turn)
|
|
46
|
+
* - chat-index `indexedAt` (AI-generated title / summary, updated in
|
|
47
|
+
* the background and doesn't touch the jsonl)
|
|
48
|
+
* - meta json mtime (hasUnread flip, origin update — also sidecar
|
|
49
|
+
* to the jsonl)
|
|
50
|
+
*
|
|
51
|
+
* Missing / malformed `indexedAt` or `metaMtimeMs` contributes 0 so
|
|
52
|
+
* they don't pull the timestamp backward.
|
|
53
|
+
*/
|
|
54
|
+
export function sessionChangeMs(jsonlMtimeMs: number, indexedAtIso: string | undefined, metaMtimeMs: number | undefined = undefined): number {
|
|
55
|
+
const indexedAtMs = indexedAtIso !== undefined ? new Date(indexedAtIso).getTime() : NaN;
|
|
56
|
+
const safeIndexed = Number.isFinite(indexedAtMs) ? indexedAtMs : 0;
|
|
57
|
+
const safeMeta = typeof metaMtimeMs === "number" && Number.isFinite(metaMtimeMs) ? metaMtimeMs : 0;
|
|
58
|
+
return Math.max(jsonlMtimeMs, safeIndexed, safeMeta);
|
|
59
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// REST surface for Claude Code skills.
|
|
2
|
+
//
|
|
3
|
+
// GET /api/skills → { skills: SkillSummary[] } phase 0
|
|
4
|
+
// GET /api/skills/:name → { skill: Skill } | 404 phase 0
|
|
5
|
+
// POST /api/skills → { saved: true, path } | 400/409 phase 1
|
|
6
|
+
// PUT /api/skills/:name → { updated: true, path } | 400/403/404 phase 2
|
|
7
|
+
// DELETE /api/skills/:name → { deleted: true } | 400/403/404 phase 1
|
|
8
|
+
//
|
|
9
|
+
// Discovery reads both ~/.claude/skills/ (user) and
|
|
10
|
+
// <workspace>/.claude/skills/ (project); project wins on name
|
|
11
|
+
// collision. Writes are confined to the project scope —
|
|
12
|
+
// `saveProjectSkill` / `updateProjectSkill` / `deleteProjectSkill`
|
|
13
|
+
// enforce that.
|
|
14
|
+
|
|
15
|
+
import { Router, Request, Response } from "express";
|
|
16
|
+
import { deleteProjectSkill, discoverSkills, saveProjectSkill, updateProjectSkill } from "../../workspace/skills/index.js";
|
|
17
|
+
import type { Skill, SkillSummary } from "../../workspace/skills/index.js";
|
|
18
|
+
import { workspacePath } from "../../workspace/workspace.js";
|
|
19
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
20
|
+
import { log } from "../../system/logger/index.js";
|
|
21
|
+
import { refreshScheduledSkills } from "../../workspace/skills/scheduler.js";
|
|
22
|
+
import { logBackgroundError } from "../../utils/logBackgroundError.js";
|
|
23
|
+
import { badRequest, conflict, forbidden, notFound } from "../../utils/httpError.js";
|
|
24
|
+
|
|
25
|
+
const router = Router();
|
|
26
|
+
|
|
27
|
+
interface SkillsListResponse {
|
|
28
|
+
skills: SkillSummary[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SkillDetailResponse {
|
|
32
|
+
skill: Skill;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ErrorResponse {
|
|
36
|
+
error: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SaveSkillBody {
|
|
40
|
+
name?: unknown;
|
|
41
|
+
description?: unknown;
|
|
42
|
+
body?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SaveSkillResponse {
|
|
46
|
+
saved: true;
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface DeleteSkillResponse {
|
|
51
|
+
deleted: true;
|
|
52
|
+
name: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
router.get(API_ROUTES.skills.list, async (_req: Request, res: Response<SkillsListResponse>) => {
|
|
56
|
+
const skills = await discoverSkills({ workspaceRoot: workspacePath });
|
|
57
|
+
res.json({
|
|
58
|
+
skills: skills.map((s) => ({
|
|
59
|
+
name: s.name,
|
|
60
|
+
description: s.description,
|
|
61
|
+
source: s.source,
|
|
62
|
+
})),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
router.get(API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res: Response<SkillDetailResponse | ErrorResponse>) => {
|
|
67
|
+
const skills = await discoverSkills({ workspaceRoot: workspacePath });
|
|
68
|
+
const skill = skills.find((s) => s.name === req.params.name);
|
|
69
|
+
if (!skill) {
|
|
70
|
+
notFound(res, `skill not found: ${req.params.name}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
res.json({ skill });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
router.post(API_ROUTES.skills.create, async (req: Request<object, unknown, SaveSkillBody>, res: Response<SaveSkillResponse | ErrorResponse>) => {
|
|
77
|
+
const { name, description, body } = req.body ?? {};
|
|
78
|
+
if (typeof name !== "string") {
|
|
79
|
+
badRequest(res, "name must be a string");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof description !== "string") {
|
|
83
|
+
badRequest(res, "description must be a string");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (typeof body !== "string") {
|
|
87
|
+
badRequest(res, "body must be a string");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const result = await saveProjectSkill({
|
|
91
|
+
workspaceRoot: workspacePath,
|
|
92
|
+
name,
|
|
93
|
+
description,
|
|
94
|
+
body,
|
|
95
|
+
});
|
|
96
|
+
if (result.kind === "saved") {
|
|
97
|
+
log.info("skills", "saved", { name });
|
|
98
|
+
refreshScheduledSkills().catch(logBackgroundError("skills"));
|
|
99
|
+
res.json({ saved: true, path: result.path });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (result.kind === "invalid-slug") {
|
|
103
|
+
badRequest(
|
|
104
|
+
res,
|
|
105
|
+
`invalid slug: "${result.slug}". Use lowercase letters, digits, and hyphens (1-64 chars, no leading/trailing hyphen, no consecutive hyphens).`,
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (result.kind === "missing-field") {
|
|
110
|
+
badRequest(res, `${result.field} must be a non-empty string`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (result.kind === "exists") {
|
|
114
|
+
conflict(res, `skill already exists: ${result.name}. Choose a different name or delete the existing one first.`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
interface UpdateSkillBody {
|
|
119
|
+
description?: unknown;
|
|
120
|
+
body?: unknown;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface UpdateSkillResponse {
|
|
124
|
+
updated: true;
|
|
125
|
+
path: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
router.put(API_ROUTES.skills.update, async (req: Request<{ name: string }, unknown, UpdateSkillBody>, res: Response<UpdateSkillResponse | ErrorResponse>) => {
|
|
129
|
+
const { name } = req.params;
|
|
130
|
+
const { description, body } = req.body ?? {};
|
|
131
|
+
if (typeof description !== "string") {
|
|
132
|
+
badRequest(res, "description must be a string");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (typeof body !== "string") {
|
|
136
|
+
badRequest(res, "body must be a string");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const result = await updateProjectSkill({
|
|
140
|
+
workspaceRoot: workspacePath,
|
|
141
|
+
name,
|
|
142
|
+
description,
|
|
143
|
+
body,
|
|
144
|
+
});
|
|
145
|
+
if (result.kind === "updated") {
|
|
146
|
+
log.info("skills", "updated", { name });
|
|
147
|
+
refreshScheduledSkills().catch(logBackgroundError("skills"));
|
|
148
|
+
res.json({ updated: true, path: result.path });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (result.kind === "invalid-slug") {
|
|
152
|
+
badRequest(res, `invalid slug: "${result.slug}"`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (result.kind === "missing-field") {
|
|
156
|
+
badRequest(res, `${result.field} must be a non-empty string`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (result.kind === "user-scope") {
|
|
160
|
+
forbidden(res, `cannot update user-scope skill "${result.name}" — only project-scope skills are writable.`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (result.kind === "not-found") {
|
|
164
|
+
notFound(res, `skill not found: ${result.name}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
router.delete(API_ROUTES.skills.remove, async (req: Request<{ name: string }>, res: Response<DeleteSkillResponse | ErrorResponse>) => {
|
|
169
|
+
const result = await deleteProjectSkill({
|
|
170
|
+
workspaceRoot: workspacePath,
|
|
171
|
+
name: req.params.name,
|
|
172
|
+
});
|
|
173
|
+
if (result.kind === "deleted") {
|
|
174
|
+
log.info("skills", "deleted", { name: result.name });
|
|
175
|
+
refreshScheduledSkills().catch(logBackgroundError("skills"));
|
|
176
|
+
res.json({ deleted: true, name: result.name });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (result.kind === "invalid-slug") {
|
|
180
|
+
badRequest(res, `invalid slug: "${result.slug}"`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (result.kind === "user-scope") {
|
|
184
|
+
forbidden(
|
|
185
|
+
res,
|
|
186
|
+
`cannot delete user-scope skill "${result.name}" — only project-scope skills under ~/mulmoclaude/.claude/skills/ are writable from MulmoClaude.`,
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (result.kind === "not-found") {
|
|
191
|
+
notFound(res, `skill not found: ${result.name}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
export default router;
|