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,209 @@
|
|
|
1
|
+
// Per-session indexing logic. `indexSession` summarizes a single
|
|
2
|
+
// session jsonl and writes both a per-session file and a manifest
|
|
3
|
+
// upsert to workspace/chat/index/. `readManifest` is a tiny helper
|
|
4
|
+
// the sessions route uses to join entries into its /api/sessions
|
|
5
|
+
// response.
|
|
6
|
+
//
|
|
7
|
+
// All functions take an explicit `workspaceRoot` so tests can point
|
|
8
|
+
// at a `mkdtempSync` directory without touching the real
|
|
9
|
+
// ~/mulmoclaude.
|
|
10
|
+
|
|
11
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
12
|
+
import { defaultSummarize, loadJsonlInput, type SummarizeFn } from "./summarizer.js";
|
|
13
|
+
import { chatDirFor, indexEntryPathFor, manifestPathFor, sessionJsonlPathFor, sessionMetaPathFor } from "./paths.js";
|
|
14
|
+
import type { ChatIndexEntry, ChatIndexManifest } from "./types.js";
|
|
15
|
+
import { writeJsonAtomic } from "../../utils/files/index.js";
|
|
16
|
+
import { DEFAULT_ROLE_ID } from "../../../src/config/roles.js";
|
|
17
|
+
import { ONE_MINUTE_MS } from "../../utils/time.js";
|
|
18
|
+
import { isRecord } from "../../utils/types.js";
|
|
19
|
+
|
|
20
|
+
// Freshness throttle: a session whose existing index entry is
|
|
21
|
+
// newer than this is skipped. The 15-minute window is a compromise
|
|
22
|
+
// — long enough that a single conversation doesn't re-summarize
|
|
23
|
+
// every turn, short enough that a user who leaves for lunch and
|
|
24
|
+
// comes back sees the title refresh.
|
|
25
|
+
export const MIN_INDEX_INTERVAL_MS = 15 * ONE_MINUTE_MS;
|
|
26
|
+
|
|
27
|
+
// Injection points for tests. Defaults are the production spawn +
|
|
28
|
+
// wall-clock.
|
|
29
|
+
export interface IndexerDeps {
|
|
30
|
+
summarize?: SummarizeFn;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
minIntervalMs?: number;
|
|
33
|
+
// Bypass the `isFresh` freshness throttle. Used by the
|
|
34
|
+
// backfill helper and the debug trigger endpoint so a manual
|
|
35
|
+
// "rebuild everything" run doesn't silently skip entries that
|
|
36
|
+
// happen to be within the 15-minute window.
|
|
37
|
+
force?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- manifest I/O ---------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export async function readManifest(workspaceRoot: string): Promise<ChatIndexManifest> {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(manifestPathFor(workspaceRoot), "utf-8");
|
|
45
|
+
const parsed: unknown = JSON.parse(raw);
|
|
46
|
+
if (isManifest(parsed)) return parsed;
|
|
47
|
+
return { version: 1, entries: [] };
|
|
48
|
+
} catch {
|
|
49
|
+
return { version: 1, entries: [] };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isManifest(raw: unknown): raw is ChatIndexManifest {
|
|
54
|
+
if (!isRecord(raw)) return false;
|
|
55
|
+
const o = raw as Record<string, unknown>;
|
|
56
|
+
return o.version === 1 && Array.isArray(o.entries);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// In-process mutex serializing the read-modify-write sequence on
|
|
60
|
+
// the shared manifest file. Two concurrent `indexSession` calls
|
|
61
|
+
// for different session ids would otherwise both read an empty
|
|
62
|
+
// manifest, each append their own entry, and the last writer would
|
|
63
|
+
// clobber the first. Chain-based mutex keeps it simple and fits
|
|
64
|
+
// this module's single-process assumption.
|
|
65
|
+
let manifestMutex: Promise<void> = Promise.resolve();
|
|
66
|
+
|
|
67
|
+
async function withManifestLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
68
|
+
const prev = manifestMutex;
|
|
69
|
+
let release: () => void = () => {};
|
|
70
|
+
manifestMutex = new Promise<void>((resolve) => {
|
|
71
|
+
release = resolve;
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
await prev;
|
|
75
|
+
return await fn();
|
|
76
|
+
} finally {
|
|
77
|
+
release();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Atomic write: stage to a per-call unique tmp file and rename.
|
|
82
|
+
// The unique suffix is belt-and-suspenders — the mutex above
|
|
83
|
+
// already serializes callers within this process, but a unique
|
|
84
|
+
// name means the rename can't collide even if a stray .tmp file
|
|
85
|
+
// is left behind by a previous crashed run.
|
|
86
|
+
async function writeManifestAtomic(workspaceRoot: string, m: ChatIndexManifest): Promise<void> {
|
|
87
|
+
// `uniqueTmp` belt-and-suspenders: the in-process mutex above
|
|
88
|
+
// already serializes callers, but a unique tmp name means the
|
|
89
|
+
// rename can't collide even if a stray .tmp file is left behind
|
|
90
|
+
// by a previous crashed run.
|
|
91
|
+
await writeJsonAtomic(manifestPathFor(workspaceRoot), m, {
|
|
92
|
+
uniqueTmp: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Read, mutate, and write the manifest under the in-process lock
|
|
97
|
+
// so concurrent callers cannot lose each other's updates.
|
|
98
|
+
export async function updateManifest(workspaceRoot: string, mutator: (m: ChatIndexManifest) => ChatIndexManifest): Promise<ChatIndexManifest> {
|
|
99
|
+
return withManifestLock(async () => {
|
|
100
|
+
const current = await readManifest(workspaceRoot);
|
|
101
|
+
const next = mutator(current);
|
|
102
|
+
await writeManifestAtomic(workspaceRoot, next);
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- freshness check ------------------------------------------------
|
|
108
|
+
|
|
109
|
+
// A session is "fresh" when its per-session index file exists and
|
|
110
|
+
// was written less than `minIntervalMs` ago. Fresh sessions are
|
|
111
|
+
// skipped so a long conversation doesn't spam the CLI on every
|
|
112
|
+
// turn.
|
|
113
|
+
export async function isFresh(workspaceRoot: string, sessionId: string, now: number, minIntervalMs: number): Promise<boolean> {
|
|
114
|
+
try {
|
|
115
|
+
const raw = await readFile(indexEntryPathFor(workspaceRoot, sessionId), "utf-8");
|
|
116
|
+
const entry: unknown = JSON.parse(raw);
|
|
117
|
+
if (!isRecord(entry)) return false;
|
|
118
|
+
const indexedAt = (entry as Record<string, unknown>).indexedAt;
|
|
119
|
+
if (typeof indexedAt !== "string") return false;
|
|
120
|
+
const ts = Date.parse(indexedAt);
|
|
121
|
+
if (Number.isNaN(ts)) return false;
|
|
122
|
+
return now - ts < minIntervalMs;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- session metadata ----------------------------------------------
|
|
129
|
+
|
|
130
|
+
interface SessionMeta {
|
|
131
|
+
roleId?: string;
|
|
132
|
+
startedAt?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readSessionMeta(workspaceRoot: string, sessionId: string): Promise<SessionMeta> {
|
|
136
|
+
try {
|
|
137
|
+
const raw = await readFile(sessionMetaPathFor(workspaceRoot, sessionId), "utf-8");
|
|
138
|
+
const parsed: unknown = JSON.parse(raw);
|
|
139
|
+
if (!isRecord(parsed)) return {};
|
|
140
|
+
const o = parsed as Record<string, unknown>;
|
|
141
|
+
return {
|
|
142
|
+
roleId: typeof o.roleId === "string" ? o.roleId : undefined,
|
|
143
|
+
startedAt: typeof o.startedAt === "string" ? o.startedAt : undefined,
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// List every session id that has a .jsonl file in the workspace
|
|
151
|
+
// chat dir. Used by the backfill helper.
|
|
152
|
+
export async function listSessionIds(workspaceRoot: string): Promise<string[]> {
|
|
153
|
+
try {
|
|
154
|
+
const files = await readdir(chatDirFor(workspaceRoot));
|
|
155
|
+
return files.filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
|
|
156
|
+
} catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- the core indexSession call ------------------------------------
|
|
162
|
+
|
|
163
|
+
// Index (or re-index) a single session. Returns the entry on
|
|
164
|
+
// success, or null if the session was skipped (fresh, empty,
|
|
165
|
+
// missing). The only exception that escapes is
|
|
166
|
+
// `ClaudeCliNotFoundError` — the caller uses it to disable the
|
|
167
|
+
// module for the rest of the process lifetime.
|
|
168
|
+
export async function indexSession(workspaceRoot: string, sessionId: string, deps: IndexerDeps = {}): Promise<ChatIndexEntry | null> {
|
|
169
|
+
const summarize = deps.summarize ?? defaultSummarize;
|
|
170
|
+
const now = (deps.now ?? Date.now)();
|
|
171
|
+
const minInterval = deps.minIntervalMs ?? MIN_INDEX_INTERVAL_MS;
|
|
172
|
+
const force = deps.force === true;
|
|
173
|
+
|
|
174
|
+
if (!force && (await isFresh(workspaceRoot, sessionId, now, minInterval))) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const input = await loadJsonlInput(sessionJsonlPathFor(workspaceRoot, sessionId));
|
|
179
|
+
if (!input.trim()) return null;
|
|
180
|
+
|
|
181
|
+
const summary = await summarize(input);
|
|
182
|
+
const meta = await readSessionMeta(workspaceRoot, sessionId);
|
|
183
|
+
|
|
184
|
+
const entry: ChatIndexEntry = {
|
|
185
|
+
id: sessionId,
|
|
186
|
+
roleId: meta.roleId ?? DEFAULT_ROLE_ID,
|
|
187
|
+
startedAt: meta.startedAt ?? new Date(now).toISOString(),
|
|
188
|
+
indexedAt: new Date(now).toISOString(),
|
|
189
|
+
title: summary.title,
|
|
190
|
+
summary: summary.summary,
|
|
191
|
+
keywords: summary.keywords,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Per-session file is written first so partial progress survives
|
|
195
|
+
// a crash between the two writes: the next run can still observe
|
|
196
|
+
// the fresh entry via isFresh and skip it.
|
|
197
|
+
await writeJsonAtomic(indexEntryPathFor(workspaceRoot, sessionId), entry);
|
|
198
|
+
|
|
199
|
+
// Upsert into manifest under the in-process lock: replace any
|
|
200
|
+
// prior entry with the same id, sort newest-first by startedAt.
|
|
201
|
+
await updateManifest(workspaceRoot, (current) => {
|
|
202
|
+
const filtered = current.entries.filter((e) => e.id !== sessionId);
|
|
203
|
+
filtered.push(entry);
|
|
204
|
+
filtered.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
|
|
205
|
+
return { version: 1, entries: filtered };
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return entry;
|
|
209
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Pure path helpers for the chat index cache. Kept in their own
|
|
2
|
+
// file so tests can compute expected paths without needing the
|
|
3
|
+
// summarizer / indexer modules (which transitively pull in the
|
|
4
|
+
// claude CLI spawn code).
|
|
5
|
+
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
export const CHAT_DIR = "chat";
|
|
9
|
+
export const INDEX_DIR = "index";
|
|
10
|
+
export const MANIFEST_FILE = "manifest.json";
|
|
11
|
+
|
|
12
|
+
export function chatDirFor(workspaceRoot: string): string {
|
|
13
|
+
return path.join(workspaceRoot, CHAT_DIR);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function indexDirFor(workspaceRoot: string): string {
|
|
17
|
+
return path.join(chatDirFor(workspaceRoot), INDEX_DIR);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sessionJsonlPathFor(workspaceRoot: string, sessionId: string): string {
|
|
21
|
+
return path.join(chatDirFor(workspaceRoot), `${sessionId}.jsonl`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sessionMetaPathFor(workspaceRoot: string, sessionId: string): string {
|
|
25
|
+
return path.join(chatDirFor(workspaceRoot), `${sessionId}.json`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function indexEntryPathFor(workspaceRoot: string, sessionId: string): string {
|
|
29
|
+
return path.join(indexDirFor(workspaceRoot), `${sessionId}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function manifestPathFor(workspaceRoot: string): string {
|
|
33
|
+
return path.join(indexDirFor(workspaceRoot), MANIFEST_FILE);
|
|
34
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Summarizes a single session jsonl into a title / summary /
|
|
2
|
+
// keywords triple using the Claude Code CLI. Cherry-picked and
|
|
3
|
+
// trimmed from the closed PR #94.
|
|
4
|
+
//
|
|
5
|
+
// Splits cleanly into three layers so tests can exercise the pure
|
|
6
|
+
// bits without spawning the CLI:
|
|
7
|
+
//
|
|
8
|
+
// extractText / truncate — jsonl → prompt input
|
|
9
|
+
// parseClaudeJsonResult — CLI stdout → SummaryResult
|
|
10
|
+
// validateSummaryResult — unknown → SummaryResult
|
|
11
|
+
//
|
|
12
|
+
// `defaultSummarize` composes them with the real spawn; tests
|
|
13
|
+
// inject their own SummarizeFn via `IndexerDeps.summarize`.
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { EVENT_TYPES } from "../../../src/types/events.js";
|
|
17
|
+
import { readFile } from "node:fs/promises";
|
|
18
|
+
import { formatSpawnFailure } from "../../utils/spawn.js";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { ClaudeCliNotFoundError } from "../journal/archivist.js";
|
|
21
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
22
|
+
import type { SummaryResult } from "./types.js";
|
|
23
|
+
import { ONE_MINUTE_MS } from "../../utils/time.js";
|
|
24
|
+
import { isRecord } from "../../utils/types.js";
|
|
25
|
+
|
|
26
|
+
const SYSTEM_PROMPT =
|
|
27
|
+
"You summarize a single chat session. Output strict JSON matching the provided schema. " +
|
|
28
|
+
"Rules: title <= 60 characters in the source language, summary <= 200 characters in the same language, " +
|
|
29
|
+
"5 to 10 short lowercase keywords useful for search. Respond with structured output only.";
|
|
30
|
+
|
|
31
|
+
const SUMMARY_SCHEMA = {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
title: { type: "string" },
|
|
35
|
+
summary: { type: "string" },
|
|
36
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
37
|
+
},
|
|
38
|
+
required: ["title", "summary", "keywords"],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Prompt-building constants.
|
|
42
|
+
const MAX_INPUT_CHARS = 8000;
|
|
43
|
+
const HEAD_CHARS = 3000;
|
|
44
|
+
const TAIL_CHARS = 5000;
|
|
45
|
+
const PER_MESSAGE_MAX = 500;
|
|
46
|
+
|
|
47
|
+
// Spawn / budget constants.
|
|
48
|
+
const DEFAULT_TIMEOUT_MS = 2 * ONE_MINUTE_MS;
|
|
49
|
+
// Budget cap per summarization call, forwarded to `claude
|
|
50
|
+
// --max-budget-usd`. Previously 0.05 but that was tight enough
|
|
51
|
+
// that a first-burst call — which pays a one-time cache creation
|
|
52
|
+
// cost on haiku (~28k cache-creation tokens) — would trip the cap
|
|
53
|
+
// and fail with `error_max_budget_usd` even for tiny 600-char
|
|
54
|
+
// transcripts. 0.15 leaves comfortable headroom for cache
|
|
55
|
+
// creation + a generous output allowance while still capping a
|
|
56
|
+
// full 100-session backfill to well under $20.
|
|
57
|
+
const MAX_BUDGET_USD = 0.15;
|
|
58
|
+
|
|
59
|
+
// Any module that wants to drive the summarizer — including the
|
|
60
|
+
// indexer — takes a SummarizeFn so tests can supply a deterministic
|
|
61
|
+
// fake. Production path is `defaultSummarize` below.
|
|
62
|
+
export type SummarizeFn = (input: string) => Promise<SummaryResult>;
|
|
63
|
+
|
|
64
|
+
interface JsonlEntry {
|
|
65
|
+
source?: string;
|
|
66
|
+
type?: string;
|
|
67
|
+
message?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function trimMessage(text: string): string {
|
|
71
|
+
if (text.length <= PER_MESSAGE_MAX) return text;
|
|
72
|
+
return `${text.slice(0, PER_MESSAGE_MAX)}…`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Walk a session jsonl and keep only the user / assistant text
|
|
76
|
+
// turns, joined into a compact transcript. Tool results are
|
|
77
|
+
// skipped because they are noisy and rarely contribute to a useful
|
|
78
|
+
// summary title.
|
|
79
|
+
export function extractText(jsonlContent: string): string {
|
|
80
|
+
const lines = jsonlContent.split("\n").filter(Boolean);
|
|
81
|
+
const parts: string[] = [];
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
let entry: JsonlEntry;
|
|
84
|
+
try {
|
|
85
|
+
entry = JSON.parse(line);
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const source = entry.source;
|
|
90
|
+
if ((source === "user" || source === "assistant") && entry.type === EVENT_TYPES.text && typeof entry.message === "string") {
|
|
91
|
+
parts.push(`[${source}] ${trimMessage(entry.message)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return parts.join("\n\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Long sessions are truncated to first ~3000 + last ~5000 chars so
|
|
98
|
+
// claude sees both the original topic and the most recent state.
|
|
99
|
+
export function truncate(text: string): string {
|
|
100
|
+
if (text.length <= MAX_INPUT_CHARS) return text;
|
|
101
|
+
const head = text.slice(0, HEAD_CHARS);
|
|
102
|
+
const tail = text.slice(-TAIL_CHARS);
|
|
103
|
+
return `${head}\n\n…\n\n${tail}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ClaudeJsonResult {
|
|
107
|
+
type?: string;
|
|
108
|
+
is_error?: boolean;
|
|
109
|
+
structured_output?: unknown;
|
|
110
|
+
result?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Parse the JSON envelope that `claude --output-format json`
|
|
114
|
+
// prints, raising a useful error if the envelope is malformed or
|
|
115
|
+
// the CLI reported an error.
|
|
116
|
+
export function parseClaudeJsonResult(stdout: string): SummaryResult {
|
|
117
|
+
let parsed: ClaudeJsonResult;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(stdout.trim());
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new Error(`[chat-index] failed to parse claude json output: ${errorMessage(err)}`);
|
|
122
|
+
}
|
|
123
|
+
if (parsed.is_error) {
|
|
124
|
+
throw new Error(`[chat-index] claude returned error: ${parsed.result ?? "unknown"}`);
|
|
125
|
+
}
|
|
126
|
+
return validateSummaryResult(parsed.structured_output);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Build the error message for a non-zero `claude` CLI exit.
|
|
130
|
+
//
|
|
131
|
+
// The claude CLI writes its structured result — including error
|
|
132
|
+
// envelopes like `{"is_error":true,"subtype":"error_max_budget_usd",
|
|
133
|
+
// "errors":["Reached maximum budget ($0.05)"]}` — to **stdout**,
|
|
134
|
+
// not stderr. Our previous handler only inspected stderr, so
|
|
135
|
+
// budget-exhaustion and similar failures surfaced as
|
|
136
|
+
// `claude summarize exited 1:` with no details at all, making
|
|
137
|
+
// them impossible to diagnose from the log.
|
|
138
|
+
//
|
|
139
|
+
// Strategy: try to parse stdout as a claude JSON envelope first
|
|
140
|
+
// and extract a human-readable reason from `errors[]` /
|
|
141
|
+
// `subtype` / `result`; fall back to stderr, then to a raw
|
|
142
|
+
// stdout slice, then to a generic "no error output".
|
|
143
|
+
export function formatSpawnError(code: number | null, stdout: string, stderr: string): string {
|
|
144
|
+
return formatSpawnFailure("[chat-index]", code, stdout, stderr);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Runtime-validate an arbitrary value into a SummaryResult. Missing
|
|
148
|
+
// or wrong-typed fields fall back to safe defaults rather than
|
|
149
|
+
// crashing the indexer — a degraded title is better than a dropped
|
|
150
|
+
// session.
|
|
151
|
+
export function validateSummaryResult(obj: unknown): SummaryResult {
|
|
152
|
+
if (!isRecord(obj)) {
|
|
153
|
+
throw new Error("[chat-index] summary result is not an object");
|
|
154
|
+
}
|
|
155
|
+
const o = obj as Record<string, unknown>;
|
|
156
|
+
const title = typeof o.title === "string" ? o.title : "";
|
|
157
|
+
const summary = typeof o.summary === "string" ? o.summary : "";
|
|
158
|
+
const keywords = Array.isArray(o.keywords) ? o.keywords.filter((k): k is string => typeof k === "string") : [];
|
|
159
|
+
return { title, summary, keywords };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read a jsonl file and produce the pre-truncated transcript that
|
|
163
|
+
// goes into the CLI prompt. Returns the empty string for an empty
|
|
164
|
+
// or unreadable file so the caller can decide whether to skip.
|
|
165
|
+
export async function loadJsonlInput(jsonlPath: string): Promise<string> {
|
|
166
|
+
try {
|
|
167
|
+
const content = await readFile(jsonlPath, "utf-8");
|
|
168
|
+
return truncate(extractText(content));
|
|
169
|
+
} catch {
|
|
170
|
+
return "";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- spawn layer ----------------------------------------------------
|
|
175
|
+
|
|
176
|
+
function spawnClaudeSummarize(input: string, timeoutMs: number): Promise<string> {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const args = [
|
|
179
|
+
"--print",
|
|
180
|
+
"--no-session-persistence",
|
|
181
|
+
"--output-format",
|
|
182
|
+
"json",
|
|
183
|
+
"--model",
|
|
184
|
+
"haiku",
|
|
185
|
+
"--max-budget-usd",
|
|
186
|
+
String(MAX_BUDGET_USD),
|
|
187
|
+
"--json-schema",
|
|
188
|
+
JSON.stringify(SUMMARY_SCHEMA),
|
|
189
|
+
"--system-prompt",
|
|
190
|
+
SYSTEM_PROMPT,
|
|
191
|
+
"-p",
|
|
192
|
+
input,
|
|
193
|
+
];
|
|
194
|
+
// Run from tmpdir so claude does not load the project's
|
|
195
|
+
// CLAUDE.md / plugins / memory and inflate the context.
|
|
196
|
+
const proc = spawn("claude", args, {
|
|
197
|
+
cwd: tmpdir(),
|
|
198
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
let stdout = "";
|
|
202
|
+
let stderr = "";
|
|
203
|
+
let settled = false;
|
|
204
|
+
|
|
205
|
+
const timer = setTimeout(() => {
|
|
206
|
+
if (settled) return;
|
|
207
|
+
settled = true;
|
|
208
|
+
proc.kill("SIGKILL");
|
|
209
|
+
reject(new Error(`[chat-index] claude summarize timed out after ${timeoutMs}ms`));
|
|
210
|
+
}, timeoutMs);
|
|
211
|
+
|
|
212
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
213
|
+
stdout += chunk.toString();
|
|
214
|
+
});
|
|
215
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
216
|
+
stderr += chunk.toString();
|
|
217
|
+
});
|
|
218
|
+
proc.on("error", (err: Error & { code?: string }) => {
|
|
219
|
+
if (settled) return;
|
|
220
|
+
settled = true;
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
if (err.code === "ENOENT") {
|
|
223
|
+
reject(new ClaudeCliNotFoundError());
|
|
224
|
+
} else {
|
|
225
|
+
reject(err);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
proc.on("close", (code) => {
|
|
229
|
+
if (settled) return;
|
|
230
|
+
settled = true;
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
if (code !== 0) {
|
|
233
|
+
reject(new Error(formatSpawnError(code, stdout, stderr)));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
resolve(stdout);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Production SummarizeFn: prepare the input from a jsonl path and
|
|
242
|
+
// drive the CLI. Tests inject their own SummarizeFn that bypasses
|
|
243
|
+
// the CLI entirely.
|
|
244
|
+
export const defaultSummarize: SummarizeFn = async (input: string) => {
|
|
245
|
+
const stdout = await spawnClaudeSummarize(input, DEFAULT_TIMEOUT_MS);
|
|
246
|
+
return parseClaudeJsonResult(stdout);
|
|
247
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// On-disk shapes for the per-session chat summaries cached under
|
|
2
|
+
// workspace/chat/index/. These power the title + summary shown for
|
|
3
|
+
// past sessions in the sidebar history pane. The full design lives
|
|
4
|
+
// in plans/done/feat-session-index-titles.md.
|
|
5
|
+
|
|
6
|
+
export interface SummaryResult {
|
|
7
|
+
// <= 60 chars in the source language
|
|
8
|
+
title: string;
|
|
9
|
+
// <= 200 chars in the source language
|
|
10
|
+
summary: string;
|
|
11
|
+
// 5-10 short lowercase keywords
|
|
12
|
+
keywords: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// One cached summary per session. Written to chat/index/<id>.json
|
|
16
|
+
// and also mirrored into manifest.json for bulk-read from the
|
|
17
|
+
// /api/sessions route.
|
|
18
|
+
export interface ChatIndexEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
roleId: string;
|
|
21
|
+
startedAt: string;
|
|
22
|
+
// ISO timestamp of when this summary was produced. Used by the
|
|
23
|
+
// freshness throttle — we skip re-summarizing a session whose
|
|
24
|
+
// existing entry is less than MIN_INDEX_INTERVAL_MS old, so a
|
|
25
|
+
// 20-turn conversation over 30 min summarizes ~twice, not 20
|
|
26
|
+
// times. See `isFresh` in indexer.ts.
|
|
27
|
+
indexedAt: string;
|
|
28
|
+
title: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
keywords: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ChatIndexManifest {
|
|
34
|
+
version: 1;
|
|
35
|
+
// Sorted newest-first by startedAt so the sidebar gets them in
|
|
36
|
+
// display order without a second sort pass.
|
|
37
|
+
entries: ChatIndexEntry[];
|
|
38
|
+
}
|