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,61 @@
|
|
|
1
|
+
// Helpers for the most common error-response pattern in route
|
|
2
|
+
// handlers:
|
|
3
|
+
//
|
|
4
|
+
// return res.status(400).json({ error: "..." });
|
|
5
|
+
//
|
|
6
|
+
// Before consolidation this appeared in ~100 places, each handler
|
|
7
|
+
// hand-rolling the `{ error: string }` body and picking a status
|
|
8
|
+
// code. The helpers below keep the call site to one line while
|
|
9
|
+
// centralising the response shape so cross-cutting concerns
|
|
10
|
+
// (e.g. adding a `requestId` or `timestamp` later) only need to
|
|
11
|
+
// change here.
|
|
12
|
+
//
|
|
13
|
+
// All helpers return the `Response` object so callers can write
|
|
14
|
+
// either of:
|
|
15
|
+
//
|
|
16
|
+
// return badRequest(res, "filePath is required");
|
|
17
|
+
//
|
|
18
|
+
// badRequest(res, "filePath is required");
|
|
19
|
+
// return;
|
|
20
|
+
//
|
|
21
|
+
// Non-`{ error: string }` shapes (e.g. `{ success: false, message }`
|
|
22
|
+
// returned by a handful of legacy routes, or multi-field error
|
|
23
|
+
// bodies) stay as explicit `res.status(N).json(...)` calls — the
|
|
24
|
+
// helpers intentionally cover only the dominant pattern.
|
|
25
|
+
|
|
26
|
+
import type { Response } from "express";
|
|
27
|
+
|
|
28
|
+
/** Send a `{ error: string }` body with the given HTTP status. */
|
|
29
|
+
export function sendError(res: Response, status: number, error: string): Response {
|
|
30
|
+
return res.status(status).json({ error });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 400 Bad Request — malformed input, missing required field, etc. */
|
|
34
|
+
export function badRequest(res: Response, error: string): Response {
|
|
35
|
+
return sendError(res, 400, error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 401 Unauthorized — missing or invalid credentials. */
|
|
39
|
+
export function unauthorized(res: Response, error: string): Response {
|
|
40
|
+
return sendError(res, 401, error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 403 Forbidden — auth present but not authorised for the resource. */
|
|
44
|
+
export function forbidden(res: Response, error: string): Response {
|
|
45
|
+
return sendError(res, 403, error);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 404 Not Found — resource doesn't exist. */
|
|
49
|
+
export function notFound(res: Response, error: string): Response {
|
|
50
|
+
return sendError(res, 404, error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 409 Conflict — duplicate, concurrent modification, already running, etc. */
|
|
54
|
+
export function conflict(res: Response, error: string): Response {
|
|
55
|
+
return sendError(res, 409, error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 500 Internal Server Error — unexpected failure on the server side. */
|
|
59
|
+
export function serverError(res: Response, error: string): Response {
|
|
60
|
+
return sendError(res, 500, error);
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Unique-ID generation for persisted records. Previously duplicated
|
|
2
|
+
// in schedulerHandlers, todosHandlers, and todosItemsHandlers —
|
|
3
|
+
// consolidated here as part of the server/utils grouping (#350 CLAUDE.md).
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a short, unique, human-scannable ID.
|
|
9
|
+
*
|
|
10
|
+
* Format: `<prefix>_<epochMs>_<6 random hex chars>`. The prefix
|
|
11
|
+
* is required so IDs from different domains (todo, scheduler, column)
|
|
12
|
+
* are visually distinguishable in logs and JSON files.
|
|
13
|
+
*/
|
|
14
|
+
export function makeId(prefix: string): string {
|
|
15
|
+
return `${prefix}_${Date.now()}_${randomBytes(3).toString("hex")}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Tolerant JSON extraction from Claude CLI text output. Claude
|
|
2
|
+
// often wraps JSON in a ```json fenced block or precedes it with
|
|
3
|
+
// conversational text. These helpers find and parse the first
|
|
4
|
+
// valid JSON object regardless of surrounding prose.
|
|
5
|
+
//
|
|
6
|
+
// Previously lived in workspace/journal/archivist.ts; moved here
|
|
7
|
+
// so any module that calls the Claude CLI can reuse them.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract the first JSON object from a Claude CLI response.
|
|
11
|
+
*
|
|
12
|
+
* Strategy:
|
|
13
|
+
* 1. Look for a ```json fenced block — most reliable when present.
|
|
14
|
+
* 2. Fall back to the first balanced `{...}` block in the raw text.
|
|
15
|
+
* 3. Return `null` if neither yields valid JSON.
|
|
16
|
+
*/
|
|
17
|
+
export function extractJsonObject(raw: string): unknown | null {
|
|
18
|
+
const fencedBody = findFencedJsonBody(raw);
|
|
19
|
+
if (fencedBody !== null) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fencedBody);
|
|
22
|
+
} catch {
|
|
23
|
+
// fall through to scan
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const balanced = findBalancedBraceBlock(raw);
|
|
27
|
+
if (balanced === null) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(balanced);
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find the first balanced `{...}` substring, respecting JSON string
|
|
37
|
+
* escapes. Uses a char-by-char scan (no regex) to avoid slow-regex
|
|
38
|
+
* lint warnings and backtracking risks on large LLM output.
|
|
39
|
+
*/
|
|
40
|
+
export function findBalancedBraceBlock(raw: string): string | null {
|
|
41
|
+
const start = raw.indexOf("{");
|
|
42
|
+
if (start === -1) return null;
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let inString = false;
|
|
45
|
+
let escape = false;
|
|
46
|
+
for (let i = start; i < raw.length; i++) {
|
|
47
|
+
const ch = raw[i];
|
|
48
|
+
if (escape) {
|
|
49
|
+
escape = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch === "\\") {
|
|
53
|
+
escape = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === '"') {
|
|
57
|
+
inString = !inString;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (inString) continue;
|
|
61
|
+
if (ch === "{") depth++;
|
|
62
|
+
if (ch === "}" && --depth === 0) return raw.slice(start, i + 1);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract the body of the first ` ```json ... ``` ` fenced block.
|
|
69
|
+
* Returns `null` if no fenced block is found.
|
|
70
|
+
*/
|
|
71
|
+
export function findFencedJsonBody(raw: string): string | null {
|
|
72
|
+
const OPEN = "```json";
|
|
73
|
+
const CLOSE = "```";
|
|
74
|
+
const openIdx = raw.indexOf(OPEN);
|
|
75
|
+
if (openIdx === -1) return null;
|
|
76
|
+
const afterOpen = openIdx + OPEN.length;
|
|
77
|
+
const bodyStart = raw.indexOf("\n", afterOpen);
|
|
78
|
+
if (bodyStart === -1) return null;
|
|
79
|
+
const closeIdx = raw.indexOf(CLOSE, bodyStart + 1);
|
|
80
|
+
if (closeIdx === -1) return null;
|
|
81
|
+
const bodyEnd = raw[closeIdx - 1] === "\n" ? closeIdx - 1 : closeIdx;
|
|
82
|
+
return raw.slice(bodyStart + 1, bodyEnd);
|
|
83
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { log } from "../system/logger/index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build a `.catch` handler for a fire-and-forget background job that
|
|
5
|
+
* logs the failure under the given prefix. Consolidates the
|
|
6
|
+
* "unexpected error in background" pattern used across journal,
|
|
7
|
+
* chat-index, wiki-backlinks, tool-trace, etc.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
*
|
|
11
|
+
* maybeRunJournal({ ... }).catch(logBackgroundError("journal"));
|
|
12
|
+
*
|
|
13
|
+
* The handler never rethrows — the caller's promise chain is
|
|
14
|
+
* terminated cleanly so nothing propagates into the request path.
|
|
15
|
+
*/
|
|
16
|
+
export function logBackgroundError(prefix: string): (err: unknown) => void {
|
|
17
|
+
return (err) => {
|
|
18
|
+
log.warn(prefix, "unexpected error in background", {
|
|
19
|
+
error: String(err),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Markdown link rewriting utilities. Originally in
|
|
2
|
+
// workspace/journal/linkRewrite.ts; moved to utils/ so any module
|
|
3
|
+
// (journal, wiki, sources) can reuse them.
|
|
4
|
+
//
|
|
5
|
+
// All functions are pure — no filesystem access.
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rewrite every `[text](/workspace/path)` link in `content` to a
|
|
11
|
+
* true-relative path computed from the given current-file location.
|
|
12
|
+
* Non-workspace-absolute links (true relative, external URLs,
|
|
13
|
+
* anchors) are left untouched.
|
|
14
|
+
*/
|
|
15
|
+
export function rewriteWorkspaceLinks(currentFileWsPath: string, content: string): string {
|
|
16
|
+
const currentDir = path.posix.dirname(currentFileWsPath);
|
|
17
|
+
return rewriteMarkdownLinks(content, (href) => {
|
|
18
|
+
if (href.startsWith("//")) return href;
|
|
19
|
+
if (!href.startsWith("/")) return href;
|
|
20
|
+
const target = href.slice(1);
|
|
21
|
+
if (target.length === 0) return href;
|
|
22
|
+
const { pathPart, suffix } = splitFragmentAndQuery(target);
|
|
23
|
+
const rel = path.posix.relative(currentDir, pathPart);
|
|
24
|
+
const safeRel = rel.length > 0 ? rel : ".";
|
|
25
|
+
return `${safeRel}${suffix}`;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Walk through `input` and invoke `rewrite` for every `[text](href)`
|
|
31
|
+
* it encounters, substituting the returned href. Character-level scan
|
|
32
|
+
* (no regex) to stay lint-clean.
|
|
33
|
+
*/
|
|
34
|
+
export function rewriteMarkdownLinks(input: string, rewrite: (href: string) => string): string {
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i < input.length) {
|
|
38
|
+
if (input[i] !== "[") {
|
|
39
|
+
parts.push(input[i]);
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const closeBracket = input.indexOf("]", i + 1);
|
|
44
|
+
if (closeBracket === -1) {
|
|
45
|
+
parts.push(input.slice(i));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (input[closeBracket + 1] !== "(") {
|
|
49
|
+
parts.push(input.slice(i, closeBracket + 1));
|
|
50
|
+
i = closeBracket + 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const openParen = closeBracket + 1;
|
|
54
|
+
const closeParen = input.indexOf(")", openParen + 1);
|
|
55
|
+
if (closeParen === -1) {
|
|
56
|
+
parts.push(input.slice(i));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const linkText = input.slice(i + 1, closeBracket);
|
|
60
|
+
const href = input.slice(openParen + 1, closeParen);
|
|
61
|
+
parts.push(`[${linkText}](${rewrite(href)})`);
|
|
62
|
+
i = closeParen + 1;
|
|
63
|
+
}
|
|
64
|
+
return parts.join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Split a trailing `#fragment` or `?query` off a path so the caller
|
|
69
|
+
* can rewrite the path portion and concatenate the suffix back.
|
|
70
|
+
*/
|
|
71
|
+
export function splitFragmentAndQuery(s: string): {
|
|
72
|
+
pathPart: string;
|
|
73
|
+
suffix: string;
|
|
74
|
+
} {
|
|
75
|
+
const hashIdx = s.indexOf("#");
|
|
76
|
+
const queryIdx = s.indexOf("?");
|
|
77
|
+
let cut = -1;
|
|
78
|
+
if (hashIdx !== -1) cut = hashIdx;
|
|
79
|
+
if (queryIdx !== -1 && (cut === -1 || queryIdx < cut)) cut = queryIdx;
|
|
80
|
+
if (cut === -1) return { pathPart: s, suffix: "" };
|
|
81
|
+
return { pathPart: s.slice(0, cut), suffix: s.slice(cut) };
|
|
82
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Express request helpers — shared query/param extraction.
|
|
2
|
+
//
|
|
3
|
+
// Centralizes patterns that were duplicated across route handlers
|
|
4
|
+
// (3+ different ways to read `req.query.session`).
|
|
5
|
+
|
|
6
|
+
// Use a minimal interface so the helpers work with any Express
|
|
7
|
+
// Request generic (Request<object, ...>, Request<Params, ...>, etc.)
|
|
8
|
+
// without type incompatibility.
|
|
9
|
+
interface HasQuery {
|
|
10
|
+
query: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract the session ID from `req.query.session`.
|
|
15
|
+
* Returns the string value, or "" if missing/non-string.
|
|
16
|
+
*/
|
|
17
|
+
export function getSessionQuery(req: HasQuery): string {
|
|
18
|
+
const raw = req.query.session;
|
|
19
|
+
return typeof raw === "string" ? raw : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract an optional string query parameter.
|
|
24
|
+
* Returns the string value, or undefined if missing/non-string.
|
|
25
|
+
*/
|
|
26
|
+
export function getOptionalStringQuery(req: HasQuery, key: string): string | undefined {
|
|
27
|
+
const raw = req.query[key];
|
|
28
|
+
return typeof raw === "string" ? raw : undefined;
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
|
|
3
|
+
// Bits of sha256 kept as the non-ASCII fallback id. 16 base64url chars =
|
|
4
|
+
// 96 bits; birthday-collision expectation lives at ~2^48 entries, so
|
|
5
|
+
// collisions are effectively impossible for any realistic workspace.
|
|
6
|
+
const NON_ASCII_HASH_LEN = 16;
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line no-control-regex
|
|
9
|
+
const NON_ASCII_RE = /[^\x00-\x7F]/;
|
|
10
|
+
|
|
11
|
+
export function hasNonAscii(input: string): boolean {
|
|
12
|
+
return NON_ASCII_RE.test(input);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Deterministic short hash for inputs that can't be represented as an
|
|
16
|
+
// ASCII slug. base64url is URL-safe and denser than hex.
|
|
17
|
+
export function hashSlug(input: string, length: number = NON_ASCII_HASH_LEN): string {
|
|
18
|
+
return createHash("sha256").update(input, "utf-8").digest("base64url").slice(0, length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Validates a slug: lowercase alphanumeric + hyphens, 1–64 chars,
|
|
22
|
+
// no leading/trailing hyphen, no consecutive hyphens. Previously
|
|
23
|
+
// duplicated in sources/paths.ts and skills/paths.ts.
|
|
24
|
+
export function isValidSlug(slug: string): boolean {
|
|
25
|
+
if (typeof slug !== "string") return false;
|
|
26
|
+
if (slug.length === 0 || slug.length > 64) return false;
|
|
27
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(slug)) return false;
|
|
28
|
+
if (slug.includes("--")) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function slugify(title: string, defaultSlug = "page", maxLength = 60): string {
|
|
33
|
+
const asciiSlug = title
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
36
|
+
.replace(/^-|-$/g, "")
|
|
37
|
+
.slice(0, maxLength);
|
|
38
|
+
|
|
39
|
+
if (!hasNonAscii(title)) return asciiSlug || defaultSlug;
|
|
40
|
+
|
|
41
|
+
const hash = hashSlug(title.trim());
|
|
42
|
+
// Preserve a meaningful ASCII prefix (e.g. "doing (進行中)" → "doing-<hash>")
|
|
43
|
+
// only when at least 3 chars survived the sanitise step — a shorter
|
|
44
|
+
// prefix wouldn't help readers distinguish entries.
|
|
45
|
+
if (asciiSlug.length >= 3) {
|
|
46
|
+
const prefixMax = Math.max(0, maxLength - hash.length - 1);
|
|
47
|
+
return `${asciiSlug.slice(0, prefixMax)}-${hash}`;
|
|
48
|
+
}
|
|
49
|
+
return hash;
|
|
50
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Helpers for formatting errors from `spawn`-ed Claude CLI subprocesses.
|
|
2
|
+
// Previously duplicated in chat-index/summarizer, sources/summarize,
|
|
3
|
+
// and sources/classifier — each with its own log prefix but identical
|
|
4
|
+
// logic. Consolidated as part of the server/utils grouping.
|
|
5
|
+
|
|
6
|
+
import { isRecord } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const PREVIEW_LEN = 500;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract a structured error message from Claude CLI JSON stdout.
|
|
12
|
+
*
|
|
13
|
+
* The Claude CLI writes a JSON envelope on stdout when it exits
|
|
14
|
+
* with an error (budget exhaustion, auth failure, etc.). This
|
|
15
|
+
* function extracts the human-readable reason from that envelope.
|
|
16
|
+
* Returns `null` if stdout is not parseable JSON or the envelope
|
|
17
|
+
* does not indicate an error.
|
|
18
|
+
*/
|
|
19
|
+
export function extractClaudeErrorMessage(stdout: string): string | null {
|
|
20
|
+
const text = stdout.trim();
|
|
21
|
+
if (!text) return null;
|
|
22
|
+
let parsed: unknown;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(text);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (!isRecord(parsed)) return null;
|
|
29
|
+
if (parsed.is_error !== true) return null;
|
|
30
|
+
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
|
|
31
|
+
const joined = parsed.errors.filter((e): e is string => typeof e === "string").join("; ");
|
|
32
|
+
if (joined.length > 0) return joined;
|
|
33
|
+
}
|
|
34
|
+
const subtype = typeof parsed.subtype === "string" ? parsed.subtype : "";
|
|
35
|
+
const result = typeof parsed.result === "string" ? parsed.result : "";
|
|
36
|
+
if (subtype && result) return `${subtype}: ${result}`;
|
|
37
|
+
return subtype || result || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a human-readable error message from a Claude CLI spawn failure.
|
|
42
|
+
*
|
|
43
|
+
* Tries structured JSON extraction first (stdout), then falls back to
|
|
44
|
+
* stderr (plain text), then stdout as a last resort. The `prefix` is
|
|
45
|
+
* prepended for log-grep-ability (e.g. `"[chat-index]"`,
|
|
46
|
+
* `"[sources/classifier]"`).
|
|
47
|
+
*/
|
|
48
|
+
export function formatSpawnFailure(prefix: string, code: number | null, stdout: string, stderr: string): string {
|
|
49
|
+
const structured = extractClaudeErrorMessage(stdout);
|
|
50
|
+
if (structured) {
|
|
51
|
+
return `${prefix} claude exited ${code}: ${structured}`;
|
|
52
|
+
}
|
|
53
|
+
const trimmedStderr = stderr.trim();
|
|
54
|
+
if (trimmedStderr.length > 0) {
|
|
55
|
+
return `${prefix} claude exited ${code}: ${trimmedStderr.slice(0, PREVIEW_LEN)}`;
|
|
56
|
+
}
|
|
57
|
+
const trimmedStdout = stdout.trim();
|
|
58
|
+
if (trimmedStdout.length > 0) {
|
|
59
|
+
return `${prefix} claude exited ${code}: ${trimmedStdout.slice(0, PREVIEW_LEN)}`;
|
|
60
|
+
}
|
|
61
|
+
return `${prefix} claude exited ${code}: no error output`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Common time constants in milliseconds. Avoids magic numbers like
|
|
2
|
+
// 3_600_000 scattered across the codebase.
|
|
3
|
+
//
|
|
4
|
+
// All server-side code should import from here instead of using raw
|
|
5
|
+
// numeric literals. When a specific duration is needed (e.g. a
|
|
6
|
+
// 5-second timeout), express it as `5 * ONE_SECOND_MS`.
|
|
7
|
+
|
|
8
|
+
export const ONE_SECOND_MS = 1_000;
|
|
9
|
+
export const ONE_MINUTE_MS = 60_000;
|
|
10
|
+
export const ONE_HOUR_MS = 3_600_000;
|
|
11
|
+
export const ONE_DAY_MS = 86_400_000;
|
|
12
|
+
|
|
13
|
+
/** Map time-unit suffixes (s/m/h) to milliseconds. */
|
|
14
|
+
export const TIME_UNIT_MS: Record<string, number> = {
|
|
15
|
+
s: ONE_SECOND_MS,
|
|
16
|
+
m: ONE_MINUTE_MS,
|
|
17
|
+
h: ONE_HOUR_MS,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ── Common timeout presets ──────────────────────────────────────
|
|
21
|
+
// Named timeouts for recurring patterns. Prefer these over inline
|
|
22
|
+
// `5 * ONE_SECOND_MS` when the same value is used in 3+ places.
|
|
23
|
+
|
|
24
|
+
/** Quick subprocess probe (docker ps, libreoffice --version, etc.) */
|
|
25
|
+
export const SUBPROCESS_PROBE_TIMEOUT_MS = 5 * ONE_SECOND_MS;
|
|
26
|
+
|
|
27
|
+
/** Heavy subprocess work (libreoffice conversion, etc.) */
|
|
28
|
+
export const SUBPROCESS_WORK_TIMEOUT_MS = ONE_MINUTE_MS;
|
|
29
|
+
|
|
30
|
+
/** CLI subprocess timeout (claude -p for summarization, etc.) */
|
|
31
|
+
export const CLI_SUBPROCESS_TIMEOUT_MS = 5 * ONE_MINUTE_MS;
|
|
32
|
+
|
|
33
|
+
/** Maximum one-shot notification delay */
|
|
34
|
+
export const MAX_NOTIFICATION_DELAY_SEC = 3_600; // 1 hour in seconds
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shared runtime type guards (#504).
|
|
2
|
+
//
|
|
3
|
+
// Centralised here to eliminate 40+ hand-written inline checks
|
|
4
|
+
// scattered across server/ and src/. Import from this module
|
|
5
|
+
// instead of writing `typeof x !== "object" || x === null`.
|
|
6
|
+
|
|
7
|
+
/** Narrow `unknown` to a plain object (not null, not array). */
|
|
8
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Narrow `unknown` to any object (not null, arrays allowed).
|
|
13
|
+
* Use `isRecord` when you need to access string keys. */
|
|
14
|
+
export function isObj(value: unknown): value is object {
|
|
15
|
+
return typeof value === "object" && value !== null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Non-empty string after trimming whitespace. */
|
|
19
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
20
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Record whose values are all strings. */
|
|
24
|
+
export function isStringRecord(value: unknown): value is Record<string, string> {
|
|
25
|
+
if (!isRecord(value)) return false;
|
|
26
|
+
return Object.values(value).every((v) => typeof v === "string");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** String array (every element is a string). */
|
|
30
|
+
export function isStringArray(value: unknown): value is string[] {
|
|
31
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Error-like object with a `code` property (e.g. Node.js fs errors). */
|
|
35
|
+
export function isErrorWithCode(value: unknown): value is { code: string; message?: string } {
|
|
36
|
+
return isRecord(value) && typeof value.code === "string";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Check that a record has a specific key with a string value. */
|
|
40
|
+
export function hasStringProp<K extends string>(value: unknown, key: K): value is Record<K, string> & Record<string, unknown> {
|
|
41
|
+
return isRecord(value) && typeof value[key] === "string";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Check that a record has a specific key with a number value. */
|
|
45
|
+
export function hasNumberProp<K extends string>(value: unknown, key: K): value is Record<K, number> & Record<string, unknown> {
|
|
46
|
+
return isRecord(value) && typeof value[key] === "number";
|
|
47
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Public entry point for the chat index. The agent route calls
|
|
2
|
+
// `maybeIndexSession({ sessionId, activeSessionIds })` from its
|
|
3
|
+
// `finally` block — fire-and-forget. This module:
|
|
4
|
+
//
|
|
5
|
+
// - skips sessions still being written by a concurrent request
|
|
6
|
+
// - holds a per-session lock so double-fires for the same id
|
|
7
|
+
// become no-ops (two sessions can still index in parallel)
|
|
8
|
+
// - catches ClaudeCliNotFoundError and disables itself for the
|
|
9
|
+
// rest of the process lifetime to avoid spamming warnings
|
|
10
|
+
// - catches unexpected errors and logs them so nothing bubbles
|
|
11
|
+
// back into the request handler
|
|
12
|
+
//
|
|
13
|
+
// All functions accept an explicit `workspaceRoot` so tests can
|
|
14
|
+
// point at a `mkdtempSync` directory.
|
|
15
|
+
|
|
16
|
+
import { workspacePath as defaultWorkspacePath } from "../workspace.js";
|
|
17
|
+
import { ClaudeCliNotFoundError } from "../journal/archivist.js";
|
|
18
|
+
import { indexSession, listSessionIds, type IndexerDeps } from "./indexer.js";
|
|
19
|
+
import { log } from "../../system/logger/index.js";
|
|
20
|
+
|
|
21
|
+
// Per-session lock. Indexing different sessions in parallel is
|
|
22
|
+
// fine; indexing the same session twice concurrently would just
|
|
23
|
+
// burn CLI budget for no benefit.
|
|
24
|
+
const running = new Set<string>();
|
|
25
|
+
|
|
26
|
+
// Flipped once we hit ENOENT on the `claude` CLI so we stop
|
|
27
|
+
// trying for the lifetime of the server process. Reset on
|
|
28
|
+
// restart.
|
|
29
|
+
let disabled = false;
|
|
30
|
+
|
|
31
|
+
export interface MaybeIndexSessionOptions {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
// Skip indexing if the session is still being appended to by a
|
|
34
|
+
// concurrent /api/agent request — the jsonl may be mid-write.
|
|
35
|
+
// Ignored when `force` is true so manual rebuild runs can
|
|
36
|
+
// re-index even a live session (accepting that the transcript
|
|
37
|
+
// may be slightly out of date).
|
|
38
|
+
activeSessionIds?: ReadonlySet<string>;
|
|
39
|
+
workspaceRoot?: string;
|
|
40
|
+
deps?: IndexerDeps;
|
|
41
|
+
// Bypass the activeSessionIds guard and the isFresh throttle
|
|
42
|
+
// for this call. The per-session lock and the `disabled`
|
|
43
|
+
// sentinel are still respected — forcing doesn't help if the
|
|
44
|
+
// claude CLI is missing or the same session is already in
|
|
45
|
+
// flight.
|
|
46
|
+
force?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fire-and-forget entry point. Errors are swallowed here; a
|
|
50
|
+
// defensive `.catch(...)` at the call site is still recommended.
|
|
51
|
+
export async function maybeIndexSession(opts: MaybeIndexSessionOptions): Promise<void> {
|
|
52
|
+
if (disabled) return;
|
|
53
|
+
|
|
54
|
+
const { sessionId } = opts;
|
|
55
|
+
const force = opts.force === true;
|
|
56
|
+
if (!force && opts.activeSessionIds?.has(sessionId)) return;
|
|
57
|
+
if (running.has(sessionId)) return;
|
|
58
|
+
|
|
59
|
+
// Thread `force` through the indexer via IndexerDeps so the
|
|
60
|
+
// freshness throttle is also bypassed on forced runs.
|
|
61
|
+
const effectiveDeps: IndexerDeps = {
|
|
62
|
+
...(opts.deps ?? {}),
|
|
63
|
+
...(force ? { force: true } : {}),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
running.add(sessionId);
|
|
67
|
+
try {
|
|
68
|
+
await indexSession(opts.workspaceRoot ?? defaultWorkspacePath, sessionId, effectiveDeps);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err instanceof ClaudeCliNotFoundError) {
|
|
71
|
+
disabled = true;
|
|
72
|
+
log.warn("chat-index", err.message);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
log.warn("chat-index", "unexpected failure, continuing", {
|
|
76
|
+
error: String(err),
|
|
77
|
+
});
|
|
78
|
+
} finally {
|
|
79
|
+
running.delete(sessionId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Debug helper: index every session jsonl under workspace/chat/
|
|
84
|
+
// sequentially with `force: true`. Used by the manual rebuild
|
|
85
|
+
// endpoint and the CHAT_INDEX_FORCE_RUN_ON_STARTUP switch so the
|
|
86
|
+
// user can populate titles for existing sessions without waiting
|
|
87
|
+
// for each one to be revisited.
|
|
88
|
+
//
|
|
89
|
+
// Returns counts for logging. Errors on individual sessions do
|
|
90
|
+
// not stop the walk — the failure is logged and processing
|
|
91
|
+
// continues.
|
|
92
|
+
export interface BackfillResult {
|
|
93
|
+
total: number;
|
|
94
|
+
indexed: number;
|
|
95
|
+
skipped: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function backfillAllSessions(
|
|
99
|
+
opts: {
|
|
100
|
+
workspaceRoot?: string;
|
|
101
|
+
deps?: IndexerDeps;
|
|
102
|
+
} = {},
|
|
103
|
+
): Promise<BackfillResult> {
|
|
104
|
+
const workspaceRoot = opts.workspaceRoot ?? defaultWorkspacePath;
|
|
105
|
+
const ids = await listSessionIds(workspaceRoot);
|
|
106
|
+
const result: BackfillResult = {
|
|
107
|
+
total: ids.length,
|
|
108
|
+
indexed: 0,
|
|
109
|
+
skipped: 0,
|
|
110
|
+
};
|
|
111
|
+
for (const sessionId of ids) {
|
|
112
|
+
if (disabled) {
|
|
113
|
+
result.skipped++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const entry = await indexSession(workspaceRoot, sessionId, {
|
|
118
|
+
...(opts.deps ?? {}),
|
|
119
|
+
force: true,
|
|
120
|
+
});
|
|
121
|
+
if (entry) {
|
|
122
|
+
result.indexed++;
|
|
123
|
+
log.info("chat-index", "indexed", {
|
|
124
|
+
sessionId,
|
|
125
|
+
title: entry.title,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
result.skipped++;
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err instanceof ClaudeCliNotFoundError) {
|
|
132
|
+
disabled = true;
|
|
133
|
+
log.warn("chat-index", err.message);
|
|
134
|
+
result.skipped++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
result.skipped++;
|
|
138
|
+
log.warn("chat-index", "failed to index", {
|
|
139
|
+
sessionId,
|
|
140
|
+
error: String(err),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Internal hook: tests need to reset the module-level `disabled`
|
|
148
|
+
// and `running` state between cases because node:test doesn't
|
|
149
|
+
// reload modules. Not part of the public runtime contract.
|
|
150
|
+
export function __resetForTests(): void {
|
|
151
|
+
disabled = false;
|
|
152
|
+
running.clear();
|
|
153
|
+
}
|