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,135 @@
|
|
|
1
|
+
// Per-source runtime state I/O.
|
|
2
|
+
//
|
|
3
|
+
// State lives at `workspace/sources/_state/<slug>.json`, kept
|
|
4
|
+
// separate from the source config (`<slug>.md`) so the config
|
|
5
|
+
// is the git-tracked source of truth while state can grow /
|
|
6
|
+
// reset / get cleared without touching committed history.
|
|
7
|
+
//
|
|
8
|
+
// All functions take an explicit `workspaceRoot` so tests use
|
|
9
|
+
// mkdtempSync without touching real workspace state.
|
|
10
|
+
|
|
11
|
+
import fsp from "node:fs/promises";
|
|
12
|
+
import { defaultSourceState, type SourceState } from "./types.js";
|
|
13
|
+
import { sourceStatePath } from "./paths.js";
|
|
14
|
+
import { isValidSlug } from "../../utils/slug.js";
|
|
15
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
16
|
+
import { writeJsonAtomic } from "../../utils/files/index.js";
|
|
17
|
+
import { isRecord } from "../../utils/types.js";
|
|
18
|
+
|
|
19
|
+
// Shallow-parse + type-guard one state record. Returns a
|
|
20
|
+
// default state (zeroed counters, empty cursor) when the file
|
|
21
|
+
// is missing, malformed, or any required field fails. Never
|
|
22
|
+
// throws.
|
|
23
|
+
export async function readSourceState(workspaceRoot: string, slug: string): Promise<SourceState> {
|
|
24
|
+
if (!isValidSlug(slug)) return defaultSourceState(slug);
|
|
25
|
+
let raw: string;
|
|
26
|
+
try {
|
|
27
|
+
raw = await fsp.readFile(sourceStatePath(workspaceRoot, slug), "utf-8");
|
|
28
|
+
} catch {
|
|
29
|
+
return defaultSourceState(slug);
|
|
30
|
+
}
|
|
31
|
+
let parsed: unknown;
|
|
32
|
+
try {
|
|
33
|
+
parsed = JSON.parse(raw);
|
|
34
|
+
} catch {
|
|
35
|
+
return defaultSourceState(slug);
|
|
36
|
+
}
|
|
37
|
+
return validateSourceState(parsed, slug);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Runtime-validate an arbitrary parse result into a
|
|
41
|
+
// SourceState. Unknown fields are dropped; missing fields get
|
|
42
|
+
// default values; wrong-typed fields collapse to the default.
|
|
43
|
+
// Defensive: a hand-edited / corrupted state file should NOT
|
|
44
|
+
// crash the pipeline, it should quietly get rebuilt on the
|
|
45
|
+
// next successful run.
|
|
46
|
+
export function validateSourceState(raw: unknown, slug: string): SourceState {
|
|
47
|
+
if (!isRecord(raw)) {
|
|
48
|
+
return defaultSourceState(slug);
|
|
49
|
+
}
|
|
50
|
+
const o = raw as Record<string, unknown>;
|
|
51
|
+
const lastFetchedAt = typeof o.lastFetchedAt === "string" ? o.lastFetchedAt : null;
|
|
52
|
+
const nextAttemptAt = typeof o.nextAttemptAt === "string" ? o.nextAttemptAt : null;
|
|
53
|
+
const consecutiveFailures =
|
|
54
|
+
typeof o.consecutiveFailures === "number" && Number.isFinite(o.consecutiveFailures) && o.consecutiveFailures >= 0 ? Math.floor(o.consecutiveFailures) : 0;
|
|
55
|
+
const cursor = validateCursor(o.cursor);
|
|
56
|
+
return {
|
|
57
|
+
slug,
|
|
58
|
+
lastFetchedAt,
|
|
59
|
+
cursor,
|
|
60
|
+
consecutiveFailures,
|
|
61
|
+
nextAttemptAt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateCursor(raw: unknown): Record<string, string> {
|
|
66
|
+
if (!isRecord(raw)) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
const out: Record<string, string> = {};
|
|
70
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
71
|
+
if (typeof value === "string") out[key] = value;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Atomic write: stage to `.tmp` then rename. Parent directory
|
|
77
|
+
// created as needed.
|
|
78
|
+
export async function writeSourceState(workspaceRoot: string, state: SourceState): Promise<void> {
|
|
79
|
+
if (!isValidSlug(state.slug)) {
|
|
80
|
+
throw new Error(`[sources/state] invalid slug: ${state.slug}`);
|
|
81
|
+
}
|
|
82
|
+
await writeJsonAtomic(sourceStatePath(workspaceRoot, state.slug), state);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Convenience: read every state file listed for the given
|
|
86
|
+
// slugs. Used by the pipeline to gather per-source state before
|
|
87
|
+
// the fetch phase.
|
|
88
|
+
export async function readManyStates(workspaceRoot: string, slugs: readonly string[]): Promise<Map<string, SourceState>> {
|
|
89
|
+
const out = new Map<string, SourceState>();
|
|
90
|
+
const reads = await Promise.all(slugs.map((slug) => readSourceState(workspaceRoot, slug)));
|
|
91
|
+
for (const state of reads) out.set(state.slug, state);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Convenience: write every state back in parallel. Failure on
|
|
96
|
+
// one state write is logged and absorbed — the daily run's
|
|
97
|
+
// summary has already landed on disk, a lost state update just
|
|
98
|
+
// means the next run re-fetches slightly more than needed.
|
|
99
|
+
export async function writeManyStates(workspaceRoot: string, states: readonly SourceState[]): Promise<{ written: number; errors: string[] }> {
|
|
100
|
+
const errors: string[] = [];
|
|
101
|
+
let written = 0;
|
|
102
|
+
for (const state of states) {
|
|
103
|
+
try {
|
|
104
|
+
await writeSourceState(workspaceRoot, state);
|
|
105
|
+
written++;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
errors.push(`[sources/state] ${state.slug}: ${errorMessage(err)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { written, errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Delete the state file for a slug. Used by `manageSource
|
|
114
|
+
// delete` so a removed source doesn't leave orphan state.
|
|
115
|
+
// Missing file is fine — returns false rather than throwing.
|
|
116
|
+
export async function deleteSourceState(workspaceRoot: string, slug: string): Promise<boolean> {
|
|
117
|
+
if (!isValidSlug(slug)) return false;
|
|
118
|
+
try {
|
|
119
|
+
await fsp.unlink(sourceStatePath(workspaceRoot, slug));
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Utility: sort outcomes by source slug so results are
|
|
127
|
+
// deterministic regardless of which fetcher finished first.
|
|
128
|
+
// Used by reporting / logging code.
|
|
129
|
+
export function sortBySlug<T extends { sourceSlug?: string; slug?: string }>(items: readonly T[]): T[] {
|
|
130
|
+
return [...items].sort((a, b) => {
|
|
131
|
+
const ak = a.sourceSlug ?? a.slug ?? "";
|
|
132
|
+
const bk = b.sourceSlug ?? b.slug ?? "";
|
|
133
|
+
return ak < bk ? -1 : ak > bk ? 1 : 0;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Fixed taxonomy of source categories. Keeping this a closed enum
|
|
2
|
+
// (rather than accepting free-form LLM-generated tags) prevents
|
|
3
|
+
// synonym sprawl — `ai` vs `artificial-intelligence` vs `AI` would
|
|
4
|
+
// otherwise all coexist and make filtering useless.
|
|
5
|
+
//
|
|
6
|
+
// The auto-categorizer (see plans/feat-source-registry.md §"Auto-
|
|
7
|
+
// categorization") classifies each new source into 1-5 of these
|
|
8
|
+
// slugs and writes them into the source file's frontmatter. Users
|
|
9
|
+
// can override by editing the file; the next daily run picks up
|
|
10
|
+
// the edits.
|
|
11
|
+
//
|
|
12
|
+
// Pin-tested so a silent enum mutation doesn't sneak past review.
|
|
13
|
+
|
|
14
|
+
export const CATEGORY_SLUGS = [
|
|
15
|
+
"tech-news",
|
|
16
|
+
"business-news",
|
|
17
|
+
"ai",
|
|
18
|
+
"security",
|
|
19
|
+
"devops",
|
|
20
|
+
"frontend",
|
|
21
|
+
"backend",
|
|
22
|
+
"ml-research",
|
|
23
|
+
"dependencies",
|
|
24
|
+
"product-updates",
|
|
25
|
+
"japanese",
|
|
26
|
+
"english",
|
|
27
|
+
"papers",
|
|
28
|
+
"general",
|
|
29
|
+
"startup",
|
|
30
|
+
"personal",
|
|
31
|
+
// --- Phase-1 expansion (resolved from #188 open-question Q1) ---
|
|
32
|
+
// Added to cover common genres the original 16 couldn't capture
|
|
33
|
+
// (which were tech-centric and collapsed everything non-tech
|
|
34
|
+
// into `general`). See plans/feat-source-registry.md §Resolved
|
|
35
|
+
// decisions for rationale per slug.
|
|
36
|
+
"finance",
|
|
37
|
+
"design",
|
|
38
|
+
"productivity",
|
|
39
|
+
"science",
|
|
40
|
+
"health",
|
|
41
|
+
"gaming",
|
|
42
|
+
"climate",
|
|
43
|
+
"culture",
|
|
44
|
+
"policy",
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export type CategorySlug = (typeof CATEGORY_SLUGS)[number];
|
|
48
|
+
|
|
49
|
+
const CATEGORY_SET: ReadonlySet<string> = new Set(CATEGORY_SLUGS);
|
|
50
|
+
|
|
51
|
+
// Runtime type-guard. Used when reading a source file from disk to
|
|
52
|
+
// drop any legacy / typo categories so downstream code only ever
|
|
53
|
+
// deals in the current enum.
|
|
54
|
+
export function isCategorySlug(value: unknown): value is CategorySlug {
|
|
55
|
+
return typeof value === "string" && CATEGORY_SET.has(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Normalize an unknown list of category candidates into a clean,
|
|
59
|
+
// deduplicated array of valid slugs. Used when reading from
|
|
60
|
+
// frontmatter (where the user may have typo'd) and when receiving
|
|
61
|
+
// classifier output (where the LLM may have hallucinated a slug
|
|
62
|
+
// outside the taxonomy).
|
|
63
|
+
export function normalizeCategories(raw: unknown): CategorySlug[] {
|
|
64
|
+
if (!Array.isArray(raw)) return [];
|
|
65
|
+
const seen = new Set<CategorySlug>();
|
|
66
|
+
const out: CategorySlug[] = [];
|
|
67
|
+
for (const item of raw) {
|
|
68
|
+
if (isCategorySlug(item) && !seen.has(item)) {
|
|
69
|
+
seen.add(item);
|
|
70
|
+
out.push(item);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Data model for the information-source registry. Every source
|
|
2
|
+
// lives as one markdown file under `workspace/sources/<slug>.md`,
|
|
3
|
+
// with these fields in the YAML frontmatter plus optional free-form
|
|
4
|
+
// markdown notes in the body.
|
|
5
|
+
//
|
|
6
|
+
// Design invariants the consumer code relies on:
|
|
7
|
+
//
|
|
8
|
+
// - `slug` is the primary key and matches the filename exactly.
|
|
9
|
+
// Enforced by `registry.ts` on both read and write.
|
|
10
|
+
// - `url` is always the normalized form (see urls.ts) so dedup
|
|
11
|
+
// across sources works by string equality.
|
|
12
|
+
// - `fetcherKind` is one of a closed set so the fetcher dispatcher
|
|
13
|
+
// can look up the right handler without `any`.
|
|
14
|
+
// - `schedule` drives the daily / weekly aggregation pipeline.
|
|
15
|
+
// - `categories` contains only valid CategorySlug values
|
|
16
|
+
// (runtime-validated on read).
|
|
17
|
+
//
|
|
18
|
+
// Secrets (API tokens, bearer auth) are NEVER stored here —
|
|
19
|
+
// phase-1 scope is public sources only. Phase-3 authed fetchers
|
|
20
|
+
// will read credentials from `.env` at runtime by name; the name
|
|
21
|
+
// reference lives in `fetcherParams` as an `envVar` field.
|
|
22
|
+
|
|
23
|
+
import type { CategorySlug } from "./taxonomy.js";
|
|
24
|
+
|
|
25
|
+
// Closed set of fetcher kinds we can dispatch on. Adding a new
|
|
26
|
+
// fetcher means: add the string literal here, implement a matching
|
|
27
|
+
// module under `server/sources/fetchers/<kind>.ts`, and register
|
|
28
|
+
// it in the fetcher index. Nothing else in the framework needs to
|
|
29
|
+
// change.
|
|
30
|
+
//
|
|
31
|
+
// Phase-1 surface:
|
|
32
|
+
// "rss" — public RSS / Atom feeds (server-side fetch)
|
|
33
|
+
// "github-releases" — GitHub /releases endpoint, unauthenticated
|
|
34
|
+
// "github-issues" — GitHub /issues + /pulls, unauthenticated
|
|
35
|
+
// "arxiv" — arXiv query API
|
|
36
|
+
// "web-fetch" — one-shot page fetch via Claude's web_fetch
|
|
37
|
+
// "web-search" — ad-hoc query via Claude's web_search
|
|
38
|
+
export const FETCHER_KINDS = ["rss", "github-releases", "github-issues", "arxiv", "web-fetch", "web-search"] as const;
|
|
39
|
+
|
|
40
|
+
export type FetcherKind = (typeof FETCHER_KINDS)[number];
|
|
41
|
+
|
|
42
|
+
const FETCHER_KIND_SET: ReadonlySet<string> = new Set(FETCHER_KINDS);
|
|
43
|
+
|
|
44
|
+
export function isFetcherKind(value: unknown): value is FetcherKind {
|
|
45
|
+
return typeof value === "string" && FETCHER_KIND_SET.has(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// How often the daily pipeline is expected to refresh this source.
|
|
49
|
+
// `on-demand` sources are never auto-fetched; they only respond to
|
|
50
|
+
// the `manageSource fetch` action or the on-demand research
|
|
51
|
+
// workflow.
|
|
52
|
+
export const SOURCE_SCHEDULES = ["hourly", "daily", "weekly", "on-demand"] as const;
|
|
53
|
+
|
|
54
|
+
export type SourceSchedule = (typeof SOURCE_SCHEDULES)[number];
|
|
55
|
+
|
|
56
|
+
const SOURCE_SCHEDULE_SET: ReadonlySet<string> = new Set(SOURCE_SCHEDULES);
|
|
57
|
+
|
|
58
|
+
export function isSourceSchedule(value: unknown): value is SourceSchedule {
|
|
59
|
+
return typeof value === "string" && SOURCE_SCHEDULE_SET.has(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Per-fetcher extra parameters carried on the Source file. Flat
|
|
63
|
+
// string map on disk so the minimal frontmatter parser can handle
|
|
64
|
+
// it without nested YAML. Fetchers interpret the keys they care
|
|
65
|
+
// about and ignore the rest — keeps cross-fetcher rewiring cheap.
|
|
66
|
+
export type FetcherParams = Record<string, string>;
|
|
67
|
+
|
|
68
|
+
// The on-disk configuration for one source. This is the exact
|
|
69
|
+
// shape serialized into the YAML frontmatter of
|
|
70
|
+
// `workspace/sources/<slug>.md` — state (cursors, etags, failure
|
|
71
|
+
// counts) lives separately under `_state/<slug>.json`.
|
|
72
|
+
export interface Source {
|
|
73
|
+
slug: string;
|
|
74
|
+
title: string;
|
|
75
|
+
url: string;
|
|
76
|
+
fetcherKind: FetcherKind;
|
|
77
|
+
fetcherParams: FetcherParams;
|
|
78
|
+
schedule: SourceSchedule;
|
|
79
|
+
categories: CategorySlug[];
|
|
80
|
+
maxItemsPerFetch: number;
|
|
81
|
+
addedAt: string; // ISO timestamp
|
|
82
|
+
notes: string; // markdown body of the file
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// One normalized item after a fetch. All fetchers produce this
|
|
86
|
+
// shape regardless of source type so the pipeline / dedup / summary
|
|
87
|
+
// layers don't care where items came from.
|
|
88
|
+
export interface SourceItem {
|
|
89
|
+
// Stable unique id for dedup. Hash of the normalized URL, or
|
|
90
|
+
// the fetcher's native id (e.g. GitHub release id) when one is
|
|
91
|
+
// available.
|
|
92
|
+
id: string;
|
|
93
|
+
title: string;
|
|
94
|
+
url: string;
|
|
95
|
+
publishedAt: string; // ISO timestamp
|
|
96
|
+
// Short one-line summary if the fetcher can produce one without
|
|
97
|
+
// LLM help. The pipeline's summarize step may replace this with
|
|
98
|
+
// a richer LLM-generated version.
|
|
99
|
+
summary?: string;
|
|
100
|
+
// Full body content if available (RSS description, GitHub release
|
|
101
|
+
// body, etc.). The summarize step reads this when the short
|
|
102
|
+
// summary is insufficient.
|
|
103
|
+
content?: string;
|
|
104
|
+
// Categories inherited from the source this item came from.
|
|
105
|
+
// Duplicated on the item so per-category daily rollups don't
|
|
106
|
+
// need a re-join.
|
|
107
|
+
categories: CategorySlug[];
|
|
108
|
+
// Slug of the parent Source so the dashboard / notification
|
|
109
|
+
// layer can link back.
|
|
110
|
+
sourceSlug: string;
|
|
111
|
+
// Optional severity hint set by the classifier or the fetcher
|
|
112
|
+
// itself (security advisories set `critical`). Daily pipeline
|
|
113
|
+
// uses this to decide whether to notify.
|
|
114
|
+
severity?: "info" | "warn" | "critical";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Per-source runtime state, NOT committed to git. Mirrors the
|
|
118
|
+
// Source-vs-_state split described in plans/feat-source-registry.md.
|
|
119
|
+
export interface SourceState {
|
|
120
|
+
slug: string;
|
|
121
|
+
// Last successful fetch.
|
|
122
|
+
lastFetchedAt: string | null;
|
|
123
|
+
// Fetcher-specific cursor — ISO timestamp, etag, GitHub release
|
|
124
|
+
// id, arXiv last-seen, whatever the fetcher persists to
|
|
125
|
+
// de-duplicate across runs. Free-form string map so the fetcher
|
|
126
|
+
// interface doesn't need to know the shape upfront.
|
|
127
|
+
cursor: Record<string, string>;
|
|
128
|
+
// Consecutive failure count. Incremented per failed fetch,
|
|
129
|
+
// reset to 0 on success. Drives exponential backoff.
|
|
130
|
+
consecutiveFailures: number;
|
|
131
|
+
// Timestamp after which the next attempt is allowed, so backoff
|
|
132
|
+
// survives server restarts.
|
|
133
|
+
nextAttemptAt: string | null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function defaultSourceState(slug: string): SourceState {
|
|
137
|
+
return {
|
|
138
|
+
slug,
|
|
139
|
+
lastFetchedAt: null,
|
|
140
|
+
cursor: {},
|
|
141
|
+
consecutiveFailures: 0,
|
|
142
|
+
nextAttemptAt: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// URL normalization utilities for dedup + cache keys.
|
|
2
|
+
//
|
|
3
|
+
// The same article commonly arrives from multiple sources with
|
|
4
|
+
// different query strings (utm_source, fbclid, gclid, mc_cid, ...)
|
|
5
|
+
// or trailing slashes. We normalize before dedup so "same article
|
|
6
|
+
// different feed" collapses into one item.
|
|
7
|
+
//
|
|
8
|
+
// Pure — no I/O, fully testable.
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
// Tracking parameters we always strip. Case-insensitive on the
|
|
13
|
+
// parameter NAME only.
|
|
14
|
+
const TRACKING_PARAM_PREFIXES: readonly string[] = ["utm_", "mc_", "pk_", "hsa_"];
|
|
15
|
+
|
|
16
|
+
const TRACKING_PARAMS: ReadonlySet<string> = new Set([
|
|
17
|
+
"fbclid",
|
|
18
|
+
"gclid",
|
|
19
|
+
"msclkid",
|
|
20
|
+
"dclid",
|
|
21
|
+
"gbraid",
|
|
22
|
+
"wbraid",
|
|
23
|
+
"yclid",
|
|
24
|
+
"ref",
|
|
25
|
+
"ref_src",
|
|
26
|
+
"ref_url",
|
|
27
|
+
"share",
|
|
28
|
+
"share_source",
|
|
29
|
+
"trk",
|
|
30
|
+
"igshid",
|
|
31
|
+
"cmp",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function isTrackingParam(name: string): boolean {
|
|
35
|
+
const lower = name.toLowerCase();
|
|
36
|
+
if (TRACKING_PARAMS.has(lower)) return true;
|
|
37
|
+
for (const prefix of TRACKING_PARAM_PREFIXES) {
|
|
38
|
+
if (lower.startsWith(prefix)) return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Normalize a URL for use as a dedup key:
|
|
44
|
+
//
|
|
45
|
+
// 1. Parse it. Invalid → return null (caller decides fallback).
|
|
46
|
+
// 2. Lowercase the protocol + host.
|
|
47
|
+
// 3. Drop the fragment.
|
|
48
|
+
// 4. Drop tracking query params.
|
|
49
|
+
// 5. Sort remaining query params so different orderings hash the
|
|
50
|
+
// same.
|
|
51
|
+
// 6. Collapse trailing slash on the pathname (except root "/").
|
|
52
|
+
// 7. Drop default ports (80 for http, 443 for https).
|
|
53
|
+
//
|
|
54
|
+
// Returns the normalized href on success, null on unparseable
|
|
55
|
+
// input.
|
|
56
|
+
export function normalizeUrl(raw: string): string | null {
|
|
57
|
+
if (typeof raw !== "string" || raw.trim() === "") return null;
|
|
58
|
+
let url: URL;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(raw.trim());
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// protocol + host lowercase (URL already normalizes these but
|
|
66
|
+
// doing it explicitly guards against future WHATWG tweaks).
|
|
67
|
+
url.protocol = url.protocol.toLowerCase();
|
|
68
|
+
url.hostname = url.hostname.toLowerCase();
|
|
69
|
+
|
|
70
|
+
// Drop fragment.
|
|
71
|
+
url.hash = "";
|
|
72
|
+
|
|
73
|
+
// Drop tracking params. Iterate on a snapshot because delete
|
|
74
|
+
// mutates the underlying list.
|
|
75
|
+
const paramNames = Array.from(url.searchParams.keys());
|
|
76
|
+
for (const name of paramNames) {
|
|
77
|
+
if (isTrackingParam(name)) url.searchParams.delete(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sort remaining params for deterministic ordering. Preserve
|
|
81
|
+
// multi-value params by iterating all entries.
|
|
82
|
+
const entries = Array.from(url.searchParams.entries());
|
|
83
|
+
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
84
|
+
// Clear and reinsert.
|
|
85
|
+
for (const name of Array.from(url.searchParams.keys())) {
|
|
86
|
+
url.searchParams.delete(name);
|
|
87
|
+
}
|
|
88
|
+
for (const [name, value] of entries) {
|
|
89
|
+
url.searchParams.append(name, value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Collapse trailing slash on non-root paths.
|
|
93
|
+
if (url.pathname.length > 1 && url.pathname.endsWith("/")) {
|
|
94
|
+
url.pathname = url.pathname.slice(0, -1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Drop default ports.
|
|
98
|
+
if ((url.protocol === "http:" && url.port === "80") || (url.protocol === "https:" && url.port === "443")) {
|
|
99
|
+
url.port = "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return url.toString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Stable content-addressed id for an item. Used for persistent
|
|
106
|
+
// cross-run dedup of news items. Truncated SHA-256 gives ~64 bits
|
|
107
|
+
// of collision resistance — safe into the billions of items, vs.
|
|
108
|
+
// FNV-1a 32-bit which starts colliding in the tens of thousands
|
|
109
|
+
// (birthday-paradox territory at our projected volume).
|
|
110
|
+
export function stableItemId(normalizedUrl: string): string {
|
|
111
|
+
return createHash("sha256").update(normalizedUrl, "utf8").digest("hex").slice(0, 16);
|
|
112
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Pure classification for built-in Claude tool_result events.
|
|
2
|
+
// Decides whether a given tool result should be stored in the session
|
|
3
|
+
// jsonl as a pointer to a real workspace file, inlined verbatim, or
|
|
4
|
+
// inlined with truncation. No filesystem access — callers do I/O
|
|
5
|
+
// separately and feed the result here.
|
|
6
|
+
//
|
|
7
|
+
// See plans/done/feat-tool-trace-persistence.md for the design rationale.
|
|
8
|
+
|
|
9
|
+
import { isRecord } from "../../utils/types.js";
|
|
10
|
+
|
|
11
|
+
export type Classification = { kind: "pointer"; contentRef: string } | { kind: "inline"; content: string; truncated: boolean };
|
|
12
|
+
|
|
13
|
+
// Max characters kept when content is stored inline in the jsonl.
|
|
14
|
+
// Picked to keep per-turn jsonl size sane while still capturing
|
|
15
|
+
// enough of a small Bash/Grep output to be useful for debugging.
|
|
16
|
+
export const MAX_INLINE_CONTENT_CHARS = 4096;
|
|
17
|
+
|
|
18
|
+
// Tools whose `args.file_path` already points at an existing file —
|
|
19
|
+
// the jsonl can simply reference that path instead of duplicating the
|
|
20
|
+
// content. Matches Claude Code's built-in file tool names exactly.
|
|
21
|
+
const FILE_POINTER_TOOLS = new Set(["Read", "Write", "Edit"]);
|
|
22
|
+
|
|
23
|
+
// Image-generation MCP tools. The tool result already carries the
|
|
24
|
+
// saved path so we extract it; the raw bytes/base64 never leave the
|
|
25
|
+
// agent stream memory.
|
|
26
|
+
const IMAGE_TOOLS = new Set(["generateImage", "editImage"]);
|
|
27
|
+
|
|
28
|
+
// Tool name we always route through `writeSearch.ts` before
|
|
29
|
+
// classifying. Exposed so callers know which tools need a
|
|
30
|
+
// pre-computed `searchContentRef` injected.
|
|
31
|
+
export const WEB_SEARCH_TOOL_NAME = "WebSearch";
|
|
32
|
+
|
|
33
|
+
export interface ClassifyInput {
|
|
34
|
+
toolName: string;
|
|
35
|
+
args: unknown;
|
|
36
|
+
content: string;
|
|
37
|
+
// Optional pre-computed contentRef for WebSearch — the caller saves
|
|
38
|
+
// the result file first (in `writeSearch.ts`) and passes the
|
|
39
|
+
// workspace-relative path in here.
|
|
40
|
+
searchContentRef?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function classifyToolResult(input: ClassifyInput): Classification {
|
|
44
|
+
const { toolName, args, content, searchContentRef } = input;
|
|
45
|
+
|
|
46
|
+
if (toolName === WEB_SEARCH_TOOL_NAME && searchContentRef) {
|
|
47
|
+
return { kind: "pointer", contentRef: searchContentRef };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (FILE_POINTER_TOOLS.has(toolName)) {
|
|
51
|
+
const ref = filePointerFromArgs(args);
|
|
52
|
+
if (ref) return { kind: "pointer", contentRef: ref };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (IMAGE_TOOLS.has(toolName)) {
|
|
56
|
+
const ref = imagePointerFromContent(content);
|
|
57
|
+
if (ref) return { kind: "pointer", contentRef: ref };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return inlineWithTruncation(content);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function filePointerFromArgs(args: unknown): string | null {
|
|
64
|
+
if (!isRecord(args)) return null;
|
|
65
|
+
const record = args;
|
|
66
|
+
const raw = record.file_path;
|
|
67
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
68
|
+
return normalizeWorkspacePath(raw);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Image MCP tool results typically include a saved path somewhere in
|
|
72
|
+
// the stringified result. Be conservative: only treat it as a pointer
|
|
73
|
+
// when we can confidently extract an `images/` or absolute path. No
|
|
74
|
+
// match → fall back to inline (truncated) handling so the record
|
|
75
|
+
// still carries *something* useful.
|
|
76
|
+
function imagePointerFromContent(content: string): string | null {
|
|
77
|
+
// Matches a JSON-ish "filePath": "..." or "path": "..." value
|
|
78
|
+
// without using regex backtracking. Look for known keys then scan
|
|
79
|
+
// the quoted value.
|
|
80
|
+
for (const key of ['"filePath":', '"path":', '"file":']) {
|
|
81
|
+
const idx = content.indexOf(key);
|
|
82
|
+
if (idx === -1) continue;
|
|
83
|
+
const afterKey = content.slice(idx + key.length);
|
|
84
|
+
const quoteStart = afterKey.indexOf('"');
|
|
85
|
+
if (quoteStart === -1) continue;
|
|
86
|
+
const quoteEnd = afterKey.indexOf('"', quoteStart + 1);
|
|
87
|
+
if (quoteEnd === -1) continue;
|
|
88
|
+
const raw = afterKey.slice(quoteStart + 1, quoteEnd);
|
|
89
|
+
if (raw.length === 0) continue;
|
|
90
|
+
return normalizeWorkspacePath(raw);
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Strip a leading "/" or "./" so the stored ref is workspace-relative
|
|
96
|
+
// regardless of how the tool happened to quote it. Leave "../"
|
|
97
|
+
// prefixes alone — a relative escape is a bug and we want it visible
|
|
98
|
+
// rather than silently fixed up.
|
|
99
|
+
function normalizeWorkspacePath(p: string): string {
|
|
100
|
+
if (p.startsWith("./")) return p.slice(2);
|
|
101
|
+
if (p.startsWith("/")) return p.slice(1);
|
|
102
|
+
return p;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function inlineWithTruncation(content: string): Classification {
|
|
106
|
+
if (content.length <= MAX_INLINE_CONTENT_CHARS) {
|
|
107
|
+
return { kind: "inline", content, truncated: false };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
kind: "inline",
|
|
111
|
+
content: content.slice(0, MAX_INLINE_CONTENT_CHARS),
|
|
112
|
+
truncated: true,
|
|
113
|
+
};
|
|
114
|
+
}
|