specmem-hardwicksoftware 3.5.99
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/CHANGELOG.md +299 -0
- package/LICENSE.md +6406 -0
- package/README.md +539 -0
- package/bin/AegisTheme.cjs +1022 -0
- package/bin/AnsiRenderer.cjs +1055 -0
- package/bin/BoxRenderer.cjs +605 -0
- package/bin/ClaudeLiveScreen.cjs +1299 -0
- package/bin/DashboardModules.cjs +733 -0
- package/bin/LiveScreenCapture.cjs +1012 -0
- package/bin/MemoryBrowserScreen.cjs +1595 -0
- package/bin/TabManager.cjs +1414 -0
- package/bin/checkAgentStatus-fix.patch +30 -0
- package/bin/mcp-socket-client.cjs +462 -0
- package/bin/screen-utils.cjs +106 -0
- package/bin/specmem-autoclaude.cjs +663 -0
- package/bin/specmem-cleanup.cjs +421 -0
- package/bin/specmem-cli.cjs +794 -0
- package/bin/specmem-console-teamcomms-class.cjs +428 -0
- package/bin/specmem-console.cjs +8104 -0
- package/bin/specmem-statusbar.cjs +530 -0
- package/bootstrap.cjs +5065 -0
- package/claude-hooks/agent-chooser-hook.js +179 -0
- package/claude-hooks/agent-chooser-inject.js +121 -0
- package/claude-hooks/agent-loading-hook.js +990 -0
- package/claude-hooks/agent-output-fader.cjs +542 -0
- package/claude-hooks/agent-output-interceptor.js +193 -0
- package/claude-hooks/agent-type-matcher.js +419 -0
- package/claude-hooks/auto-bypass.py +74 -0
- package/claude-hooks/background-completion-silencer.js +134 -0
- package/claude-hooks/bash-auto-background.js +182 -0
- package/claude-hooks/build-cedict-dictionary.mjs +167 -0
- package/claude-hooks/bullshit-radar.cjs +323 -0
- package/claude-hooks/cedict-codes.json +270 -0
- package/claude-hooks/cedict-extracted.json +22632 -0
- package/claude-hooks/claude-watchdog.sh +401 -0
- package/claude-hooks/context-dedup.cjs +144 -0
- package/claude-hooks/context-yeeter.cjs +244 -0
- package/claude-hooks/debug-suffix.cjs +15 -0
- package/claude-hooks/debug2.cjs +7 -0
- package/claude-hooks/drilldown-enforcer.js +242 -0
- package/claude-hooks/english-morphology-standalone.cjs +149 -0
- package/claude-hooks/english-morphology.cjs +152 -0
- package/claude-hooks/extract-translations.mjs +193 -0
- package/claude-hooks/file-claim-enforcer.cjs +293 -0
- package/claude-hooks/file-claim-enforcer.js +293 -0
- package/claude-hooks/find-collisions.cjs +39 -0
- package/claude-hooks/fix-abbreviations.cjs +60 -0
- package/claude-hooks/fix-collisions.cjs +60 -0
- package/claude-hooks/fix-decompressor.cjs +79 -0
- package/claude-hooks/fix-suffixes.cjs +66 -0
- package/claude-hooks/grammar-engine.cjs +159 -0
- package/claude-hooks/input-aware-improver.js +231 -0
- package/claude-hooks/is-agent.cjs +64 -0
- package/claude-hooks/mega-test.cjs +213 -0
- package/claude-hooks/merge-dictionaries.mjs +207 -0
- package/claude-hooks/merged-codes.cjs +22675 -0
- package/claude-hooks/merged-codes.json +22676 -0
- package/claude-hooks/output-cleaner.cjs +388 -0
- package/claude-hooks/post-write-memory-hook.cjs +430 -0
- package/claude-hooks/quick-test.cjs +24 -0
- package/claude-hooks/quick-test2.cjs +24 -0
- package/claude-hooks/remove-bad-codes.cjs +23 -0
- package/claude-hooks/search-reminder-hook.js +90 -0
- package/claude-hooks/semantic-test.cjs +93 -0
- package/claude-hooks/settings.json +445 -0
- package/claude-hooks/smart-context-hook.cjs +547 -0
- package/claude-hooks/smart-context-hook.js +539 -0
- package/claude-hooks/smart-search-interceptor.js +364 -0
- package/claude-hooks/socket-connect-helper.cjs +235 -0
- package/claude-hooks/specmem/sockets/session-start.lock +1 -0
- package/claude-hooks/specmem-context-hook.cjs +357 -0
- package/claude-hooks/specmem-context-hook.js +357 -0
- package/claude-hooks/specmem-drilldown-hook.cjs +480 -0
- package/claude-hooks/specmem-drilldown-hook.js +480 -0
- package/claude-hooks/specmem-drilldown-setter.js +210 -0
- package/claude-hooks/specmem-paths.cjs +213 -0
- package/claude-hooks/specmem-precompact.js +183 -0
- package/claude-hooks/specmem-session-init.sh +33 -0
- package/claude-hooks/specmem-session-start.cjs +498 -0
- package/claude-hooks/specmem-stop-hook.cjs +73 -0
- package/claude-hooks/specmem-stop-hook.js +5 -0
- package/claude-hooks/specmem-team-comms.cjs +434 -0
- package/claude-hooks/specmem-team-member-inject.js +271 -0
- package/claude-hooks/specmem-unified-hook.py +670 -0
- package/claude-hooks/subagent-loading-hook.js +194 -0
- package/claude-hooks/sysprompt-squisher.cjs +167 -0
- package/claude-hooks/task-progress-hook.js +204 -0
- package/claude-hooks/team-comms-enforcer.cjs +585 -0
- package/claude-hooks/test-accuracy.cjs +27 -0
- package/claude-hooks/test-big.cjs +28 -0
- package/claude-hooks/test-inflectors.cjs +39 -0
- package/claude-hooks/test-pluralize.cjs +37 -0
- package/claude-hooks/test-quick.cjs +8 -0
- package/claude-hooks/test-wink.cjs +20 -0
- package/claude-hooks/token-compressor.cjs +940 -0
- package/claude-hooks/use-code-pointers.cjs +279 -0
- package/commands/COMMAND_TOOL_MAP.md +299 -0
- package/commands/specmem-agents.md +412 -0
- package/commands/specmem-autoclaude.md +295 -0
- package/commands/specmem-changes.md +247 -0
- package/commands/specmem-code.md +103 -0
- package/commands/specmem-configteammembercomms.md +322 -0
- package/commands/specmem-drilldown.md +208 -0
- package/commands/specmem-find.md +195 -0
- package/commands/specmem-getdashboard.md +243 -0
- package/commands/specmem-hooks.md +219 -0
- package/commands/specmem-pointers.md +149 -0
- package/commands/specmem-progress.md +287 -0
- package/commands/specmem-remember.md +123 -0
- package/commands/specmem-service.md +349 -0
- package/commands/specmem-stats.md +189 -0
- package/commands/specmem-team-member.md +409 -0
- package/commands/specmem-webdev.md +583 -0
- package/commands/specmem.md +363 -0
- package/dist/autoStart/index.d.ts +214 -0
- package/dist/autoStart/index.d.ts.map +1 -0
- package/dist/autoStart/index.js +883 -0
- package/dist/autoStart/index.js.map +1 -0
- package/dist/claude-sessions/contextRestorationParser.d.ts +74 -0
- package/dist/claude-sessions/contextRestorationParser.d.ts.map +1 -0
- package/dist/claude-sessions/contextRestorationParser.js +570 -0
- package/dist/claude-sessions/contextRestorationParser.js.map +1 -0
- package/dist/claude-sessions/index.d.ts +13 -0
- package/dist/claude-sessions/index.d.ts.map +1 -0
- package/dist/claude-sessions/index.js +11 -0
- package/dist/claude-sessions/index.js.map +1 -0
- package/dist/claude-sessions/sessionIntegration.d.ts +48 -0
- package/dist/claude-sessions/sessionIntegration.d.ts.map +1 -0
- package/dist/claude-sessions/sessionIntegration.js +146 -0
- package/dist/claude-sessions/sessionIntegration.js.map +1 -0
- package/dist/claude-sessions/sessionParser.d.ts +293 -0
- package/dist/claude-sessions/sessionParser.d.ts.map +1 -0
- package/dist/claude-sessions/sessionParser.js +1028 -0
- package/dist/claude-sessions/sessionParser.js.map +1 -0
- package/dist/claude-sessions/sessionWatcher.d.ts +139 -0
- package/dist/claude-sessions/sessionWatcher.d.ts.map +1 -0
- package/dist/claude-sessions/sessionWatcher.js +722 -0
- package/dist/claude-sessions/sessionWatcher.js.map +1 -0
- package/dist/cli/deploy-to-claude.d.ts +56 -0
- package/dist/cli/deploy-to-claude.d.ts.map +1 -0
- package/dist/cli/deploy-to-claude.js +576 -0
- package/dist/cli/deploy-to-claude.js.map +1 -0
- package/dist/code-explanations/explainCode.d.ts +86 -0
- package/dist/code-explanations/explainCode.d.ts.map +1 -0
- package/dist/code-explanations/explainCode.js +286 -0
- package/dist/code-explanations/explainCode.js.map +1 -0
- package/dist/code-explanations/feedback.d.ts +87 -0
- package/dist/code-explanations/feedback.d.ts.map +1 -0
- package/dist/code-explanations/feedback.js +212 -0
- package/dist/code-explanations/feedback.js.map +1 -0
- package/dist/code-explanations/getRelatedCode.d.ts +80 -0
- package/dist/code-explanations/getRelatedCode.d.ts.map +1 -0
- package/dist/code-explanations/getRelatedCode.js +262 -0
- package/dist/code-explanations/getRelatedCode.js.map +1 -0
- package/dist/code-explanations/index.d.ts +284 -0
- package/dist/code-explanations/index.d.ts.map +1 -0
- package/dist/code-explanations/index.js +249 -0
- package/dist/code-explanations/index.js.map +1 -0
- package/dist/code-explanations/linkCodeToPrompt.d.ts +79 -0
- package/dist/code-explanations/linkCodeToPrompt.d.ts.map +1 -0
- package/dist/code-explanations/linkCodeToPrompt.js +213 -0
- package/dist/code-explanations/linkCodeToPrompt.js.map +1 -0
- package/dist/code-explanations/recallExplanation.d.ts +88 -0
- package/dist/code-explanations/recallExplanation.d.ts.map +1 -0
- package/dist/code-explanations/recallExplanation.js +218 -0
- package/dist/code-explanations/recallExplanation.js.map +1 -0
- package/dist/code-explanations/schema.d.ts +32 -0
- package/dist/code-explanations/schema.d.ts.map +1 -0
- package/dist/code-explanations/schema.js +221 -0
- package/dist/code-explanations/schema.js.map +1 -0
- package/dist/code-explanations/semanticSearch.d.ts +75 -0
- package/dist/code-explanations/semanticSearch.d.ts.map +1 -0
- package/dist/code-explanations/semanticSearch.js +203 -0
- package/dist/code-explanations/semanticSearch.js.map +1 -0
- package/dist/code-explanations/types.d.ts +328 -0
- package/dist/code-explanations/types.d.ts.map +1 -0
- package/dist/code-explanations/types.js +122 -0
- package/dist/code-explanations/types.js.map +1 -0
- package/dist/codebase/codeAnalyzer.d.ts +272 -0
- package/dist/codebase/codeAnalyzer.d.ts.map +1 -0
- package/dist/codebase/codeAnalyzer.js +1353 -0
- package/dist/codebase/codeAnalyzer.js.map +1 -0
- package/dist/codebase/codebaseIndexer.d.ts +360 -0
- package/dist/codebase/codebaseIndexer.d.ts.map +1 -0
- package/dist/codebase/codebaseIndexer.js +1735 -0
- package/dist/codebase/codebaseIndexer.js.map +1 -0
- package/dist/codebase/codebaseTools.d.ts +853 -0
- package/dist/codebase/codebaseTools.d.ts.map +1 -0
- package/dist/codebase/codebaseTools.js +1279 -0
- package/dist/codebase/codebaseTools.js.map +1 -0
- package/dist/codebase/exclusions.d.ts +111 -0
- package/dist/codebase/exclusions.d.ts.map +1 -0
- package/dist/codebase/exclusions.js +771 -0
- package/dist/codebase/exclusions.js.map +1 -0
- package/dist/codebase/fileWatcher.d.ts +135 -0
- package/dist/codebase/fileWatcher.d.ts.map +1 -0
- package/dist/codebase/fileWatcher.js +309 -0
- package/dist/codebase/fileWatcher.js.map +1 -0
- package/dist/codebase/index.d.ts +33 -0
- package/dist/codebase/index.d.ts.map +1 -0
- package/dist/codebase/index.js +77 -0
- package/dist/codebase/index.js.map +1 -0
- package/dist/codebase/ingestion.d.ts +177 -0
- package/dist/codebase/ingestion.d.ts.map +1 -0
- package/dist/codebase/ingestion.js +690 -0
- package/dist/codebase/ingestion.js.map +1 -0
- package/dist/codebase/languageDetection.d.ts +75 -0
- package/dist/codebase/languageDetection.d.ts.map +1 -0
- package/dist/codebase/languageDetection.js +768 -0
- package/dist/codebase/languageDetection.js.map +1 -0
- package/dist/commands/codebaseCommands.d.ts +101 -0
- package/dist/commands/codebaseCommands.d.ts.map +1 -0
- package/dist/commands/codebaseCommands.js +911 -0
- package/dist/commands/codebaseCommands.js.map +1 -0
- package/dist/commands/commandHandler.d.ts +126 -0
- package/dist/commands/commandHandler.d.ts.map +1 -0
- package/dist/commands/commandHandler.js +296 -0
- package/dist/commands/commandHandler.js.map +1 -0
- package/dist/commands/commandLoader.d.ts +103 -0
- package/dist/commands/commandLoader.d.ts.map +1 -0
- package/dist/commands/commandLoader.js +223 -0
- package/dist/commands/commandLoader.js.map +1 -0
- package/dist/commands/contextCommands.d.ts +83 -0
- package/dist/commands/contextCommands.d.ts.map +1 -0
- package/dist/commands/contextCommands.js +512 -0
- package/dist/commands/contextCommands.js.map +1 -0
- package/dist/commands/index.d.ts +24 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/mcpResources.d.ts +50 -0
- package/dist/commands/mcpResources.d.ts.map +1 -0
- package/dist/commands/mcpResources.js +372 -0
- package/dist/commands/mcpResources.js.map +1 -0
- package/dist/commands/memoryCommands.d.ts +74 -0
- package/dist/commands/memoryCommands.d.ts.map +1 -0
- package/dist/commands/memoryCommands.js +609 -0
- package/dist/commands/memoryCommands.js.map +1 -0
- package/dist/commands/promptCommands.d.ts +91 -0
- package/dist/commands/promptCommands.d.ts.map +1 -0
- package/dist/commands/promptCommands.js +801 -0
- package/dist/commands/promptCommands.js.map +1 -0
- package/dist/commands/teamMemberCommands.d.ts +21 -0
- package/dist/commands/teamMemberCommands.d.ts.map +1 -0
- package/dist/commands/teamMemberCommands.js +137 -0
- package/dist/commands/teamMemberCommands.js.map +1 -0
- package/dist/comms/fileCommsTransport.d.ts +91 -0
- package/dist/comms/fileCommsTransport.d.ts.map +1 -0
- package/dist/comms/fileCommsTransport.js +244 -0
- package/dist/comms/fileCommsTransport.js.map +1 -0
- package/dist/comms/index.d.ts +7 -0
- package/dist/comms/index.d.ts.map +1 -0
- package/dist/comms/index.js +7 -0
- package/dist/comms/index.js.map +1 -0
- package/dist/config/apiKeyDetection.d.ts +41 -0
- package/dist/config/apiKeyDetection.d.ts.map +1 -0
- package/dist/config/apiKeyDetection.js +211 -0
- package/dist/config/apiKeyDetection.js.map +1 -0
- package/dist/config/autoConfig.d.ts +188 -0
- package/dist/config/autoConfig.d.ts.map +1 -0
- package/dist/config/autoConfig.js +850 -0
- package/dist/config/autoConfig.js.map +1 -0
- package/dist/config/configSync.d.ts +119 -0
- package/dist/config/configSync.d.ts.map +1 -0
- package/dist/config/configSync.js +878 -0
- package/dist/config/configSync.js.map +1 -0
- package/dist/config/embeddingTimeouts.d.ts +145 -0
- package/dist/config/embeddingTimeouts.d.ts.map +1 -0
- package/dist/config/embeddingTimeouts.js +255 -0
- package/dist/config/embeddingTimeouts.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/languageConfig.d.ts +68 -0
- package/dist/config/languageConfig.d.ts.map +1 -0
- package/dist/config/languageConfig.js +473 -0
- package/dist/config/languageConfig.js.map +1 -0
- package/dist/config/password.d.ts +145 -0
- package/dist/config/password.d.ts.map +1 -0
- package/dist/config/password.js +428 -0
- package/dist/config/password.js.map +1 -0
- package/dist/config.d.ts +338 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1177 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidation.d.ts +44 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +447 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/constants.d.ts +371 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +552 -0
- package/dist/constants.js.map +1 -0
- package/dist/coordination/TeamMemberRegistry.d.ts +192 -0
- package/dist/coordination/TeamMemberRegistry.d.ts.map +1 -0
- package/dist/coordination/TeamMemberRegistry.js +415 -0
- package/dist/coordination/TeamMemberRegistry.js.map +1 -0
- package/dist/coordination/events.d.ts +369 -0
- package/dist/coordination/events.d.ts.map +1 -0
- package/dist/coordination/events.js +232 -0
- package/dist/coordination/events.js.map +1 -0
- package/dist/coordination/handlers.d.ts +116 -0
- package/dist/coordination/handlers.d.ts.map +1 -0
- package/dist/coordination/handlers.js +400 -0
- package/dist/coordination/handlers.js.map +1 -0
- package/dist/coordination/index.d.ts +14 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +31 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/integration.d.ts +260 -0
- package/dist/coordination/integration.d.ts.map +1 -0
- package/dist/coordination/integration.js +472 -0
- package/dist/coordination/integration.js.map +1 -0
- package/dist/coordination/server.d.ts +266 -0
- package/dist/coordination/server.d.ts.map +1 -0
- package/dist/coordination/server.js +995 -0
- package/dist/coordination/server.js.map +1 -0
- package/dist/coordination/serviceProvider.d.ts +70 -0
- package/dist/coordination/serviceProvider.d.ts.map +1 -0
- package/dist/coordination/serviceProvider.js +273 -0
- package/dist/coordination/serviceProvider.js.map +1 -0
- package/dist/dashboard/api/claudeControl.d.ts +44 -0
- package/dist/dashboard/api/claudeControl.d.ts.map +1 -0
- package/dist/dashboard/api/claudeControl.js +650 -0
- package/dist/dashboard/api/claudeControl.js.map +1 -0
- package/dist/dashboard/api/claudeHistory.d.ts +4 -0
- package/dist/dashboard/api/claudeHistory.d.ts.map +1 -0
- package/dist/dashboard/api/claudeHistory.js +319 -0
- package/dist/dashboard/api/claudeHistory.js.map +1 -0
- package/dist/dashboard/api/dataExport.d.ts +23 -0
- package/dist/dashboard/api/dataExport.d.ts.map +1 -0
- package/dist/dashboard/api/dataExport.js +509 -0
- package/dist/dashboard/api/dataExport.js.map +1 -0
- package/dist/dashboard/api/fileManager.d.ts +39 -0
- package/dist/dashboard/api/fileManager.d.ts.map +1 -0
- package/dist/dashboard/api/fileManager.js +814 -0
- package/dist/dashboard/api/fileManager.js.map +1 -0
- package/dist/dashboard/api/hooks.d.ts +16 -0
- package/dist/dashboard/api/hooks.d.ts.map +1 -0
- package/dist/dashboard/api/hooks.js +342 -0
- package/dist/dashboard/api/hooks.js.map +1 -0
- package/dist/dashboard/api/hotReload.d.ts +14 -0
- package/dist/dashboard/api/hotReload.d.ts.map +1 -0
- package/dist/dashboard/api/hotReload.js +219 -0
- package/dist/dashboard/api/hotReload.js.map +1 -0
- package/dist/dashboard/api/liveSessionStream.d.ts +19 -0
- package/dist/dashboard/api/liveSessionStream.d.ts.map +1 -0
- package/dist/dashboard/api/liveSessionStream.js +430 -0
- package/dist/dashboard/api/liveSessionStream.js.map +1 -0
- package/dist/dashboard/api/memoryRecall.d.ts +20 -0
- package/dist/dashboard/api/memoryRecall.d.ts.map +1 -0
- package/dist/dashboard/api/memoryRecall.js +524 -0
- package/dist/dashboard/api/memoryRecall.js.map +1 -0
- package/dist/dashboard/api/promptSend.d.ts +33 -0
- package/dist/dashboard/api/promptSend.d.ts.map +1 -0
- package/dist/dashboard/api/promptSend.js +544 -0
- package/dist/dashboard/api/promptSend.js.map +1 -0
- package/dist/dashboard/api/settings.d.ts +10 -0
- package/dist/dashboard/api/settings.d.ts.map +1 -0
- package/dist/dashboard/api/settings.js +656 -0
- package/dist/dashboard/api/settings.js.map +1 -0
- package/dist/dashboard/api/setup.d.ts +21 -0
- package/dist/dashboard/api/setup.d.ts.map +1 -0
- package/dist/dashboard/api/setup.js +663 -0
- package/dist/dashboard/api/setup.js.map +1 -0
- package/dist/dashboard/api/specmemTools.d.ts +14 -0
- package/dist/dashboard/api/specmemTools.d.ts.map +1 -0
- package/dist/dashboard/api/specmemTools.js +1059 -0
- package/dist/dashboard/api/specmemTools.js.map +1 -0
- package/dist/dashboard/api/taskTeamMembers.d.ts +8 -0
- package/dist/dashboard/api/taskTeamMembers.d.ts.map +1 -0
- package/dist/dashboard/api/taskTeamMembers.js +136 -0
- package/dist/dashboard/api/taskTeamMembers.js.map +1 -0
- package/dist/dashboard/api/teamMemberDeploy.d.ts +15 -0
- package/dist/dashboard/api/teamMemberDeploy.d.ts.map +1 -0
- package/dist/dashboard/api/teamMemberDeploy.js +421 -0
- package/dist/dashboard/api/teamMemberDeploy.js.map +1 -0
- package/dist/dashboard/api/teamMemberHistory.d.ts +38 -0
- package/dist/dashboard/api/teamMemberHistory.d.ts.map +1 -0
- package/dist/dashboard/api/teamMemberHistory.js +583 -0
- package/dist/dashboard/api/teamMemberHistory.js.map +1 -0
- package/dist/dashboard/api/terminal.d.ts +12 -0
- package/dist/dashboard/api/terminal.d.ts.map +1 -0
- package/dist/dashboard/api/terminal.js +344 -0
- package/dist/dashboard/api/terminal.js.map +1 -0
- package/dist/dashboard/api/terminalInject.d.ts +17 -0
- package/dist/dashboard/api/terminalInject.d.ts.map +1 -0
- package/dist/dashboard/api/terminalInject.js +322 -0
- package/dist/dashboard/api/terminalInject.js.map +1 -0
- package/dist/dashboard/api/terminalStream.d.ts +12 -0
- package/dist/dashboard/api/terminalStream.d.ts.map +1 -0
- package/dist/dashboard/api/terminalStream.js +482 -0
- package/dist/dashboard/api/terminalStream.js.map +1 -0
- package/dist/dashboard/index.d.ts +7 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/ptyStreamer.d.ts +173 -0
- package/dist/dashboard/ptyStreamer.d.ts.map +1 -0
- package/dist/dashboard/ptyStreamer.js +661 -0
- package/dist/dashboard/ptyStreamer.js.map +1 -0
- package/dist/dashboard/public/DASHBOARD-README.md +378 -0
- package/dist/dashboard/public/INTEGRATION-GUIDE.md +395 -0
- package/dist/dashboard/public/codebase-config.html +1247 -0
- package/dist/dashboard/public/dashboard-v2.html +1942 -0
- package/dist/dashboard/public/data-export.html +819 -0
- package/dist/dashboard/public/example-page.html +164 -0
- package/dist/dashboard/public/file-explorer.html +1023 -0
- package/dist/dashboard/public/hooks.html +1103 -0
- package/dist/dashboard/public/index-improvements.css +499 -0
- package/dist/dashboard/public/index.html +5534 -0
- package/dist/dashboard/public/memory-controls.html +1959 -0
- package/dist/dashboard/public/memory-recall.html +1495 -0
- package/dist/dashboard/public/previews/skeleton-memory-graph.html +361 -0
- package/dist/dashboard/public/previews/skeleton-memory-list.html +366 -0
- package/dist/dashboard/public/previews/skeleton-search-results.html +609 -0
- package/dist/dashboard/public/previews/skeleton-stats-dashboard.html +556 -0
- package/dist/dashboard/public/prompt-console.html +2763 -0
- package/dist/dashboard/public/react-dist/assets/index-CkjobT5B.js +871 -0
- package/dist/dashboard/public/react-dist/assets/index-iRclxMst.css +1 -0
- package/dist/dashboard/public/react-dist/index.html +16 -0
- package/dist/dashboard/public/shared-header.js +325 -0
- package/dist/dashboard/public/shared-language-selector.js +626 -0
- package/dist/dashboard/public/shared-logger.js +66 -0
- package/dist/dashboard/public/shared-nav.js +325 -0
- package/dist/dashboard/public/shared-theme-blue.css +331 -0
- package/dist/dashboard/public/shared-theme.css +813 -0
- package/dist/dashboard/public/shared-toast.js +415 -0
- package/dist/dashboard/public/team-member-history.html +1291 -0
- package/dist/dashboard/public/team-member-spy.html +1199 -0
- package/dist/dashboard/public/team-members.html +3756 -0
- package/dist/dashboard/public/terminal-output.html +1013 -0
- package/dist/dashboard/public/terminal.html +372 -0
- package/dist/dashboard/sessionStore.d.ts +86 -0
- package/dist/dashboard/sessionStore.d.ts.map +1 -0
- package/dist/dashboard/sessionStore.js +262 -0
- package/dist/dashboard/sessionStore.js.map +1 -0
- package/dist/dashboard/standalone.d.ts +27 -0
- package/dist/dashboard/standalone.d.ts.map +1 -0
- package/dist/dashboard/standalone.js +380 -0
- package/dist/dashboard/standalone.js.map +1 -0
- package/dist/dashboard/webServer.d.ts +390 -0
- package/dist/dashboard/webServer.d.ts.map +1 -0
- package/dist/dashboard/webServer.js +4297 -0
- package/dist/dashboard/webServer.js.map +1 -0
- package/dist/dashboard/websocket/teamMemberStream.d.ts +87 -0
- package/dist/dashboard/websocket/teamMemberStream.d.ts.map +1 -0
- package/dist/dashboard/websocket/teamMemberStream.js +366 -0
- package/dist/dashboard/websocket/teamMemberStream.js.map +1 -0
- package/dist/dashboard/websocket/terminalStream.d.ts +130 -0
- package/dist/dashboard/websocket/terminalStream.d.ts.map +1 -0
- package/dist/dashboard/websocket/terminalStream.js +456 -0
- package/dist/dashboard/websocket/terminalStream.js.map +1 -0
- package/dist/database/embeddedPostgres.d.ts +187 -0
- package/dist/database/embeddedPostgres.d.ts.map +1 -0
- package/dist/database/embeddedPostgres.js +763 -0
- package/dist/database/embeddedPostgres.js.map +1 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +20 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/initEmbeddedPostgres.d.ts +124 -0
- package/dist/database/initEmbeddedPostgres.d.ts.map +1 -0
- package/dist/database/initEmbeddedPostgres.js +855 -0
- package/dist/database/initEmbeddedPostgres.js.map +1 -0
- package/dist/database.d.ts +256 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +1209 -0
- package/dist/database.js.map +1 -0
- package/dist/db/apiDataManager.d.ts +334 -0
- package/dist/db/apiDataManager.d.ts.map +1 -0
- package/dist/db/apiDataManager.js +631 -0
- package/dist/db/apiDataManager.js.map +1 -0
- package/dist/db/batchOperations.d.ts +154 -0
- package/dist/db/batchOperations.d.ts.map +1 -0
- package/dist/db/batchOperations.js +564 -0
- package/dist/db/batchOperations.js.map +1 -0
- package/dist/db/bigBrainMigrations.d.ts +48 -0
- package/dist/db/bigBrainMigrations.d.ts.map +1 -0
- package/dist/db/bigBrainMigrations.js +4266 -0
- package/dist/db/bigBrainMigrations.js.map +1 -0
- package/dist/db/connectionPoolGoBrrr.d.ts +94 -0
- package/dist/db/connectionPoolGoBrrr.d.ts.map +1 -0
- package/dist/db/connectionPoolGoBrrr.js +548 -0
- package/dist/db/connectionPoolGoBrrr.js.map +1 -0
- package/dist/db/dashboardQueries.d.ts +182 -0
- package/dist/db/dashboardQueries.d.ts.map +1 -0
- package/dist/db/dashboardQueries.js +821 -0
- package/dist/db/dashboardQueries.js.map +1 -0
- package/dist/db/deploymentBootstrap.d.ts +43 -0
- package/dist/db/deploymentBootstrap.d.ts.map +1 -0
- package/dist/db/deploymentBootstrap.js +329 -0
- package/dist/db/deploymentBootstrap.js.map +1 -0
- package/dist/db/dimensionService.d.ts +140 -0
- package/dist/db/dimensionService.d.ts.map +1 -0
- package/dist/db/dimensionService.js +261 -0
- package/dist/db/dimensionService.js.map +1 -0
- package/dist/db/embeddingOverflow.d.ts +69 -0
- package/dist/db/embeddingOverflow.d.ts.map +1 -0
- package/dist/db/embeddingOverflow.js +332 -0
- package/dist/db/embeddingOverflow.js.map +1 -0
- package/dist/db/embeddingOverflow.sql +221 -0
- package/dist/db/findThatShit.d.ts +145 -0
- package/dist/db/findThatShit.d.ts.map +1 -0
- package/dist/db/findThatShit.js +782 -0
- package/dist/db/findThatShit.js.map +1 -0
- package/dist/db/hotPathManager.d.ts +187 -0
- package/dist/db/hotPathManager.d.ts.map +1 -0
- package/dist/db/hotPathManager.js +504 -0
- package/dist/db/hotPathManager.js.map +1 -0
- package/dist/db/index.d.ts +85 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +219 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/memoryDrilldown.sql +99 -0
- package/dist/db/migrate.d.ts +3 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +97 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrateJsonToPostgres.d.ts +43 -0
- package/dist/db/migrateJsonToPostgres.d.ts.map +1 -0
- package/dist/db/migrateJsonToPostgres.js +465 -0
- package/dist/db/migrateJsonToPostgres.js.map +1 -0
- package/dist/db/nukeFromOrbit.d.ts +63 -0
- package/dist/db/nukeFromOrbit.d.ts.map +1 -0
- package/dist/db/nukeFromOrbit.js +499 -0
- package/dist/db/nukeFromOrbit.js.map +1 -0
- package/dist/db/processedTraining.sql +60 -0
- package/dist/db/projectNamespacing.d.ts +258 -0
- package/dist/db/projectNamespacing.d.ts.map +1 -0
- package/dist/db/projectNamespacing.js +920 -0
- package/dist/db/projectNamespacing.js.map +1 -0
- package/dist/db/projectNamespacing.sql +374 -0
- package/dist/db/projectSchemaInit.sql +271 -0
- package/dist/db/spatialMemory.d.ts +296 -0
- package/dist/db/spatialMemory.d.ts.map +1 -0
- package/dist/db/spatialMemory.js +818 -0
- package/dist/db/spatialMemory.js.map +1 -0
- package/dist/db/streamingQuery.d.ts +143 -0
- package/dist/db/streamingQuery.d.ts.map +1 -0
- package/dist/db/streamingQuery.js +350 -0
- package/dist/db/streamingQuery.js.map +1 -0
- package/dist/db/teamComms.sql +224 -0
- package/dist/db/yeetStuffInDb.d.ts +72 -0
- package/dist/db/yeetStuffInDb.d.ts.map +1 -0
- package/dist/db/yeetStuffInDb.js +473 -0
- package/dist/db/yeetStuffInDb.js.map +1 -0
- package/dist/embedding-providers/index.d.ts +10 -0
- package/dist/embedding-providers/index.d.ts.map +1 -0
- package/dist/embedding-providers/index.js +12 -0
- package/dist/embedding-providers/index.js.map +1 -0
- package/dist/embeddings/projectionLayer.d.ts +114 -0
- package/dist/embeddings/projectionLayer.d.ts.map +1 -0
- package/dist/embeddings/projectionLayer.js +345 -0
- package/dist/embeddings/projectionLayer.js.map +1 -0
- package/dist/events/Publisher.d.ts +193 -0
- package/dist/events/Publisher.d.ts.map +1 -0
- package/dist/events/Publisher.js +439 -0
- package/dist/events/Publisher.js.map +1 -0
- package/dist/events/config.d.ts +139 -0
- package/dist/events/config.d.ts.map +1 -0
- package/dist/events/config.js +266 -0
- package/dist/events/config.js.map +1 -0
- package/dist/events/index.d.ts +19 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +31 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/integration.d.ts +206 -0
- package/dist/events/integration.d.ts.map +1 -0
- package/dist/events/integration.js +348 -0
- package/dist/events/integration.js.map +1 -0
- package/dist/events/metrics.d.ts +147 -0
- package/dist/events/metrics.d.ts.map +1 -0
- package/dist/events/metrics.js +343 -0
- package/dist/events/metrics.js.map +1 -0
- package/dist/hooks/cli.d.ts +28 -0
- package/dist/hooks/cli.d.ts.map +1 -0
- package/dist/hooks/cli.js +118 -0
- package/dist/hooks/cli.js.map +1 -0
- package/dist/hooks/contextInjectionHook.d.ts +60 -0
- package/dist/hooks/contextInjectionHook.d.ts.map +1 -0
- package/dist/hooks/contextInjectionHook.js +294 -0
- package/dist/hooks/contextInjectionHook.js.map +1 -0
- package/dist/hooks/drilldownHook.d.ts +125 -0
- package/dist/hooks/drilldownHook.d.ts.map +1 -0
- package/dist/hooks/drilldownHook.js +181 -0
- package/dist/hooks/drilldownHook.js.map +1 -0
- package/dist/hooks/hookManager.d.ts +180 -0
- package/dist/hooks/hookManager.d.ts.map +1 -0
- package/dist/hooks/hookManager.js +782 -0
- package/dist/hooks/hookManager.js.map +1 -0
- package/dist/hooks/index.d.ts +62 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +66 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/lowContextHook.d.ts +71 -0
- package/dist/hooks/lowContextHook.d.ts.map +1 -0
- package/dist/hooks/lowContextHook.js +258 -0
- package/dist/hooks/lowContextHook.js.map +1 -0
- package/dist/hooks/simpleContextHook.d.ts +65 -0
- package/dist/hooks/simpleContextHook.d.ts.map +1 -0
- package/dist/hooks/simpleContextHook.js +194 -0
- package/dist/hooks/simpleContextHook.js.map +1 -0
- package/dist/hooks/teamFramingCli.d.ts +56 -0
- package/dist/hooks/teamFramingCli.d.ts.map +1 -0
- package/dist/hooks/teamFramingCli.js +264 -0
- package/dist/hooks/teamFramingCli.js.map +1 -0
- package/dist/hooks/teamMemberPrepromptHook.d.ts +150 -0
- package/dist/hooks/teamMemberPrepromptHook.d.ts.map +1 -0
- package/dist/hooks/teamMemberPrepromptHook.js +308 -0
- package/dist/hooks/teamMemberPrepromptHook.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4433 -0
- package/dist/index.js.map +1 -0
- package/dist/init/claudeConfigInjector.d.ts +116 -0
- package/dist/init/claudeConfigInjector.d.ts.map +1 -0
- package/dist/init/claudeConfigInjector.js +1154 -0
- package/dist/init/claudeConfigInjector.js.map +1 -0
- package/dist/installer/autoInstall.d.ts +72 -0
- package/dist/installer/autoInstall.d.ts.map +1 -0
- package/dist/installer/autoInstall.js +617 -0
- package/dist/installer/autoInstall.js.map +1 -0
- package/dist/installer/dbSetup.d.ts +84 -0
- package/dist/installer/dbSetup.d.ts.map +1 -0
- package/dist/installer/dbSetup.js +350 -0
- package/dist/installer/dbSetup.js.map +1 -0
- package/dist/installer/firstRun.d.ts +49 -0
- package/dist/installer/firstRun.d.ts.map +1 -0
- package/dist/installer/firstRun.js +207 -0
- package/dist/installer/firstRun.js.map +1 -0
- package/dist/installer/index.d.ts +10 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +10 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/silentAutoInstall.d.ts +99 -0
- package/dist/installer/silentAutoInstall.d.ts.map +1 -0
- package/dist/installer/silentAutoInstall.js +491 -0
- package/dist/installer/silentAutoInstall.js.map +1 -0
- package/dist/installer/systemDeps.d.ts +54 -0
- package/dist/installer/systemDeps.d.ts.map +1 -0
- package/dist/installer/systemDeps.js +322 -0
- package/dist/installer/systemDeps.js.map +1 -0
- package/dist/mcp/cliNotifications.d.ts +133 -0
- package/dist/mcp/cliNotifications.d.ts.map +1 -0
- package/dist/mcp/cliNotifications.js +289 -0
- package/dist/mcp/cliNotifications.js.map +1 -0
- package/dist/mcp/embeddingServerManager.d.ts +307 -0
- package/dist/mcp/embeddingServerManager.d.ts.map +1 -0
- package/dist/mcp/embeddingServerManager.js +2081 -0
- package/dist/mcp/embeddingServerManager.js.map +1 -0
- package/dist/mcp/healthMonitor.d.ts +196 -0
- package/dist/mcp/healthMonitor.d.ts.map +1 -0
- package/dist/mcp/healthMonitor.js +685 -0
- package/dist/mcp/healthMonitor.js.map +1 -0
- package/dist/mcp/hotReloadManager.d.ts +101 -0
- package/dist/mcp/hotReloadManager.d.ts.map +1 -0
- package/dist/mcp/hotReloadManager.js +251 -0
- package/dist/mcp/hotReloadManager.js.map +1 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcpProtocolHandler.d.ts +64 -0
- package/dist/mcp/mcpProtocolHandler.d.ts.map +1 -0
- package/dist/mcp/mcpProtocolHandler.js +253 -0
- package/dist/mcp/mcpProtocolHandler.js.map +1 -0
- package/dist/mcp/miniCOTServerManager.d.ts +336 -0
- package/dist/mcp/miniCOTServerManager.d.ts.map +1 -0
- package/dist/mcp/miniCOTServerManager.js +1384 -0
- package/dist/mcp/miniCOTServerManager.js.map +1 -0
- package/dist/mcp/promptExecutor.d.ts +188 -0
- package/dist/mcp/promptExecutor.d.ts.map +1 -0
- package/dist/mcp/promptExecutor.js +469 -0
- package/dist/mcp/promptExecutor.js.map +1 -0
- package/dist/mcp/reloadBroadcast.d.ts +127 -0
- package/dist/mcp/reloadBroadcast.d.ts.map +1 -0
- package/dist/mcp/reloadBroadcast.js +275 -0
- package/dist/mcp/reloadBroadcast.js.map +1 -0
- package/dist/mcp/resilientTransport.d.ts +249 -0
- package/dist/mcp/resilientTransport.d.ts.map +1 -0
- package/dist/mcp/resilientTransport.js +931 -0
- package/dist/mcp/resilientTransport.js.map +1 -0
- package/dist/mcp/samplingHandler.d.ts +129 -0
- package/dist/mcp/samplingHandler.d.ts.map +1 -0
- package/dist/mcp/samplingHandler.js +276 -0
- package/dist/mcp/samplingHandler.js.map +1 -0
- package/dist/mcp/specMemServer.d.ts +305 -0
- package/dist/mcp/specMemServer.d.ts.map +1 -0
- package/dist/mcp/specMemServer.js +2048 -0
- package/dist/mcp/specMemServer.js.map +1 -0
- package/dist/mcp/toolRegistry.d.ts +122 -0
- package/dist/mcp/toolRegistry.d.ts.map +1 -0
- package/dist/mcp/toolRegistry.js +609 -0
- package/dist/mcp/toolRegistry.js.map +1 -0
- package/dist/mcp/tools/embeddingControl.d.ts +114 -0
- package/dist/mcp/tools/embeddingControl.d.ts.map +1 -0
- package/dist/mcp/tools/embeddingControl.js +222 -0
- package/dist/mcp/tools/embeddingControl.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +10 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +17 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/teamComms.d.ts +444 -0
- package/dist/mcp/tools/teamComms.d.ts.map +1 -0
- package/dist/mcp/tools/teamComms.js +1953 -0
- package/dist/mcp/tools/teamComms.js.map +1 -0
- package/dist/mcp/triggerSystem.d.ts +129 -0
- package/dist/mcp/triggerSystem.d.ts.map +1 -0
- package/dist/mcp/triggerSystem.js +363 -0
- package/dist/mcp/triggerSystem.js.map +1 -0
- package/dist/mcp/watcherIntegration.d.ts +77 -0
- package/dist/mcp/watcherIntegration.d.ts.map +1 -0
- package/dist/mcp/watcherIntegration.js +428 -0
- package/dist/mcp/watcherIntegration.js.map +1 -0
- package/dist/mcp/watcherToolWrappers.d.ts +89 -0
- package/dist/mcp/watcherToolWrappers.d.ts.map +1 -0
- package/dist/mcp/watcherToolWrappers.js +91 -0
- package/dist/mcp/watcherToolWrappers.js.map +1 -0
- package/dist/memorization/claudeCodeMigration.d.ts +34 -0
- package/dist/memorization/claudeCodeMigration.d.ts.map +1 -0
- package/dist/memorization/claudeCodeMigration.js +210 -0
- package/dist/memorization/claudeCodeMigration.js.map +1 -0
- package/dist/memorization/claudeCodeTracker.d.ts +147 -0
- package/dist/memorization/claudeCodeTracker.d.ts.map +1 -0
- package/dist/memorization/claudeCodeTracker.js +424 -0
- package/dist/memorization/claudeCodeTracker.js.map +1 -0
- package/dist/memorization/codeMemorizer.d.ts +158 -0
- package/dist/memorization/codeMemorizer.d.ts.map +1 -0
- package/dist/memorization/codeMemorizer.js +357 -0
- package/dist/memorization/codeMemorizer.js.map +1 -0
- package/dist/memorization/codeRecall.d.ts +156 -0
- package/dist/memorization/codeRecall.d.ts.map +1 -0
- package/dist/memorization/codeRecall.js +499 -0
- package/dist/memorization/codeRecall.js.map +1 -0
- package/dist/memorization/index.d.ts +55 -0
- package/dist/memorization/index.d.ts.map +1 -0
- package/dist/memorization/index.js +64 -0
- package/dist/memorization/index.js.map +1 -0
- package/dist/memorization/memorizationTools.d.ts +413 -0
- package/dist/memorization/memorizationTools.d.ts.map +1 -0
- package/dist/memorization/memorizationTools.js +513 -0
- package/dist/memorization/memorizationTools.js.map +1 -0
- package/dist/memorization/watcherIntegration.d.ts +100 -0
- package/dist/memorization/watcherIntegration.d.ts.map +1 -0
- package/dist/memorization/watcherIntegration.js +196 -0
- package/dist/memorization/watcherIntegration.js.map +1 -0
- package/dist/memory/humanLikeMemory.d.ts +206 -0
- package/dist/memory/humanLikeMemory.d.ts.map +1 -0
- package/dist/memory/humanLikeMemory.js +603 -0
- package/dist/memory/humanLikeMemory.js.map +1 -0
- package/dist/memory/index.d.ts +22 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +24 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/memoryEvolutionMigration.d.ts +36 -0
- package/dist/memory/memoryEvolutionMigration.d.ts.map +1 -0
- package/dist/memory/memoryEvolutionMigration.js +371 -0
- package/dist/memory/memoryEvolutionMigration.js.map +1 -0
- package/dist/memory/quadrantSearch.d.ts +221 -0
- package/dist/memory/quadrantSearch.d.ts.map +1 -0
- package/dist/memory/quadrantSearch.js +897 -0
- package/dist/memory/quadrantSearch.js.map +1 -0
- package/dist/middleware/apiVersioning.d.ts +83 -0
- package/dist/middleware/apiVersioning.d.ts.map +1 -0
- package/dist/middleware/apiVersioning.js +152 -0
- package/dist/middleware/apiVersioning.js.map +1 -0
- package/dist/middleware/compression.d.ts +48 -0
- package/dist/middleware/compression.d.ts.map +1 -0
- package/dist/middleware/compression.js +240 -0
- package/dist/middleware/compression.js.map +1 -0
- package/dist/middleware/csrf.d.ts +118 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf.js +300 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/index.d.ts +13 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +17 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/wsRateLimiter.d.ts +129 -0
- package/dist/middleware/wsRateLimiter.d.ts.map +1 -0
- package/dist/middleware/wsRateLimiter.js +279 -0
- package/dist/middleware/wsRateLimiter.js.map +1 -0
- package/dist/migrations/run.d.ts +2 -0
- package/dist/migrations/run.d.ts.map +1 -0
- package/dist/migrations/run.js +25 -0
- package/dist/migrations/run.js.map +1 -0
- package/dist/migrations/syncDimensions.d.ts +24 -0
- package/dist/migrations/syncDimensions.d.ts.map +1 -0
- package/dist/migrations/syncDimensions.js +140 -0
- package/dist/migrations/syncDimensions.js.map +1 -0
- package/dist/migrations/teamComms.d.ts +16 -0
- package/dist/migrations/teamComms.d.ts.map +1 -0
- package/dist/migrations/teamComms.js +210 -0
- package/dist/migrations/teamComms.js.map +1 -0
- package/dist/openapi/index.d.ts +10 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +10 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/spec.d.ts +902 -0
- package/dist/openapi/spec.d.ts.map +1 -0
- package/dist/openapi/spec.js +733 -0
- package/dist/openapi/spec.js.map +1 -0
- package/dist/packages/dependencyHistory.d.ts +113 -0
- package/dist/packages/dependencyHistory.d.ts.map +1 -0
- package/dist/packages/dependencyHistory.js +360 -0
- package/dist/packages/dependencyHistory.js.map +1 -0
- package/dist/packages/index.d.ts +30 -0
- package/dist/packages/index.d.ts.map +1 -0
- package/dist/packages/index.js +65 -0
- package/dist/packages/index.js.map +1 -0
- package/dist/packages/packageTools.d.ts +255 -0
- package/dist/packages/packageTools.d.ts.map +1 -0
- package/dist/packages/packageTools.js +242 -0
- package/dist/packages/packageTools.js.map +1 -0
- package/dist/packages/packageTracker.d.ts +98 -0
- package/dist/packages/packageTracker.d.ts.map +1 -0
- package/dist/packages/packageTracker.js +268 -0
- package/dist/packages/packageTracker.js.map +1 -0
- package/dist/packages/packageWatcher.d.ts +62 -0
- package/dist/packages/packageWatcher.d.ts.map +1 -0
- package/dist/packages/packageWatcher.js +146 -0
- package/dist/packages/packageWatcher.js.map +1 -0
- package/dist/providers/MiniCOTProvider.d.ts +48 -0
- package/dist/providers/MiniCOTProvider.d.ts.map +1 -0
- package/dist/providers/MiniCOTProvider.js +98 -0
- package/dist/providers/MiniCOTProvider.js.map +1 -0
- package/dist/reminders/index.d.ts +5 -0
- package/dist/reminders/index.d.ts.map +1 -0
- package/dist/reminders/index.js +5 -0
- package/dist/reminders/index.js.map +1 -0
- package/dist/reminders/skillReminder.d.ts +131 -0
- package/dist/reminders/skillReminder.d.ts.map +1 -0
- package/dist/reminders/skillReminder.js +386 -0
- package/dist/reminders/skillReminder.js.map +1 -0
- package/dist/search.d.ts +35 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +574 -0
- package/dist/search.js.map +1 -0
- package/dist/security/localhostOnly.d.ts +36 -0
- package/dist/security/localhostOnly.d.ts.map +1 -0
- package/dist/security/localhostOnly.js +101 -0
- package/dist/security/localhostOnly.js.map +1 -0
- package/dist/services/CameraZoomSearch.d.ts +206 -0
- package/dist/services/CameraZoomSearch.d.ts.map +1 -0
- package/dist/services/CameraZoomSearch.js +669 -0
- package/dist/services/CameraZoomSearch.js.map +1 -0
- package/dist/services/DataFlowPipeline.d.ts +111 -0
- package/dist/services/DataFlowPipeline.d.ts.map +1 -0
- package/dist/services/DataFlowPipeline.js +379 -0
- package/dist/services/DataFlowPipeline.js.map +1 -0
- package/dist/services/DimensionAdapter.d.ts +194 -0
- package/dist/services/DimensionAdapter.d.ts.map +1 -0
- package/dist/services/DimensionAdapter.js +566 -0
- package/dist/services/DimensionAdapter.js.map +1 -0
- package/dist/services/DimensionService.d.ts +252 -0
- package/dist/services/DimensionService.d.ts.map +1 -0
- package/dist/services/DimensionService.js +564 -0
- package/dist/services/DimensionService.js.map +1 -0
- package/dist/services/EmbeddingQueue.d.ts +71 -0
- package/dist/services/EmbeddingQueue.d.ts.map +1 -0
- package/dist/services/EmbeddingQueue.js +258 -0
- package/dist/services/EmbeddingQueue.js.map +1 -0
- package/dist/services/MemoryDrilldown.d.ts +226 -0
- package/dist/services/MemoryDrilldown.d.ts.map +1 -0
- package/dist/services/MemoryDrilldown.js +479 -0
- package/dist/services/MemoryDrilldown.js.map +1 -0
- package/dist/services/MiniCOTScorer.d.ts +140 -0
- package/dist/services/MiniCOTScorer.d.ts.map +1 -0
- package/dist/services/MiniCOTScorer.js +292 -0
- package/dist/services/MiniCOTScorer.js.map +1 -0
- package/dist/services/ProjectContext.d.ts +342 -0
- package/dist/services/ProjectContext.d.ts.map +1 -0
- package/dist/services/ProjectContext.js +667 -0
- package/dist/services/ProjectContext.js.map +1 -0
- package/dist/services/ResponseCompactor.d.ts +135 -0
- package/dist/services/ResponseCompactor.d.ts.map +1 -0
- package/dist/services/ResponseCompactor.js +501 -0
- package/dist/services/ResponseCompactor.js.map +1 -0
- package/dist/services/TeamCommsDbService.d.ts +202 -0
- package/dist/services/TeamCommsDbService.d.ts.map +1 -0
- package/dist/services/TeamCommsDbService.js +526 -0
- package/dist/services/TeamCommsDbService.js.map +1 -0
- package/dist/services/UnifiedPasswordService.d.ts +166 -0
- package/dist/services/UnifiedPasswordService.d.ts.map +1 -0
- package/dist/services/UnifiedPasswordService.js +587 -0
- package/dist/services/UnifiedPasswordService.js.map +1 -0
- package/dist/services/adaptiveSearchConfig.d.ts +64 -0
- package/dist/services/adaptiveSearchConfig.d.ts.map +1 -0
- package/dist/services/adaptiveSearchConfig.js +187 -0
- package/dist/services/adaptiveSearchConfig.js.map +1 -0
- package/dist/skills/index.d.ts +8 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +8 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skillScanner.d.ts +203 -0
- package/dist/skills/skillScanner.d.ts.map +1 -0
- package/dist/skills/skillScanner.js +559 -0
- package/dist/skills/skillScanner.js.map +1 -0
- package/dist/skills/skillsResource.d.ts +69 -0
- package/dist/skills/skillsResource.d.ts.map +1 -0
- package/dist/skills/skillsResource.js +257 -0
- package/dist/skills/skillsResource.js.map +1 -0
- package/dist/startup/index.d.ts +9 -0
- package/dist/startup/index.d.ts.map +1 -0
- package/dist/startup/index.js +12 -0
- package/dist/startup/index.js.map +1 -0
- package/dist/startup/startupIndexing.d.ts +80 -0
- package/dist/startup/startupIndexing.d.ts.map +1 -0
- package/dist/startup/startupIndexing.js +463 -0
- package/dist/startup/startupIndexing.js.map +1 -0
- package/dist/startup/validation.d.ts +89 -0
- package/dist/startup/validation.d.ts.map +1 -0
- package/dist/startup/validation.js +590 -0
- package/dist/startup/validation.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/overflowManager.d.ts +80 -0
- package/dist/storage/overflowManager.d.ts.map +1 -0
- package/dist/storage/overflowManager.js +317 -0
- package/dist/storage/overflowManager.js.map +1 -0
- package/dist/storage/overflowStorage.d.ts +69 -0
- package/dist/storage/overflowStorage.d.ts.map +1 -0
- package/dist/storage/overflowStorage.js +379 -0
- package/dist/storage/overflowStorage.js.map +1 -0
- package/dist/storage/toonFormat.d.ts +50 -0
- package/dist/storage/toonFormat.d.ts.map +1 -0
- package/dist/storage/toonFormat.js +224 -0
- package/dist/storage/toonFormat.js.map +1 -0
- package/dist/team-members/communication.d.ts +237 -0
- package/dist/team-members/communication.d.ts.map +1 -0
- package/dist/team-members/communication.js +650 -0
- package/dist/team-members/communication.js.map +1 -0
- package/dist/team-members/index.d.ts +14 -0
- package/dist/team-members/index.d.ts.map +1 -0
- package/dist/team-members/index.js +22 -0
- package/dist/team-members/index.js.map +1 -0
- package/dist/team-members/taskOrchestrator.d.ts +224 -0
- package/dist/team-members/taskOrchestrator.d.ts.map +1 -0
- package/dist/team-members/taskOrchestrator.js +574 -0
- package/dist/team-members/taskOrchestrator.js.map +1 -0
- package/dist/team-members/taskTeamMemberLogger.d.ts +157 -0
- package/dist/team-members/taskTeamMemberLogger.d.ts.map +1 -0
- package/dist/team-members/taskTeamMemberLogger.js +478 -0
- package/dist/team-members/taskTeamMemberLogger.js.map +1 -0
- package/dist/team-members/teamCommsService.d.ts +221 -0
- package/dist/team-members/teamCommsService.d.ts.map +1 -0
- package/dist/team-members/teamCommsService.js +628 -0
- package/dist/team-members/teamCommsService.js.map +1 -0
- package/dist/team-members/teamMemberChannels.d.ts +217 -0
- package/dist/team-members/teamMemberChannels.d.ts.map +1 -0
- package/dist/team-members/teamMemberChannels.js +687 -0
- package/dist/team-members/teamMemberChannels.js.map +1 -0
- package/dist/team-members/teamMemberDashboard.d.ts +222 -0
- package/dist/team-members/teamMemberDashboard.d.ts.map +1 -0
- package/dist/team-members/teamMemberDashboard.js +610 -0
- package/dist/team-members/teamMemberDashboard.js.map +1 -0
- package/dist/team-members/teamMemberDeployment.d.ts +60 -0
- package/dist/team-members/teamMemberDeployment.d.ts.map +1 -0
- package/dist/team-members/teamMemberDeployment.js +429 -0
- package/dist/team-members/teamMemberDeployment.js.map +1 -0
- package/dist/team-members/teamMemberDiscovery.d.ts +178 -0
- package/dist/team-members/teamMemberDiscovery.d.ts.map +1 -0
- package/dist/team-members/teamMemberDiscovery.js +446 -0
- package/dist/team-members/teamMemberDiscovery.js.map +1 -0
- package/dist/team-members/teamMemberHistory.d.ts +80 -0
- package/dist/team-members/teamMemberHistory.d.ts.map +1 -0
- package/dist/team-members/teamMemberHistory.js +426 -0
- package/dist/team-members/teamMemberHistory.js.map +1 -0
- package/dist/team-members/teamMemberLimits.d.ts +66 -0
- package/dist/team-members/teamMemberLimits.d.ts.map +1 -0
- package/dist/team-members/teamMemberLimits.js +259 -0
- package/dist/team-members/teamMemberLimits.js.map +1 -0
- package/dist/team-members/teamMemberRegistry.d.ts +199 -0
- package/dist/team-members/teamMemberRegistry.d.ts.map +1 -0
- package/dist/team-members/teamMemberRegistry.js +572 -0
- package/dist/team-members/teamMemberRegistry.js.map +1 -0
- package/dist/team-members/teamMemberTracker.d.ts +148 -0
- package/dist/team-members/teamMemberTracker.d.ts.map +1 -0
- package/dist/team-members/teamMemberTracker.js +828 -0
- package/dist/team-members/teamMemberTracker.js.map +1 -0
- package/dist/team-members/workers/aiWorker.d.ts +53 -0
- package/dist/team-members/workers/aiWorker.d.ts.map +1 -0
- package/dist/team-members/workers/aiWorker.js +322 -0
- package/dist/team-members/workers/aiWorker.js.map +1 -0
- package/dist/team-members/workers/baseWorker.d.ts +101 -0
- package/dist/team-members/workers/baseWorker.d.ts.map +1 -0
- package/dist/team-members/workers/baseWorker.js +179 -0
- package/dist/team-members/workers/baseWorker.js.map +1 -0
- package/dist/team-members/workers/codeReviewWorker.d.ts +3 -0
- package/dist/team-members/workers/codeReviewWorker.d.ts.map +1 -0
- package/dist/team-members/workers/codeReviewWorker.js +144 -0
- package/dist/team-members/workers/codeReviewWorker.js.map +1 -0
- package/dist/team-members/workers/index.d.ts +7 -0
- package/dist/team-members/workers/index.d.ts.map +1 -0
- package/dist/team-members/workers/index.js +7 -0
- package/dist/team-members/workers/index.js.map +1 -0
- package/dist/team-members/workers/repairWorker.d.ts +9 -0
- package/dist/team-members/workers/repairWorker.d.ts.map +1 -0
- package/dist/team-members/workers/repairWorker.js +102 -0
- package/dist/team-members/workers/repairWorker.js.map +1 -0
- package/dist/team-members/workers/sendToTeamMemberB.d.ts +9 -0
- package/dist/team-members/workers/sendToTeamMemberB.d.ts.map +1 -0
- package/dist/team-members/workers/sendToTeamMemberB.js +105 -0
- package/dist/team-members/workers/sendToTeamMemberB.js.map +1 -0
- package/dist/team-members/workers/specmemClient.d.ts +179 -0
- package/dist/team-members/workers/specmemClient.d.ts.map +1 -0
- package/dist/team-members/workers/specmemClient.js +421 -0
- package/dist/team-members/workers/specmemClient.js.map +1 -0
- package/dist/team-members/workers/testCommunication.d.ts +8 -0
- package/dist/team-members/workers/testCommunication.d.ts.map +1 -0
- package/dist/team-members/workers/testCommunication.js +136 -0
- package/dist/team-members/workers/testCommunication.js.map +1 -0
- package/dist/team-members/workers/testCommunicationSuite.d.ts +26 -0
- package/dist/team-members/workers/testCommunicationSuite.d.ts.map +1 -0
- package/dist/team-members/workers/testCommunicationSuite.js +415 -0
- package/dist/team-members/workers/testCommunicationSuite.js.map +1 -0
- package/dist/team-members/workers/testWorker.d.ts +9 -0
- package/dist/team-members/workers/testWorker.d.ts.map +1 -0
- package/dist/team-members/workers/testWorker.js +107 -0
- package/dist/team-members/workers/testWorker.js.map +1 -0
- package/dist/tools/agentDefinitions.d.ts +30 -0
- package/dist/tools/agentDefinitions.d.ts.map +1 -0
- package/dist/tools/agentDefinitions.js +166 -0
- package/dist/tools/agentDefinitions.js.map +1 -0
- package/dist/tools/goofy/checkSyncStatus.d.ts +68 -0
- package/dist/tools/goofy/checkSyncStatus.d.ts.map +1 -0
- package/dist/tools/goofy/checkSyncStatus.js +112 -0
- package/dist/tools/goofy/checkSyncStatus.js.map +1 -0
- package/dist/tools/goofy/codeMemoryLink.d.ts +82 -0
- package/dist/tools/goofy/codeMemoryLink.d.ts.map +1 -0
- package/dist/tools/goofy/codeMemoryLink.js +212 -0
- package/dist/tools/goofy/codeMemoryLink.js.map +1 -0
- package/dist/tools/goofy/compareInstanceMemory.d.ts +97 -0
- package/dist/tools/goofy/compareInstanceMemory.d.ts.map +1 -0
- package/dist/tools/goofy/compareInstanceMemory.js +218 -0
- package/dist/tools/goofy/compareInstanceMemory.js.map +1 -0
- package/dist/tools/goofy/createReasoningChain.d.ts +135 -0
- package/dist/tools/goofy/createReasoningChain.d.ts.map +1 -0
- package/dist/tools/goofy/createReasoningChain.js +257 -0
- package/dist/tools/goofy/createReasoningChain.js.map +1 -0
- package/dist/tools/goofy/deployTeamMember.d.ts +63 -0
- package/dist/tools/goofy/deployTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/deployTeamMember.js +103 -0
- package/dist/tools/goofy/deployTeamMember.js.map +1 -0
- package/dist/tools/goofy/drillDown.d.ts +143 -0
- package/dist/tools/goofy/drillDown.d.ts.map +1 -0
- package/dist/tools/goofy/drillDown.js +288 -0
- package/dist/tools/goofy/drillDown.js.map +1 -0
- package/dist/tools/goofy/extractClaudeSessions.d.ts +90 -0
- package/dist/tools/goofy/extractClaudeSessions.d.ts.map +1 -0
- package/dist/tools/goofy/extractClaudeSessions.js +277 -0
- package/dist/tools/goofy/extractClaudeSessions.js.map +1 -0
- package/dist/tools/goofy/extractContextRestorations.d.ts +70 -0
- package/dist/tools/goofy/extractContextRestorations.d.ts.map +1 -0
- package/dist/tools/goofy/extractContextRestorations.js +100 -0
- package/dist/tools/goofy/extractContextRestorations.js.map +1 -0
- package/dist/tools/goofy/findCodePointers.d.ts +364 -0
- package/dist/tools/goofy/findCodePointers.d.ts.map +1 -0
- package/dist/tools/goofy/findCodePointers.js +1764 -0
- package/dist/tools/goofy/findCodePointers.js.map +1 -0
- package/dist/tools/goofy/findMemoryGallery.d.ts +40 -0
- package/dist/tools/goofy/findMemoryGallery.d.ts.map +1 -0
- package/dist/tools/goofy/findMemoryGallery.js +66 -0
- package/dist/tools/goofy/findMemoryGallery.js.map +1 -0
- package/dist/tools/goofy/findWhatISaid.d.ts +300 -0
- package/dist/tools/goofy/findWhatISaid.d.ts.map +1 -0
- package/dist/tools/goofy/findWhatISaid.js +2547 -0
- package/dist/tools/goofy/findWhatISaid.js.map +1 -0
- package/dist/tools/goofy/forceResync.d.ts +57 -0
- package/dist/tools/goofy/forceResync.d.ts.map +1 -0
- package/dist/tools/goofy/forceResync.js +100 -0
- package/dist/tools/goofy/forceResync.js.map +1 -0
- package/dist/tools/goofy/getActiveTeamMembers.d.ts +48 -0
- package/dist/tools/goofy/getActiveTeamMembers.d.ts.map +1 -0
- package/dist/tools/goofy/getActiveTeamMembers.js +136 -0
- package/dist/tools/goofy/getActiveTeamMembers.js.map +1 -0
- package/dist/tools/goofy/getMemoryFull.d.ts +34 -0
- package/dist/tools/goofy/getMemoryFull.d.ts.map +1 -0
- package/dist/tools/goofy/getMemoryFull.js +58 -0
- package/dist/tools/goofy/getMemoryFull.js.map +1 -0
- package/dist/tools/goofy/getSessionWatcherStatus.d.ts +43 -0
- package/dist/tools/goofy/getSessionWatcherStatus.d.ts.map +1 -0
- package/dist/tools/goofy/getSessionWatcherStatus.js +92 -0
- package/dist/tools/goofy/getSessionWatcherStatus.js.map +1 -0
- package/dist/tools/goofy/getTeamMemberOutput.d.ts +35 -0
- package/dist/tools/goofy/getTeamMemberOutput.d.ts.map +1 -0
- package/dist/tools/goofy/getTeamMemberOutput.js +62 -0
- package/dist/tools/goofy/getTeamMemberOutput.js.map +1 -0
- package/dist/tools/goofy/getTeamMemberScreen.d.ts +28 -0
- package/dist/tools/goofy/getTeamMemberScreen.d.ts.map +1 -0
- package/dist/tools/goofy/getTeamMemberScreen.js +59 -0
- package/dist/tools/goofy/getTeamMemberScreen.js.map +1 -0
- package/dist/tools/goofy/getTeamMemberStatus.d.ts +33 -0
- package/dist/tools/goofy/getTeamMemberStatus.d.ts.map +1 -0
- package/dist/tools/goofy/getTeamMemberStatus.js +56 -0
- package/dist/tools/goofy/getTeamMemberStatus.js.map +1 -0
- package/dist/tools/goofy/index.d.ts +39 -0
- package/dist/tools/goofy/index.d.ts.map +1 -0
- package/dist/tools/goofy/index.js +51 -0
- package/dist/tools/goofy/index.js.map +1 -0
- package/dist/tools/goofy/interveneTeamMember.d.ts +33 -0
- package/dist/tools/goofy/interveneTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/interveneTeamMember.js +69 -0
- package/dist/tools/goofy/interveneTeamMember.js.map +1 -0
- package/dist/tools/goofy/killDeployedTeamMember.d.ts +29 -0
- package/dist/tools/goofy/killDeployedTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/killDeployedTeamMember.js +56 -0
- package/dist/tools/goofy/killDeployedTeamMember.js.map +1 -0
- package/dist/tools/goofy/linkTheVibes.d.ts +125 -0
- package/dist/tools/goofy/linkTheVibes.d.ts.map +1 -0
- package/dist/tools/goofy/linkTheVibes.js +354 -0
- package/dist/tools/goofy/linkTheVibes.js.map +1 -0
- package/dist/tools/goofy/listDeployedTeamMembers.d.ts +26 -0
- package/dist/tools/goofy/listDeployedTeamMembers.d.ts.map +1 -0
- package/dist/tools/goofy/listDeployedTeamMembers.js +52 -0
- package/dist/tools/goofy/listDeployedTeamMembers.js.map +1 -0
- package/dist/tools/goofy/listenForMessages.d.ts +56 -0
- package/dist/tools/goofy/listenForMessages.d.ts.map +1 -0
- package/dist/tools/goofy/listenForMessages.js +122 -0
- package/dist/tools/goofy/listenForMessages.js.map +1 -0
- package/dist/tools/goofy/memoryHealthCheck.d.ts +159 -0
- package/dist/tools/goofy/memoryHealthCheck.d.ts.map +1 -0
- package/dist/tools/goofy/memoryHealthCheck.js +443 -0
- package/dist/tools/goofy/memoryHealthCheck.js.map +1 -0
- package/dist/tools/goofy/rememberThisShit.d.ts +103 -0
- package/dist/tools/goofy/rememberThisShit.d.ts.map +1 -0
- package/dist/tools/goofy/rememberThisShit.js +291 -0
- package/dist/tools/goofy/rememberThisShit.js.map +1 -0
- package/dist/tools/goofy/sayToTeamMember.d.ts +55 -0
- package/dist/tools/goofy/sayToTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/sayToTeamMember.js +116 -0
- package/dist/tools/goofy/sayToTeamMember.js.map +1 -0
- package/dist/tools/goofy/selfMessage.d.ts +54 -0
- package/dist/tools/goofy/selfMessage.d.ts.map +1 -0
- package/dist/tools/goofy/selfMessage.js +111 -0
- package/dist/tools/goofy/selfMessage.js.map +1 -0
- package/dist/tools/goofy/sendHeartbeat.d.ts +53 -0
- package/dist/tools/goofy/sendHeartbeat.d.ts.map +1 -0
- package/dist/tools/goofy/sendHeartbeat.js +119 -0
- package/dist/tools/goofy/sendHeartbeat.js.map +1 -0
- package/dist/tools/goofy/showMeTheStats.d.ts +216 -0
- package/dist/tools/goofy/showMeTheStats.d.ts.map +1 -0
- package/dist/tools/goofy/showMeTheStats.js +535 -0
- package/dist/tools/goofy/showMeTheStats.js.map +1 -0
- package/dist/tools/goofy/smartRecall.d.ts +136 -0
- package/dist/tools/goofy/smartRecall.d.ts.map +1 -0
- package/dist/tools/goofy/smartRecall.js +286 -0
- package/dist/tools/goofy/smartRecall.js.map +1 -0
- package/dist/tools/goofy/smartSearch.d.ts +64 -0
- package/dist/tools/goofy/smartSearch.d.ts.map +1 -0
- package/dist/tools/goofy/smartSearch.js +89 -0
- package/dist/tools/goofy/smartSearch.js.map +1 -0
- package/dist/tools/goofy/smushMemoriesTogether.d.ts +128 -0
- package/dist/tools/goofy/smushMemoriesTogether.d.ts.map +1 -0
- package/dist/tools/goofy/smushMemoriesTogether.js +536 -0
- package/dist/tools/goofy/smushMemoriesTogether.js.map +1 -0
- package/dist/tools/goofy/spatialSearch.d.ts +198 -0
- package/dist/tools/goofy/spatialSearch.d.ts.map +1 -0
- package/dist/tools/goofy/spatialSearch.js +551 -0
- package/dist/tools/goofy/spatialSearch.js.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMember.d.ts +104 -0
- package/dist/tools/goofy/spawnResearchTeamMember.d.ts.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMember.js +290 -0
- package/dist/tools/goofy/spawnResearchTeamMember.js.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.d.ts +121 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.d.ts.map +1 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.js +215 -0
- package/dist/tools/goofy/spawnResearchTeamMemberTool.js.map +1 -0
- package/dist/tools/goofy/startWatchingTheFiles.d.ts +81 -0
- package/dist/tools/goofy/startWatchingTheFiles.d.ts.map +1 -0
- package/dist/tools/goofy/startWatchingTheFiles.js +161 -0
- package/dist/tools/goofy/startWatchingTheFiles.js.map +1 -0
- package/dist/tools/goofy/stopWatchingTheFiles.d.ts +50 -0
- package/dist/tools/goofy/stopWatchingTheFiles.d.ts.map +1 -0
- package/dist/tools/goofy/stopWatchingTheFiles.js +81 -0
- package/dist/tools/goofy/stopWatchingTheFiles.js.map +1 -0
- package/dist/tools/goofy/whatDidIMean.d.ts +113 -0
- package/dist/tools/goofy/whatDidIMean.d.ts.map +1 -0
- package/dist/tools/goofy/whatDidIMean.js +401 -0
- package/dist/tools/goofy/whatDidIMean.js.map +1 -0
- package/dist/tools/goofy/yeahNahDeleteThat.d.ts +109 -0
- package/dist/tools/goofy/yeahNahDeleteThat.d.ts.map +1 -0
- package/dist/tools/goofy/yeahNahDeleteThat.js +319 -0
- package/dist/tools/goofy/yeahNahDeleteThat.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/teamMemberDeployer.d.ts +117 -0
- package/dist/tools/teamMemberDeployer.d.ts.map +1 -0
- package/dist/tools/teamMemberDeployer.js +613 -0
- package/dist/tools/teamMemberDeployer.js.map +1 -0
- package/dist/trace/index.d.ts +14 -0
- package/dist/trace/index.d.ts.map +1 -0
- package/dist/trace/index.js +16 -0
- package/dist/trace/index.js.map +1 -0
- package/dist/trace/tools/analyzeImpact.d.ts +90 -0
- package/dist/trace/tools/analyzeImpact.d.ts.map +1 -0
- package/dist/trace/tools/analyzeImpact.js +240 -0
- package/dist/trace/tools/analyzeImpact.js.map +1 -0
- package/dist/trace/tools/exploreDependencies.d.ts +81 -0
- package/dist/trace/tools/exploreDependencies.d.ts.map +1 -0
- package/dist/trace/tools/exploreDependencies.js +161 -0
- package/dist/trace/tools/exploreDependencies.js.map +1 -0
- package/dist/trace/tools/findSimilarBugs.d.ts +112 -0
- package/dist/trace/tools/findSimilarBugs.d.ts.map +1 -0
- package/dist/trace/tools/findSimilarBugs.js +216 -0
- package/dist/trace/tools/findSimilarBugs.js.map +1 -0
- package/dist/trace/tools/index.d.ts +22 -0
- package/dist/trace/tools/index.d.ts.map +1 -0
- package/dist/trace/tools/index.js +39 -0
- package/dist/trace/tools/index.js.map +1 -0
- package/dist/trace/tools/smartExplore.d.ts +126 -0
- package/dist/trace/tools/smartExplore.d.ts.map +1 -0
- package/dist/trace/tools/smartExplore.js +303 -0
- package/dist/trace/tools/smartExplore.js.map +1 -0
- package/dist/trace/tools/traceError.d.ts +101 -0
- package/dist/trace/tools/traceError.d.ts.map +1 -0
- package/dist/trace/tools/traceError.js +175 -0
- package/dist/trace/tools/traceError.js.map +1 -0
- package/dist/trace/traceExploreSystem.d.ts +271 -0
- package/dist/trace/traceExploreSystem.d.ts.map +1 -0
- package/dist/trace/traceExploreSystem.js +789 -0
- package/dist/trace/traceExploreSystem.js.map +1 -0
- package/dist/types/index.d.ts +421 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +118 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/circuitBreaker.d.ts +195 -0
- package/dist/utils/circuitBreaker.d.ts.map +1 -0
- package/dist/utils/circuitBreaker.js +374 -0
- package/dist/utils/circuitBreaker.js.map +1 -0
- package/dist/utils/cleanupHandler.d.ts +108 -0
- package/dist/utils/cleanupHandler.d.ts.map +1 -0
- package/dist/utils/cleanupHandler.js +203 -0
- package/dist/utils/cleanupHandler.js.map +1 -0
- package/dist/utils/compactXmlResponse.d.ts +60 -0
- package/dist/utils/compactXmlResponse.d.ts.map +1 -0
- package/dist/utils/compactXmlResponse.js +209 -0
- package/dist/utils/compactXmlResponse.js.map +1 -0
- package/dist/utils/cotBroadcast.d.ts +56 -0
- package/dist/utils/cotBroadcast.d.ts.map +1 -0
- package/dist/utils/cotBroadcast.js +157 -0
- package/dist/utils/cotBroadcast.js.map +1 -0
- package/dist/utils/debugLogger.d.ts +95 -0
- package/dist/utils/debugLogger.d.ts.map +1 -0
- package/dist/utils/debugLogger.js +610 -0
- package/dist/utils/debugLogger.js.map +1 -0
- package/dist/utils/fileProcessingQueue.d.ts +259 -0
- package/dist/utils/fileProcessingQueue.d.ts.map +1 -0
- package/dist/utils/fileProcessingQueue.js +714 -0
- package/dist/utils/fileProcessingQueue.js.map +1 -0
- package/dist/utils/humanReadableOutput.d.ts +124 -0
- package/dist/utils/humanReadableOutput.d.ts.map +1 -0
- package/dist/utils/humanReadableOutput.js +340 -0
- package/dist/utils/humanReadableOutput.js.map +1 -0
- package/dist/utils/index.d.ts +32 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +71 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/instanceManager.d.ts +530 -0
- package/dist/utils/instanceManager.d.ts.map +1 -0
- package/dist/utils/instanceManager.js +1784 -0
- package/dist/utils/instanceManager.js.map +1 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mapCleanup.d.ts +58 -0
- package/dist/utils/mapCleanup.d.ts.map +1 -0
- package/dist/utils/mapCleanup.js +150 -0
- package/dist/utils/mapCleanup.js.map +1 -0
- package/dist/utils/memoryManager.d.ts +349 -0
- package/dist/utils/memoryManager.d.ts.map +1 -0
- package/dist/utils/memoryManager.js +799 -0
- package/dist/utils/memoryManager.js.map +1 -0
- package/dist/utils/metrics.d.ts +160 -0
- package/dist/utils/metrics.d.ts.map +1 -0
- package/dist/utils/metrics.js +558 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/pathValidator.d.ts +96 -0
- package/dist/utils/pathValidator.d.ts.map +1 -0
- package/dist/utils/pathValidator.js +320 -0
- package/dist/utils/pathValidator.js.map +1 -0
- package/dist/utils/portAllocator.d.ts +296 -0
- package/dist/utils/portAllocator.d.ts.map +1 -0
- package/dist/utils/portAllocator.js +768 -0
- package/dist/utils/portAllocator.js.map +1 -0
- package/dist/utils/portUtils.d.ts +97 -0
- package/dist/utils/portUtils.d.ts.map +1 -0
- package/dist/utils/portUtils.js +285 -0
- package/dist/utils/portUtils.js.map +1 -0
- package/dist/utils/postgresAutoSetup.d.ts +55 -0
- package/dist/utils/postgresAutoSetup.d.ts.map +1 -0
- package/dist/utils/postgresAutoSetup.js +406 -0
- package/dist/utils/postgresAutoSetup.js.map +1 -0
- package/dist/utils/processHealthCheck.d.ts +61 -0
- package/dist/utils/processHealthCheck.d.ts.map +1 -0
- package/dist/utils/processHealthCheck.js +313 -0
- package/dist/utils/processHealthCheck.js.map +1 -0
- package/dist/utils/progressReporter.d.ts +151 -0
- package/dist/utils/progressReporter.d.ts.map +1 -0
- package/dist/utils/progressReporter.js +345 -0
- package/dist/utils/progressReporter.js.map +1 -0
- package/dist/utils/projectEnv.d.ts +73 -0
- package/dist/utils/projectEnv.d.ts.map +1 -0
- package/dist/utils/projectEnv.js +137 -0
- package/dist/utils/projectEnv.js.map +1 -0
- package/dist/utils/qoms.d.ts +122 -0
- package/dist/utils/qoms.d.ts.map +1 -0
- package/dist/utils/qoms.js +650 -0
- package/dist/utils/qoms.js.map +1 -0
- package/dist/utils/retryHelper.d.ts +122 -0
- package/dist/utils/retryHelper.d.ts.map +1 -0
- package/dist/utils/retryHelper.js +272 -0
- package/dist/utils/retryHelper.js.map +1 -0
- package/dist/utils/safeProcessTermination.d.ts +206 -0
- package/dist/utils/safeProcessTermination.d.ts.map +1 -0
- package/dist/utils/safeProcessTermination.js +552 -0
- package/dist/utils/safeProcessTermination.js.map +1 -0
- package/dist/utils/sessionInjector.d.ts +68 -0
- package/dist/utils/sessionInjector.d.ts.map +1 -0
- package/dist/utils/sessionInjector.js +189 -0
- package/dist/utils/sessionInjector.js.map +1 -0
- package/dist/utils/statsCache.d.ts +134 -0
- package/dist/utils/statsCache.d.ts.map +1 -0
- package/dist/utils/statsCache.js +285 -0
- package/dist/utils/statsCache.js.map +1 -0
- package/dist/utils/timeoutMiddleware.d.ts +81 -0
- package/dist/utils/timeoutMiddleware.d.ts.map +1 -0
- package/dist/utils/timeoutMiddleware.js +155 -0
- package/dist/utils/timeoutMiddleware.js.map +1 -0
- package/dist/utils/timerRegistry.d.ts +91 -0
- package/dist/utils/timerRegistry.d.ts.map +1 -0
- package/dist/utils/timerRegistry.js +187 -0
- package/dist/utils/timerRegistry.js.map +1 -0
- package/dist/utils/tokenCompressor.d.ts +332 -0
- package/dist/utils/tokenCompressor.d.ts.map +1 -0
- package/dist/utils/tokenCompressor.js +1306 -0
- package/dist/utils/tokenCompressor.js.map +1 -0
- package/dist/utils/tracing.d.ts +236 -0
- package/dist/utils/tracing.d.ts.map +1 -0
- package/dist/utils/tracing.js +378 -0
- package/dist/utils/tracing.js.map +1 -0
- package/dist/watcher/changeHandler.d.ts +123 -0
- package/dist/watcher/changeHandler.d.ts.map +1 -0
- package/dist/watcher/changeHandler.js +623 -0
- package/dist/watcher/changeHandler.js.map +1 -0
- package/dist/watcher/changeQueue.d.ts +133 -0
- package/dist/watcher/changeQueue.d.ts.map +1 -0
- package/dist/watcher/changeQueue.js +355 -0
- package/dist/watcher/changeQueue.js.map +1 -0
- package/dist/watcher/fileWatcher.d.ts +121 -0
- package/dist/watcher/fileWatcher.d.ts.map +1 -0
- package/dist/watcher/fileWatcher.js +531 -0
- package/dist/watcher/fileWatcher.js.map +1 -0
- package/dist/watcher/index.d.ts +94 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +235 -0
- package/dist/watcher/index.js.map +1 -0
- package/dist/watcher/syncChecker.d.ts +93 -0
- package/dist/watcher/syncChecker.d.ts.map +1 -0
- package/dist/watcher/syncChecker.js +401 -0
- package/dist/watcher/syncChecker.js.map +1 -0
- package/dist/watcher/tsCompiler.d.ts +88 -0
- package/dist/watcher/tsCompiler.d.ts.map +1 -0
- package/dist/watcher/tsCompiler.js +212 -0
- package/dist/watcher/tsCompiler.js.map +1 -0
- package/embedding-sandbox/Dockerfile +77 -0
- package/embedding-sandbox/Dockerfile.frankenstein +91 -0
- package/embedding-sandbox/README.md +193 -0
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-312.pyc +0 -0
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-313.pyc +0 -0
- package/embedding-sandbox/__pycache__/qqms_v2.cpython-312.pyc +0 -0
- package/embedding-sandbox/__pycache__/qqms_v2.cpython-313.pyc +0 -0
- package/embedding-sandbox/add_js_docs.py +684 -0
- package/embedding-sandbox/build_docs_db.py +239 -0
- package/embedding-sandbox/client.cjs +376 -0
- package/embedding-sandbox/client.ts +913 -0
- package/embedding-sandbox/deploy-frankenstein.sh +240 -0
- package/embedding-sandbox/docker-compose.yml +60 -0
- package/embedding-sandbox/docker-manager.py +325 -0
- package/embedding-sandbox/docs/python_docs.db +0 -0
- package/embedding-sandbox/download-model.mjs +79 -0
- package/embedding-sandbox/download-model.py +28 -0
- package/embedding-sandbox/embedding-supervisor.sh +164 -0
- package/embedding-sandbox/frankenstein-embeddings.py +3940 -0
- package/embedding-sandbox/manage-services.sh +354 -0
- package/embedding-sandbox/overflow_queue.py +345 -0
- package/embedding-sandbox/package.json +17 -0
- package/embedding-sandbox/project_isolation.py +292 -0
- package/embedding-sandbox/qqms_v2.py +967 -0
- package/embedding-sandbox/ram-manager.sh +311 -0
- package/embedding-sandbox/requirements-frankenstein.txt +7 -0
- package/embedding-sandbox/run_js_docs.py +59 -0
- package/embedding-sandbox/seed_docs.py +885 -0
- package/embedding-sandbox/server-batch.mjs +228 -0
- package/embedding-sandbox/server.mjs +389 -0
- package/embedding-sandbox/specmem/sockets/claude-input-state.json +1 -0
- package/embedding-sandbox/specmem/sockets/embedding-death-reason.txt +3 -0
- package/embedding-sandbox/specmem/sockets/seen-sessions.json +1 -0
- package/embedding-sandbox/specmem/sockets/session-start.lock +1 -0
- package/embedding-sandbox/specmem/sockets/session-stops.log +7 -0
- package/embedding-sandbox/start-frankenstein-throttled.sh +98 -0
- package/embedding-sandbox/start-on-demand.sh +116 -0
- package/embedding-sandbox/start-sandbox.sh +237 -0
- package/embedding-sandbox/start-supervised.sh +11 -0
- package/embedding-sandbox/stop-sandbox.sh +51 -0
- package/embedding-sandbox/test-socket.mjs +61 -0
- package/embedding-sandbox/warm-start.sh +353 -0
- package/embedding-sandbox/warm_start_feeder.py +660 -0
- package/legal/README.md +31 -0
- package/legal/anthropic-privacy-center-screenshot-2026-01-30.png +0 -0
- package/legal/anthropic-tos-screenshot-2026-01-30.png +0 -0
- package/lib/codebase-bridge.cjs +308 -0
- package/package.json +136 -0
- package/plugins/specmem-agents/agents/bug-hunter.md +79 -0
- package/plugins/specmem-agents/agents/memory-explorer.md +57 -0
- package/plugins/specmem-agents/agents/team-coordinator.md +82 -0
- package/scripts/auto-updater.cjs +399 -0
- package/scripts/backfill-code-definition-embeddings.ts +440 -0
- package/scripts/backfill-code-embeddings.ts +206 -0
- package/scripts/capture-tos-screenshots.cjs +94 -0
- package/scripts/check-global-install.cjs +67 -0
- package/scripts/cleanup-embedding-servers.sh +25 -0
- package/scripts/dashboard-standalone.sh +369 -0
- package/scripts/deploy-hooks.cjs +1451 -0
- package/scripts/deploy.sh +106 -0
- package/scripts/docker-project-down.sh +83 -0
- package/scripts/docker-project-list.sh +40 -0
- package/scripts/docker-project-up.sh +79 -0
- package/scripts/fast-backfill-embeddings.ts +173 -0
- package/scripts/fast-batch-embedder.cjs +334 -0
- package/scripts/first-run-model-setup.cjs +849 -0
- package/scripts/global-postinstall.cjs +1957 -0
- package/scripts/index-codebase.js +72 -0
- package/scripts/migrate-fix-embeddings.py +110 -0
- package/scripts/migrate-to-project-schemas.ts +525 -0
- package/scripts/optimize-embedding-model.py +324 -0
- package/scripts/optimize-instructions.cjs +530 -0
- package/scripts/pack-docker-images.sh +68 -0
- package/scripts/pack-for-testing.sh +130 -0
- package/scripts/postinstall.cjs +54 -0
- package/scripts/project-env.sh +51 -0
- package/scripts/reset-db.sh +30 -0
- package/scripts/run-indexer.ts +69 -0
- package/scripts/run-migrations.js +47 -0
- package/scripts/setup-db.sh +34 -0
- package/scripts/setup-minimal-schema.sql +143 -0
- package/scripts/skills/code-review.md +44 -0
- package/scripts/skills/debugging.md +56 -0
- package/scripts/skills/specmem-deployteam.md +239 -0
- package/scripts/skills/teammemberskills/EFFICIENT_GREP.md +171 -0
- package/scripts/skills/teammemberskills/task-planning.md +67 -0
- package/scripts/specmem/sockets/session-start.lock +1 -0
- package/scripts/specmem/sockets/session-stops.log +1 -0
- package/scripts/specmem-health.sh +382 -0
- package/scripts/specmem-init.cjs +6935 -0
- package/scripts/strip-debug-logs.cjs +43 -0
- package/scripts/test-mcp-standalone.sh +365 -0
- package/scripts/test-optimized-models.py +166 -0
- package/scripts/verify-embedding-fix.sh +148 -0
- package/skills/code-review.md +44 -0
- package/skills/debugging.md +56 -0
- package/skills/specmem-deployteam.md +239 -0
- package/skills/teammemberskills/EFFICIENT_GREP.md +171 -0
- package/skills/teammemberskills/task-planning.md +67 -0
- package/specmem-health.cjs +522 -0
- package/specmem.env +216 -0
|
@@ -0,0 +1,4297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* webServer.ts - CSGO-Themed SpecMem Web Dashboard
|
|
3
|
+
*
|
|
4
|
+
* A badass web dashboard with CS:GO vibes for managing the SpecMem MCP Server.
|
|
5
|
+
* Yellow (#FFD700) and Black (#000000) color scheme with modal-based UI.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Login system with password protection
|
|
9
|
+
* - Memory management (view/search/delete)
|
|
10
|
+
* - Session management ( sessions)
|
|
11
|
+
* - Codebase browser
|
|
12
|
+
* - Skills manager
|
|
13
|
+
* - Team member coordination viewer
|
|
14
|
+
* - Statistics dashboard
|
|
15
|
+
* - Configuration panel
|
|
16
|
+
*
|
|
17
|
+
* @author hardwicksoftwareservices
|
|
18
|
+
*/
|
|
19
|
+
// @ts-ignore - express types not installed
|
|
20
|
+
import express from 'express';
|
|
21
|
+
// @ts-ignore - express-session types not installed
|
|
22
|
+
import session from 'express-session';
|
|
23
|
+
import rateLimit from 'express-rate-limit';
|
|
24
|
+
import { createServer } from 'http';
|
|
25
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
import crypto from 'crypto';
|
|
29
|
+
import * as fs from 'fs/promises';
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { logger } from '../utils/logger.js';
|
|
32
|
+
import { isPortAvailable, sleep } from '../utils/portUtils.js';
|
|
33
|
+
import { getDatabase } from '../database.js';
|
|
34
|
+
import { createSessionStore } from './sessionStore.js';
|
|
35
|
+
import { requestTimeout, setServerTimeouts } from '../utils/timeoutMiddleware.js';
|
|
36
|
+
import { getSkillScanner } from '../skills/skillScanner.js';
|
|
37
|
+
import { getCodebaseIndexer } from '../codebase/codebaseIndexer.js';
|
|
38
|
+
import { getMemoryManager } from '../utils/memoryManager.js';
|
|
39
|
+
import { getTeamMemberTracker } from '../team-members/teamMemberTracker.js';
|
|
40
|
+
import { getTeamMemberDeployment } from '../team-members/teamMemberDeployment.js';
|
|
41
|
+
import { getTeamMemberHistoryManager } from '../team-members/teamMemberHistory.js';
|
|
42
|
+
import { createTeamMemberDiscovery } from '../team-members/teamMemberDiscovery.js';
|
|
43
|
+
import { createTeamMemberCommunicator } from '../team-members/communication.js';
|
|
44
|
+
import { createMemoryRecallRouter } from './api/memoryRecall.js';
|
|
45
|
+
import { createTeamMemberHistoryRouter } from './api/teamMemberHistory.js';
|
|
46
|
+
import { createTeamMemberDeployRouter } from './api/teamMemberDeploy.js';
|
|
47
|
+
import { initializeTeamMemberStream, shutdownTeamMemberStream } from './websocket/teamMemberStream.js';
|
|
48
|
+
// Phase 4-6 imports for Direct Prompting, Terminal Streaming, and Control
|
|
49
|
+
import { createPromptSendRouter } from './api/promptSend.js';
|
|
50
|
+
import { createTerminalRouter } from './api/terminal.js';
|
|
51
|
+
import { createControlRouter } from './api/claudeControl.js';
|
|
52
|
+
import { createSpecmemToolsRouter } from './api/specmemTools.js';
|
|
53
|
+
import { createTerminalInjectRouter } from './api/terminalInject.js';
|
|
54
|
+
import { createTerminalStreamRouter, handleTerminalWebSocket } from './api/terminalStream.js';
|
|
55
|
+
// Live Session Streaming - Team Member 2's LIVE Code session viewer!
|
|
56
|
+
import { createLiveSessionRouter } from './api/liveSessionStream.js';
|
|
57
|
+
// Task team member logging
|
|
58
|
+
import { createTaskTeamMembersRouter } from './api/taskTeamMembers.js';
|
|
59
|
+
import { initializeTaskTeamMemberLogger } from '../team-members/taskTeamMemberLogger.js';
|
|
60
|
+
// File Manager - FTP-style file browsing for codebase management
|
|
61
|
+
import { createFileManagerRouter } from './api/fileManager.js';
|
|
62
|
+
// Settings API - Password management and dashboard configuration
|
|
63
|
+
import { createSettingsRouter } from './api/settings.js';
|
|
64
|
+
// Setup API - Dashboard mode switching and initial setup
|
|
65
|
+
import { createSetupRouter } from './api/setup.js';
|
|
66
|
+
// Data Export API - Export PostgreSQL tables to JSON
|
|
67
|
+
import { createDataExportRouter } from './api/dataExport.js';
|
|
68
|
+
// Hot Reload API - Dashboard control for hot reload system
|
|
69
|
+
import { createHotReloadRouter } from './api/hotReload.js';
|
|
70
|
+
// Camera Roll Search - zoom-based memory exploration
|
|
71
|
+
import { ZOOM_CONFIGS, formatAsCameraRollItem, formatAsCameraRollResponse } from '../services/CameraZoomSearch.js';
|
|
72
|
+
// Hooks Management API - User-manageable custom hooks
|
|
73
|
+
import { hooksRouter } from './api/hooks.js';
|
|
74
|
+
// Centralized password management
|
|
75
|
+
import { getPassword, checkPassword, isUsingDefaultPassword, changePasswordWithTeamMemberNotification } from '../config/password.js';
|
|
76
|
+
// Port allocation for unique per-instance ports
|
|
77
|
+
import { getInstancePortsSync, getDashboardPort, getCoordinationPort, getPortAllocationSummary, PORT_CONFIG } from '../utils/portAllocator.js';
|
|
78
|
+
// Project path for database isolation
|
|
79
|
+
import { getProjectPathForInsert } from '../services/ProjectContext.js';
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Zod Validation Schemas for Dashboard API
|
|
82
|
+
// ============================================================================
|
|
83
|
+
const MemoriesQuerySchema = z.object({
|
|
84
|
+
search: z.string().max(1000).optional(),
|
|
85
|
+
limit: z.coerce.number().int().min(1).max(500).default(50),
|
|
86
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
87
|
+
});
|
|
88
|
+
const BulkDeleteMemoriesSchema = z.object({
|
|
89
|
+
ids: z.array(z.string().uuid()).optional(),
|
|
90
|
+
olderThan: z.string().datetime().optional(),
|
|
91
|
+
tags: z.array(z.string()).optional(),
|
|
92
|
+
expiredOnly: z.boolean().optional()
|
|
93
|
+
}).refine(data => data.ids || data.olderThan || data.tags || data.expiredOnly, { message: 'At least one deletion criterion required (ids, olderThan, tags, or expiredOnly)' });
|
|
94
|
+
// Get __dirname equivalent for ES modules
|
|
95
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
96
|
+
const __dirname = path.dirname(__filename);
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Default Configuration
|
|
99
|
+
// ============================================================================
|
|
100
|
+
/**
|
|
101
|
+
* Get or create a persistent session secret
|
|
102
|
+
* - First checks SPECMEM_SESSION_SECRET env var
|
|
103
|
+
* - Then checks SPECMEM_SESSION_SECRET_FILE for file-based secret
|
|
104
|
+
* - Falls back to generating one (logged as warning since it won't persist)
|
|
105
|
+
*/
|
|
106
|
+
function getSessionSecret() {
|
|
107
|
+
// Option 1: Environment variable
|
|
108
|
+
const envSecret = process.env['SPECMEM_SESSION_SECRET'];
|
|
109
|
+
if (envSecret && envSecret.length >= 32) {
|
|
110
|
+
logger.debug('using session secret from SPECMEM_SESSION_SECRET env var');
|
|
111
|
+
return envSecret;
|
|
112
|
+
}
|
|
113
|
+
// Option 2: File-based secret
|
|
114
|
+
const secretFile = process.env['SPECMEM_SESSION_SECRET_FILE'];
|
|
115
|
+
if (secretFile) {
|
|
116
|
+
try {
|
|
117
|
+
const fsSync = require('fs');
|
|
118
|
+
if (fsSync.existsSync(secretFile)) {
|
|
119
|
+
const fileSecret = fsSync.readFileSync(secretFile, 'utf-8').trim();
|
|
120
|
+
if (fileSecret.length >= 32) {
|
|
121
|
+
logger.debug({ secretFile }, 'using session secret from file');
|
|
122
|
+
return fileSecret;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
logger.warn({ secretFile, error: e }, 'couldnt read session secret file');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Option 3: Generate new (sessions won't persist across restarts)
|
|
131
|
+
logger.warn('generating random session secret - sessions will not persist across restarts bruh');
|
|
132
|
+
logger.warn('set SPECMEM_SESSION_SECRET or SPECMEM_SESSION_SECRET_FILE env var for persistent sessions');
|
|
133
|
+
return crypto.randomBytes(32).toString('hex');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get dashboard mode from environment
|
|
137
|
+
* - 'private' (default): Localhost only, more secure
|
|
138
|
+
* - 'public': Network accessible, requires strong password
|
|
139
|
+
*/
|
|
140
|
+
function getDashboardMode() {
|
|
141
|
+
const mode = process.env['SPECMEM_DASHBOARD_MODE'];
|
|
142
|
+
if (mode === 'public')
|
|
143
|
+
return 'public';
|
|
144
|
+
return 'private';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get dashboard host based on mode
|
|
148
|
+
* Private mode: Always 127.0.0.1 (localhost only)
|
|
149
|
+
* Public mode: Use SPECMEM_DASHBOARD_HOST or 0.0.0.0 (all interfaces)
|
|
150
|
+
*/
|
|
151
|
+
function getDashboardHost() {
|
|
152
|
+
const mode = getDashboardMode();
|
|
153
|
+
if (mode === 'private') {
|
|
154
|
+
return '127.0.0.1';
|
|
155
|
+
}
|
|
156
|
+
return process.env['SPECMEM_DASHBOARD_HOST'] || '0.0.0.0';
|
|
157
|
+
}
|
|
158
|
+
const DEFAULT_CONFIG = {
|
|
159
|
+
// Use dynamic port from portAllocator (project-hash derived)
|
|
160
|
+
port: parseInt(process.env['SPECMEM_DASHBOARD_PORT'] || '', 10) || getDashboardPort(),
|
|
161
|
+
host: getDashboardHost(),
|
|
162
|
+
mode: getDashboardMode(),
|
|
163
|
+
sessionSecret: getSessionSecret(),
|
|
164
|
+
password: '', // Must be set via SPECMEM_DASHBOARD_PASSWORD env var
|
|
165
|
+
// Use dynamic coordination port from portAllocator (project-hash derived)
|
|
166
|
+
coordinationPort: parseInt(process.env['SPECMEM_COORDINATION_PORT'] || '', 10) || getCoordinationPort(),
|
|
167
|
+
maxPortAttempts: 10,
|
|
168
|
+
maxStartupRetries: 3,
|
|
169
|
+
retryDelayMs: 1000
|
|
170
|
+
};
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// Dashboard Web Server
|
|
173
|
+
// ============================================================================
|
|
174
|
+
export class DashboardWebServer {
|
|
175
|
+
config;
|
|
176
|
+
app;
|
|
177
|
+
server;
|
|
178
|
+
wss;
|
|
179
|
+
isRunning = false;
|
|
180
|
+
startTime = 0;
|
|
181
|
+
actualPort = 0; // The port we actually bound to
|
|
182
|
+
db = null;
|
|
183
|
+
sessionStore = null;
|
|
184
|
+
skillScanner = null;
|
|
185
|
+
codebaseIndexer = null;
|
|
186
|
+
embeddingProvider = null;
|
|
187
|
+
memoryManager = null;
|
|
188
|
+
embeddingOverflowHandler = null;
|
|
189
|
+
connectedClients = new Set();
|
|
190
|
+
envFilePath = null;
|
|
191
|
+
teamMemberTracker = null;
|
|
192
|
+
teamMemberDeployment = null;
|
|
193
|
+
teamMemberHistoryManager = null;
|
|
194
|
+
terminalStreamManager = null;
|
|
195
|
+
teamMemberStreamManager = null;
|
|
196
|
+
dashboardCommunicator = null;
|
|
197
|
+
dashboardDiscovery = null; // TeamMemberDiscovery instance for querying team members
|
|
198
|
+
constructor(config) {
|
|
199
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
200
|
+
this.app = express();
|
|
201
|
+
this.server = createServer(this.app);
|
|
202
|
+
// CRITICAL: Set HTTP server keepalive to prevent connection drops
|
|
203
|
+
this.server.keepAliveTimeout = 120000; // 120 seconds
|
|
204
|
+
this.server.headersTimeout = 125000; // Must be > keepAliveTimeout
|
|
205
|
+
// CRITICAL FIX: Use noServer mode to prevent conflicts with other WebSocket handlers
|
|
206
|
+
// When multiple WebSocketServers are attached to the same HTTP server, they ALL
|
|
207
|
+
// receive the 'upgrade' event, causing the RSV1/1006 close bug where one WSS
|
|
208
|
+
// sends a 400 error after another has already completed the upgrade.
|
|
209
|
+
this.wss = new WebSocketServer({
|
|
210
|
+
noServer: true,
|
|
211
|
+
perMessageDeflate: false, // Disable compression to prevent issues
|
|
212
|
+
clientTracking: true,
|
|
213
|
+
maxPayload: 100 * 1024 * 1024 // 100MB max message size
|
|
214
|
+
});
|
|
215
|
+
// Centralized upgrade handling - route to appropriate WebSocket server
|
|
216
|
+
this.server.on('upgrade', (request, socket, head) => {
|
|
217
|
+
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
|
218
|
+
const pathname = url.pathname;
|
|
219
|
+
// Skip paths handled by other WebSocket managers (TeamMemberStreamManager handles /ws/team-members/live)
|
|
220
|
+
if (pathname === '/ws/team-members/live') {
|
|
221
|
+
// Let TeamMemberStreamManager handle this
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
// Handle all other WebSocket paths
|
|
225
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
226
|
+
this.wss.emit('connection', ws, request);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
this.setupMiddleware();
|
|
230
|
+
this.setupRoutes();
|
|
231
|
+
this.setupWebSocket();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Setup Express middleware
|
|
235
|
+
*/
|
|
236
|
+
setupMiddleware() {
|
|
237
|
+
// Parse JSON bodies
|
|
238
|
+
this.app.use(express.json());
|
|
239
|
+
this.app.use(express.urlencoded({ extended: true }));
|
|
240
|
+
// Request timeout middleware - prevents long-running requests from hanging
|
|
241
|
+
// Configurable via SPECMEM_REQUEST_TIMEOUT env var (default: 30 seconds)
|
|
242
|
+
this.app.use(requestTimeout({
|
|
243
|
+
timeout: parseInt(process.env['SPECMEM_REQUEST_TIMEOUT'] || '30000', 10),
|
|
244
|
+
message: 'Request timeout - try again or reduce the scope of your request',
|
|
245
|
+
log: true
|
|
246
|
+
}));
|
|
247
|
+
// Rate limiting for API endpoints
|
|
248
|
+
const apiLimiter = rateLimit({
|
|
249
|
+
windowMs: 1 * 60 * 1000, // 1 minute window (faster reset)
|
|
250
|
+
max: 1000, // 1000 requests per minute (teamMembers need this!)
|
|
251
|
+
message: { error: 'Too many requests, please try again later' },
|
|
252
|
+
standardHeaders: true,
|
|
253
|
+
legacyHeaders: false,
|
|
254
|
+
skip: (req) => {
|
|
255
|
+
// Skip rate limiting for localhost (teamMembers running on same machine)
|
|
256
|
+
const ip = req.ip || req.socket.remoteAddress || '';
|
|
257
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// Much more relaxed auth limiting for team member communication
|
|
261
|
+
const authLimiter = rateLimit({
|
|
262
|
+
windowMs: 1 * 60 * 1000, // 1 minute window
|
|
263
|
+
max: 500, // 500 logins per minute (teamMembers retry a lot!)
|
|
264
|
+
message: { error: 'Too many login attempts, please try again later' },
|
|
265
|
+
standardHeaders: true,
|
|
266
|
+
legacyHeaders: false,
|
|
267
|
+
skip: (req) => {
|
|
268
|
+
// Skip rate limiting for localhost
|
|
269
|
+
const ip = req.ip || req.socket.remoteAddress || '';
|
|
270
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// Apply rate limiting to API routes (but localhost is exempt!)
|
|
274
|
+
this.app.use('/api/', apiLimiter);
|
|
275
|
+
this.app.use('/api/login', authLimiter);
|
|
276
|
+
// Session management
|
|
277
|
+
// Cookie secure flag configurable via SPECMEM_COOKIE_SECURE env var (default: false)
|
|
278
|
+
const cookieSecure = process.env.SPECMEM_COOKIE_SECURE === 'true';
|
|
279
|
+
// Session options - store will be set in start() if database is available
|
|
280
|
+
const sessionOptions = {
|
|
281
|
+
secret: this.config.sessionSecret,
|
|
282
|
+
resave: false,
|
|
283
|
+
saveUninitialized: false,
|
|
284
|
+
cookie: {
|
|
285
|
+
secure: cookieSecure,
|
|
286
|
+
httpOnly: true,
|
|
287
|
+
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
// Note: We configure the session store in start() after database is available
|
|
291
|
+
// For now, use the default memory store (will be replaced in start())
|
|
292
|
+
this.app.use(session(sessionOptions));
|
|
293
|
+
// Serve static files - React app takes priority
|
|
294
|
+
this.app.use(express.static(path.join(__dirname, 'public', 'react-dist')));
|
|
295
|
+
this.app.use(express.static(path.join(__dirname, 'public')));
|
|
296
|
+
// CORS headers for API
|
|
297
|
+
// Private mode: restricted to localhost origins only
|
|
298
|
+
// Public mode: allows configured origins or same-origin requests
|
|
299
|
+
// Use dynamic ports for per-project isolation
|
|
300
|
+
const dynamicDashboardPort = this.config.port;
|
|
301
|
+
const dynamicCoordinationPort = this.config.coordinationPort;
|
|
302
|
+
const allowedOrigins = [
|
|
303
|
+
`http://localhost:${dynamicDashboardPort}`,
|
|
304
|
+
`http://127.0.0.1:${dynamicDashboardPort}`,
|
|
305
|
+
`http://localhost:${dynamicCoordinationPort}`,
|
|
306
|
+
`http://127.0.0.1:${dynamicCoordinationPort}`
|
|
307
|
+
];
|
|
308
|
+
// In public mode, add the actual host binding to allowed origins
|
|
309
|
+
if (this.config.mode === 'public') {
|
|
310
|
+
// Allow requests from any host the server is bound to
|
|
311
|
+
allowedOrigins.push(`http://${this.config.host}:${this.config.port}`);
|
|
312
|
+
// Also allow requests from the local machine's actual IP/hostname
|
|
313
|
+
// The origin will be validated against what the browser sends
|
|
314
|
+
}
|
|
315
|
+
this.app.use((req, res, next) => {
|
|
316
|
+
const origin = req.headers.origin;
|
|
317
|
+
if (this.config.mode === 'public') {
|
|
318
|
+
// In public mode, use whitelist for CORS instead of reflecting any origin
|
|
319
|
+
// This prevents CSRF attacks while still allowing legitimate cross-origin requests
|
|
320
|
+
const publicModeWhitelist = [
|
|
321
|
+
...allowedOrigins,
|
|
322
|
+
`http://${this.config.host}:${this.config.port}`,
|
|
323
|
+
// Allow common local development origins
|
|
324
|
+
'http://localhost:3000',
|
|
325
|
+
'http://localhost:5173',
|
|
326
|
+
'http://127.0.0.1:3000',
|
|
327
|
+
'http://127.0.0.1:5173'
|
|
328
|
+
];
|
|
329
|
+
if (origin && publicModeWhitelist.includes(origin)) {
|
|
330
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
331
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// Private mode: strict origin checking
|
|
336
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
337
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
341
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
342
|
+
if (req.method === 'OPTIONS') {
|
|
343
|
+
res.sendStatus(204);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
next();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Authentication middleware
|
|
351
|
+
*/
|
|
352
|
+
requireAuth(req, res, next) {
|
|
353
|
+
const session = req.session;
|
|
354
|
+
if (session?.authenticated) {
|
|
355
|
+
next();
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Setup Express routes
|
|
363
|
+
*/
|
|
364
|
+
setupRoutes() {
|
|
365
|
+
// ==================== PUBLIC ROUTES ====================
|
|
366
|
+
// Serve React app index.html
|
|
367
|
+
this.app.get('/', (req, res) => {
|
|
368
|
+
res.sendFile(path.join(__dirname, 'public', 'react-dist', 'index.html'));
|
|
369
|
+
});
|
|
370
|
+
// Health check
|
|
371
|
+
this.app.get('/health', (req, res) => {
|
|
372
|
+
res.json({
|
|
373
|
+
status: 'healthy',
|
|
374
|
+
uptime: this.isRunning ? Date.now() - this.startTime : 0,
|
|
375
|
+
service: 'specmem-dashboard'
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
// Login endpoint - uses centralized password management
|
|
379
|
+
this.app.post('/api/login', (req, res) => {
|
|
380
|
+
const { password } = req.body;
|
|
381
|
+
// Use centralized password check (supports runtime updates)
|
|
382
|
+
if (checkPassword(password)) {
|
|
383
|
+
const sess = req.session;
|
|
384
|
+
sess.authenticated = true;
|
|
385
|
+
sess.loginTime = Date.now();
|
|
386
|
+
logger.info('Dashboard login successful');
|
|
387
|
+
// Warn if using default password
|
|
388
|
+
if (isUsingDefaultPassword()) {
|
|
389
|
+
logger.warn('Login with DEFAULT password - consider changing for security');
|
|
390
|
+
}
|
|
391
|
+
res.json({ success: true, message: 'Login successful' });
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
logger.warn('Dashboard login failed - incorrect password');
|
|
395
|
+
res.status(401).json({ error: 'Invalid password' });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
// Logout endpoint
|
|
399
|
+
this.app.post('/api/logout', (req, res) => {
|
|
400
|
+
req.session.destroy((err) => {
|
|
401
|
+
if (err) {
|
|
402
|
+
logger.error({ err }, 'Error destroying session');
|
|
403
|
+
res.status(500).json({ error: 'Logout failed' });
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
res.json({ success: true, message: 'Logged out' });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
// Check auth status
|
|
411
|
+
this.app.get('/api/auth/status', (req, res) => {
|
|
412
|
+
const sess = req.session;
|
|
413
|
+
res.json({
|
|
414
|
+
authenticated: !!sess?.authenticated,
|
|
415
|
+
loginTime: sess?.loginTime || null
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
// Client-side logging endpoint - logs browser errors/messages to server
|
|
419
|
+
// This allows dashboard pages to send errors to the server for proper logging
|
|
420
|
+
this.app.post('/api/log', (req, res) => {
|
|
421
|
+
const { level, message, page, data } = req.body;
|
|
422
|
+
// Validate level
|
|
423
|
+
const validLevels = ['info', 'warn', 'error', 'debug'];
|
|
424
|
+
const logLevel = validLevels.includes(level) ? level : 'info';
|
|
425
|
+
// Construct log context
|
|
426
|
+
const logContext = {
|
|
427
|
+
source: 'dashboard-client',
|
|
428
|
+
page: page || 'unknown',
|
|
429
|
+
userAgent: req.headers['user-agent'],
|
|
430
|
+
...(data && typeof data === 'object' ? data : { extra: data })
|
|
431
|
+
};
|
|
432
|
+
// Log using the appropriate level
|
|
433
|
+
switch (logLevel) {
|
|
434
|
+
case 'error':
|
|
435
|
+
logger.error(logContext, `[Dashboard] ${message}`);
|
|
436
|
+
break;
|
|
437
|
+
case 'warn':
|
|
438
|
+
logger.warn(logContext, `[Dashboard] ${message}`);
|
|
439
|
+
break;
|
|
440
|
+
case 'debug':
|
|
441
|
+
logger.debug(logContext, `[Dashboard] ${message}`);
|
|
442
|
+
break;
|
|
443
|
+
default:
|
|
444
|
+
logger.info(logContext, `[Dashboard] ${message}`);
|
|
445
|
+
}
|
|
446
|
+
res.json({ success: true });
|
|
447
|
+
});
|
|
448
|
+
// ==================== PROTECTED ROUTES ====================
|
|
449
|
+
// Port allocation status endpoint - shows allocated ports for this instance
|
|
450
|
+
this.app.get('/api/ports', this.requireAuth.bind(this), async (req, res) => {
|
|
451
|
+
try {
|
|
452
|
+
const allocatedPorts = getInstancePortsSync();
|
|
453
|
+
if (allocatedPorts) {
|
|
454
|
+
const summary = getPortAllocationSummary(allocatedPorts);
|
|
455
|
+
res.json({
|
|
456
|
+
success: true,
|
|
457
|
+
allocated: true,
|
|
458
|
+
ports: {
|
|
459
|
+
dashboard: summary.dashboard,
|
|
460
|
+
coordination: summary.coordination
|
|
461
|
+
},
|
|
462
|
+
projectPath: summary.projectPath,
|
|
463
|
+
verified: summary.verified,
|
|
464
|
+
config: {
|
|
465
|
+
minPort: PORT_CONFIG.MIN_PORT,
|
|
466
|
+
maxPort: PORT_CONFIG.MAX_PORT,
|
|
467
|
+
defaults: PORT_CONFIG.DEFAULTS
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// Fallback to current configuration
|
|
473
|
+
res.json({
|
|
474
|
+
success: true,
|
|
475
|
+
allocated: false,
|
|
476
|
+
ports: {
|
|
477
|
+
dashboard: {
|
|
478
|
+
port: getDashboardPort(),
|
|
479
|
+
url: `http://localhost:${getDashboardPort()}`
|
|
480
|
+
},
|
|
481
|
+
coordination: {
|
|
482
|
+
port: getCoordinationPort(),
|
|
483
|
+
wsUrl: `ws://localhost:${getCoordinationPort()}/teamMembers`
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
projectPath: process.cwd(),
|
|
487
|
+
config: {
|
|
488
|
+
minPort: PORT_CONFIG.MIN_PORT,
|
|
489
|
+
maxPort: PORT_CONFIG.MAX_PORT,
|
|
490
|
+
defaults: PORT_CONFIG.DEFAULTS
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
logger.error({ error }, 'Error fetching port allocation');
|
|
497
|
+
res.status(500).json({ error: 'Port info not available' });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
// Database metrics endpoint (#31)
|
|
501
|
+
this.app.get('/api/metrics/database', this.requireAuth.bind(this), async (req, res) => {
|
|
502
|
+
try {
|
|
503
|
+
if (!this.db) {
|
|
504
|
+
res.status(503).json({ error: 'Database not connected bruh' });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const metrics = await this.db.getDetailedMetrics();
|
|
508
|
+
res.json(metrics);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
logger.error({ error }, 'Error fetching database metrics');
|
|
512
|
+
res.status(500).json({ error: 'Database metrics said nah fr fr' });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
// Stats dashboard
|
|
516
|
+
this.app.get('/api/stats', this.requireAuth.bind(this), async (req, res) => {
|
|
517
|
+
try {
|
|
518
|
+
const stats = await this.getStats();
|
|
519
|
+
res.json(stats);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
logger.error({ error }, 'Error fetching stats');
|
|
523
|
+
res.status(500).json({ error: 'Stats ain\'t loading bruh' });
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// Memory management routes
|
|
527
|
+
this.app.get('/api/memories', this.requireAuth.bind(this), async (req, res) => {
|
|
528
|
+
try {
|
|
529
|
+
const parseResult = MemoriesQuerySchema.safeParse(req.query);
|
|
530
|
+
if (!parseResult.success) {
|
|
531
|
+
res.status(400).json({
|
|
532
|
+
error: 'Invalid query parameters',
|
|
533
|
+
details: parseResult.error.issues.map(i => ({ path: i.path.join('.'), message: i.message }))
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const { search, limit, offset } = parseResult.data;
|
|
538
|
+
const memories = await this.getMemories(search, limit, offset);
|
|
539
|
+
res.json(memories);
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
logger.error({ error }, 'Error fetching memories');
|
|
543
|
+
res.status(500).json({ error: 'Memories not loading lmao' });
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
// Search memories - MUST be before :id route to avoid matching "search" as an ID
|
|
547
|
+
// Supports camera roll mode for zoom-based exploration with drilldown IDs
|
|
548
|
+
this.app.get('/api/memories/search', this.requireAuth.bind(this), async (req, res) => {
|
|
549
|
+
try {
|
|
550
|
+
const q = req.query.q || req.query.query || '';
|
|
551
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
552
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
553
|
+
// Camera roll mode parameters
|
|
554
|
+
const cameraRollMode = req.query.cameraRollMode === 'true' || req.query.cameraRollMode === '1';
|
|
555
|
+
const zoomLevelParam = req.query.zoomLevel;
|
|
556
|
+
const validZoomLevels = ['ultra-wide', 'wide', 'normal', 'close', 'macro'];
|
|
557
|
+
const zoomLevel = validZoomLevels.includes(zoomLevelParam)
|
|
558
|
+
? zoomLevelParam
|
|
559
|
+
: 'normal';
|
|
560
|
+
// Standard search mode (backward compatible)
|
|
561
|
+
if (!cameraRollMode) {
|
|
562
|
+
const memories = await this.getMemories(q, limit, offset);
|
|
563
|
+
res.json({ success: true, query: q, memories: memories.memories, total: memories.total });
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// Camera roll mode - use zoom-based search with drilldown IDs
|
|
567
|
+
const zoomConfig = ZOOM_CONFIGS[zoomLevel];
|
|
568
|
+
const effectiveLimit = Math.min(limit, zoomConfig.limit);
|
|
569
|
+
// Perform the search with zoom-appropriate threshold
|
|
570
|
+
const searchResult = await this.getCameraRollMemories(q, zoomConfig.threshold, effectiveLimit, offset);
|
|
571
|
+
// Format results as CameraRollItems with drilldown IDs
|
|
572
|
+
const items = searchResult.memories.map((memory) => {
|
|
573
|
+
return formatAsCameraRollItem({
|
|
574
|
+
id: memory.id,
|
|
575
|
+
content: memory.content,
|
|
576
|
+
similarity: memory.similarity || 0.5,
|
|
577
|
+
metadata: memory.metadata,
|
|
578
|
+
tags: memory.tags,
|
|
579
|
+
createdAt: memory.created_at
|
|
580
|
+
}, zoomConfig, {
|
|
581
|
+
claudeResponse: memory.metadata?.claudeResponse,
|
|
582
|
+
relatedCount: memory.metadata?.relatedCount,
|
|
583
|
+
codePointers: memory.metadata?.codePointers
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
// Format as CameraRollResponse - now returns compact XML string
|
|
587
|
+
const xmlResponse = formatAsCameraRollResponse(items, q, zoomLevel, searchResult.searchType === 'hybrid' ? 'hybrid' : 'memory', searchResult.total);
|
|
588
|
+
// Extract drilldownIDs for easy access
|
|
589
|
+
const drilldownIDs = items.map(item => item.drilldownID);
|
|
590
|
+
// Return XML directly with metadata
|
|
591
|
+
res.json({
|
|
592
|
+
success: true,
|
|
593
|
+
cameraRollMode: true,
|
|
594
|
+
drilldownIDs,
|
|
595
|
+
response: xmlResponse
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
logger.error({ error }, 'Error searching memories');
|
|
600
|
+
res.status(500).json({ error: 'Memory search failed fr' });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
// Get single memory by ID
|
|
604
|
+
this.app.get('/api/memories/:id', this.requireAuth.bind(this), async (req, res) => {
|
|
605
|
+
try {
|
|
606
|
+
const { id } = req.params;
|
|
607
|
+
const memory = await this.getMemoryById(id);
|
|
608
|
+
if (!memory) {
|
|
609
|
+
res.status(404).json({ error: 'Memory not found' });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
res.json(memory);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
logger.error({ error }, 'Error fetching memory');
|
|
616
|
+
res.status(500).json({ error: 'Memory fetch broke fr' });
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
this.app.delete('/api/memories/:id', this.requireAuth.bind(this), async (req, res) => {
|
|
620
|
+
try {
|
|
621
|
+
const { id } = req.params;
|
|
622
|
+
await this.deleteMemory(id);
|
|
623
|
+
res.json({ success: true, message: 'Memory deleted' });
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
logger.error({ error }, 'Error deleting memory');
|
|
627
|
+
res.status(500).json({ error: 'Couldn\'t yeet that memory' });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// Bulk delete memories
|
|
631
|
+
this.app.post('/api/memories/bulk-delete', this.requireAuth.bind(this), async (req, res) => {
|
|
632
|
+
try {
|
|
633
|
+
const parseResult = BulkDeleteMemoriesSchema.safeParse(req.body);
|
|
634
|
+
if (!parseResult.success) {
|
|
635
|
+
res.status(400).json({
|
|
636
|
+
error: 'Invalid request body',
|
|
637
|
+
details: parseResult.error.issues.map(i => ({ path: i.path.join('.'), message: i.message }))
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const { ids, olderThan, tags, expiredOnly } = parseResult.data;
|
|
642
|
+
const result = await this.bulkDeleteMemories({ ids, olderThan, tags, expiredOnly });
|
|
643
|
+
res.json({ success: true, deleted: result.deleted, message: `${result.deleted} memories deleted` });
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
logger.error({ error }, 'Error bulk deleting memories');
|
|
647
|
+
res.status(500).json({ error: 'Bulk delete didn\'t work rip' });
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
// Session management routes
|
|
651
|
+
this.app.get('/api/sessions', this.requireAuth.bind(this), async (req, res) => {
|
|
652
|
+
try {
|
|
653
|
+
const sessions = await this.getSessions();
|
|
654
|
+
res.json(sessions);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
logger.error({ error }, 'Error fetching sessions');
|
|
658
|
+
res.status(500).json({ error: 'Sessions not loading yo' });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
// Get session details by ID
|
|
662
|
+
this.app.get('/api/sessions/:id', this.requireAuth.bind(this), async (req, res) => {
|
|
663
|
+
try {
|
|
664
|
+
const sessionId = req.params.id;
|
|
665
|
+
// Validate session ID format - must be alphanumeric with dashes/underscores (UUID-like or custom format)
|
|
666
|
+
// Prevents SQL injection and other attacks via malformed session IDs
|
|
667
|
+
const sessionIdRegex = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
668
|
+
if (!sessionIdRegex.test(sessionId)) {
|
|
669
|
+
res.status(400).json({ error: 'Invalid session ID format' });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (!this.db) {
|
|
673
|
+
res.status(503).json({ error: 'Database not connected' });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// Get session details from memories - use sessionId (camelCase) from metadata
|
|
677
|
+
const sessionQuery = `
|
|
678
|
+
WITH session_tags AS (
|
|
679
|
+
SELECT DISTINCT unnest(tags) as tag
|
|
680
|
+
FROM memories
|
|
681
|
+
WHERE COALESCE(metadata->>'sessionId', metadata->>'session_id') = $1
|
|
682
|
+
)
|
|
683
|
+
SELECT
|
|
684
|
+
COALESCE(metadata->>'sessionId', metadata->>'session_id') as session_id,
|
|
685
|
+
MIN(created_at) as started_at,
|
|
686
|
+
MAX(updated_at) as last_activity,
|
|
687
|
+
COUNT(*) as memory_count,
|
|
688
|
+
COUNT(DISTINCT memory_type) as memory_types_used,
|
|
689
|
+
array_agg(DISTINCT memory_type) as memory_types,
|
|
690
|
+
array_agg(DISTINCT importance) as importance_levels,
|
|
691
|
+
(SELECT array_agg(tag) FROM session_tags) as all_tags,
|
|
692
|
+
MAX(metadata->>'project') as project,
|
|
693
|
+
MAX(metadata->>'workingDirectory') as working_directory
|
|
694
|
+
FROM memories
|
|
695
|
+
WHERE COALESCE(metadata->>'sessionId', metadata->>'session_id') = $1
|
|
696
|
+
GROUP BY COALESCE(metadata->>'sessionId', metadata->>'session_id')
|
|
697
|
+
`;
|
|
698
|
+
const result = await this.db.query(sessionQuery, [sessionId]);
|
|
699
|
+
if (result.rows.length === 0) {
|
|
700
|
+
res.status(404).json({ error: 'Session not found' });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const session = result.rows[0];
|
|
704
|
+
// Get memories for this session - use sessionId (camelCase) from metadata
|
|
705
|
+
const memoriesQuery = `
|
|
706
|
+
SELECT id, content, memory_type, importance, tags, created_at, updated_at, metadata
|
|
707
|
+
FROM memories
|
|
708
|
+
WHERE COALESCE(metadata->>'sessionId', metadata->>'session_id') = $1
|
|
709
|
+
ORDER BY created_at DESC
|
|
710
|
+
LIMIT 100
|
|
711
|
+
`;
|
|
712
|
+
const memories = await this.db.query(memoriesQuery, [sessionId]);
|
|
713
|
+
res.json({
|
|
714
|
+
...session,
|
|
715
|
+
memories: memories.rows,
|
|
716
|
+
duration_minutes: session.last_activity && session.started_at
|
|
717
|
+
? (new Date(session.last_activity).getTime() - new Date(session.started_at).getTime()) / 60000
|
|
718
|
+
: 0
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
logger.error({ error }, 'Error fetching session details');
|
|
723
|
+
res.status(500).json({ error: 'Session details broke' });
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
// Get messages for a session
|
|
727
|
+
this.app.get('/api/sessions/:id/messages', this.requireAuth.bind(this), async (req, res) => {
|
|
728
|
+
try {
|
|
729
|
+
const sessionId = req.params.id;
|
|
730
|
+
// Validate session ID format - must be alphanumeric with dashes/underscores
|
|
731
|
+
const sessionIdRegex = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
732
|
+
if (!sessionIdRegex.test(sessionId)) {
|
|
733
|
+
res.status(400).json({ error: 'Invalid session ID format' });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (!this.db) {
|
|
737
|
+
res.status(503).json({ error: 'Database not connected' });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
// Get memories for this session (these are the "messages") - use sessionId (camelCase)
|
|
741
|
+
const query = `
|
|
742
|
+
SELECT
|
|
743
|
+
id,
|
|
744
|
+
content,
|
|
745
|
+
memory_type,
|
|
746
|
+
importance,
|
|
747
|
+
tags,
|
|
748
|
+
created_at,
|
|
749
|
+
updated_at,
|
|
750
|
+
metadata
|
|
751
|
+
FROM memories
|
|
752
|
+
WHERE COALESCE(metadata->>'sessionId', metadata->>'session_id') = $1
|
|
753
|
+
ORDER BY created_at ASC
|
|
754
|
+
`;
|
|
755
|
+
const result = await this.db.query(query, [sessionId]);
|
|
756
|
+
res.json(result.rows);
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
logger.error({ error }, 'Error fetching session messages');
|
|
760
|
+
res.status(500).json({ error: 'Session messages ain\'t showing' });
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
// Codebase browser routes
|
|
764
|
+
this.app.get('/api/codebase', this.requireAuth.bind(this), async (req, res) => {
|
|
765
|
+
try {
|
|
766
|
+
const { path: filePath, search } = req.query;
|
|
767
|
+
const files = await this.getCodebaseFiles(filePath, search);
|
|
768
|
+
res.json(files);
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
logger.error({ error }, 'Error fetching codebase');
|
|
772
|
+
res.status(500).json({ error: 'Codebase fetch broke lmao' });
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
// Get file content from codebase
|
|
776
|
+
this.app.get('/api/codebase/file', this.requireAuth.bind(this), async (req, res) => {
|
|
777
|
+
try {
|
|
778
|
+
const { path: filePath } = req.query;
|
|
779
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
780
|
+
res.status(400).json({ error: 'File path required' });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const fileContent = await this.getFileContent(filePath);
|
|
784
|
+
if (!fileContent) {
|
|
785
|
+
res.status(404).json({ error: 'File not found' });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
res.json(fileContent);
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
logger.error({ error }, 'Error fetching file content');
|
|
792
|
+
res.status(500).json({ error: 'File content not loading' });
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
// Skills manager routes
|
|
796
|
+
this.app.get('/api/skills', this.requireAuth.bind(this), async (req, res) => {
|
|
797
|
+
try {
|
|
798
|
+
const skills = await this.getSkills();
|
|
799
|
+
res.json(skills);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
logger.error({ error }, 'Error fetching skills');
|
|
803
|
+
res.status(500).json({ error: 'Skills ain\'t loading' });
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
this.app.post('/api/skills/reload', this.requireAuth.bind(this), async (req, res) => {
|
|
807
|
+
try {
|
|
808
|
+
await this.reloadSkills();
|
|
809
|
+
res.json({ success: true, message: 'Skills reloaded' });
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
logger.error({ error }, 'Error reloading skills');
|
|
813
|
+
res.status(500).json({ error: 'Skills reload broke' });
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
// Get individual skill content by name
|
|
817
|
+
this.app.get('/api/skills/:name', this.requireAuth.bind(this), async (req, res) => {
|
|
818
|
+
try {
|
|
819
|
+
if (!this.skillScanner) {
|
|
820
|
+
res.status(503).json({ error: 'Skills system not initialized' });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const skillName = decodeURIComponent(req.params.name);
|
|
824
|
+
const skills = this.skillScanner.getAllSkills();
|
|
825
|
+
const skill = skills.find(s => s.name === skillName || s.id === skillName);
|
|
826
|
+
if (!skill) {
|
|
827
|
+
res.status(404).json({ error: 'Skill not found' });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
res.json({
|
|
831
|
+
id: skill.id,
|
|
832
|
+
name: skill.name,
|
|
833
|
+
category: skill.category,
|
|
834
|
+
description: skill.description,
|
|
835
|
+
content: skill.content,
|
|
836
|
+
path: skill.filePath
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
logger.error({ error }, 'Error fetching skill content');
|
|
841
|
+
res.status(500).json({ error: 'Skill content not loading' });
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
// Update skill content
|
|
845
|
+
this.app.put('/api/skills/:name', this.requireAuth.bind(this), async (req, res) => {
|
|
846
|
+
try {
|
|
847
|
+
if (!this.skillScanner) {
|
|
848
|
+
res.status(503).json({ error: 'Skills system not initialized' });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const skillName = decodeURIComponent(req.params.name);
|
|
852
|
+
const { content } = req.body;
|
|
853
|
+
if (!content) {
|
|
854
|
+
res.status(400).json({ error: 'Content is required' });
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const skills = this.skillScanner.getAllSkills();
|
|
858
|
+
const skill = skills.find(s => s.name === skillName || s.id === skillName);
|
|
859
|
+
if (!skill) {
|
|
860
|
+
res.status(404).json({ error: 'Skill not found' });
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
// Write content to file
|
|
864
|
+
const fs = await import('fs/promises');
|
|
865
|
+
await fs.writeFile(skill.filePath, content, 'utf-8');
|
|
866
|
+
// Reload skills to reflect changes
|
|
867
|
+
await this.skillScanner.scan();
|
|
868
|
+
res.json({ success: true, message: 'Skill updated, no cap' });
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
logger.error({ error }, 'Error updating skill');
|
|
872
|
+
res.status(500).json({ error: 'Skill update didn\'t work' });
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
// Team member coordination routes
|
|
876
|
+
this.app.get('/api/teamMembers', this.requireAuth.bind(this), async (req, res) => {
|
|
877
|
+
try {
|
|
878
|
+
const teamMembers = await this.getTeamMembers();
|
|
879
|
+
res.json(teamMembers);
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
logger.error({ error }, 'Error fetching team members');
|
|
883
|
+
res.status(500).json({ error: 'Failed to fetch team members' });
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
// ==================== TEAM_MEMBER COMMUNICATION DASHBOARD ROUTES ====================
|
|
887
|
+
// GET /api/team-members/active - list all currently active team member sessions
|
|
888
|
+
this.app.get('/api/team-members/active', this.requireAuth.bind(this), async (req, res) => {
|
|
889
|
+
try {
|
|
890
|
+
const activeTeamMembers = await this.getActiveTeamMemberSessions();
|
|
891
|
+
res.json(activeTeamMembers);
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
logger.error({ error }, 'Error fetching active team members');
|
|
895
|
+
res.status(500).json({ error: 'Failed to fetch active team members' });
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
// GET /api/team-members/history - list past team member sessions with pagination
|
|
899
|
+
this.app.get('/api/team-members/history', this.requireAuth.bind(this), async (req, res) => {
|
|
900
|
+
try {
|
|
901
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
902
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
903
|
+
const teamMemberType = req.query.type;
|
|
904
|
+
const status = req.query.status;
|
|
905
|
+
const history = await this.getTeamMemberSessionHistory(limit, offset, teamMemberType, status);
|
|
906
|
+
res.json(history);
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
logger.error({ error }, 'Error fetching team member history');
|
|
910
|
+
res.status(500).json({ error: 'Failed to fetch team member history' });
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
// GET /api/team-members/session/:id - get detailed session info with messages
|
|
914
|
+
this.app.get('/api/team-members/session/:id', this.requireAuth.bind(this), async (req, res) => {
|
|
915
|
+
try {
|
|
916
|
+
const { id } = req.params;
|
|
917
|
+
const includeMessages = req.query.messages !== 'false';
|
|
918
|
+
const messageLimit = Math.min(parseInt(req.query.messageLimit) || 100, 500);
|
|
919
|
+
const session = await this.getTeamMemberSessionDetails(id, includeMessages, messageLimit);
|
|
920
|
+
if (!session) {
|
|
921
|
+
res.status(404).json({ error: 'Session not found' });
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
res.json(session);
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
logger.error({ error }, 'Error fetching session details');
|
|
928
|
+
res.status(500).json({ error: 'Session details broke' });
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
// GET /api/team-members/session/:id/messages - get messages for a session with pagination
|
|
932
|
+
this.app.get('/api/team-members/session/:id/messages', this.requireAuth.bind(this), async (req, res) => {
|
|
933
|
+
try {
|
|
934
|
+
const { id } = req.params;
|
|
935
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
936
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
937
|
+
const messageType = req.query.type;
|
|
938
|
+
const messages = await this.getTeamMemberSessionMessages(id, limit, offset, messageType);
|
|
939
|
+
res.json(messages);
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
logger.error({ error }, 'Error fetching session messages');
|
|
943
|
+
res.status(500).json({ error: 'Session messages ain\'t showing' });
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
// GET /api/team-members/deployments - list team member deployments
|
|
947
|
+
this.app.get('/api/team-members/deployments', this.requireAuth.bind(this), async (req, res) => {
|
|
948
|
+
try {
|
|
949
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
950
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
951
|
+
const status = req.query.status;
|
|
952
|
+
const environment = req.query.environment;
|
|
953
|
+
const deployments = await this.getTeamMemberDeployments(limit, offset, status, environment);
|
|
954
|
+
res.json(deployments);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
logger.error({ error }, 'Error fetching deployments');
|
|
958
|
+
res.status(500).json({ error: 'Failed to fetch deployments' });
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
// GET /api/team-members/stats - get aggregate team member statistics
|
|
962
|
+
this.app.get('/api/team-members/stats', this.requireAuth.bind(this), async (req, res) => {
|
|
963
|
+
try {
|
|
964
|
+
const stats = await this.getTeamMemberStats();
|
|
965
|
+
res.json(stats);
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
logger.error({ error }, 'Error fetching team member stats');
|
|
969
|
+
res.status(500).json({ error: 'Failed to fetch team member stats' });
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
// GET /api/team-members/collaboration/stats - get team member collaboration statistics
|
|
973
|
+
this.app.get('/api/team-members/collaboration/stats', this.requireAuth.bind(this), async (req, res) => {
|
|
974
|
+
try {
|
|
975
|
+
if (!this.db) {
|
|
976
|
+
res.status(503).json({ error: 'Database not connected' });
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
// Get collaboration statistics from team member sessions and messages
|
|
980
|
+
const collaborationQuery = `
|
|
981
|
+
SELECT
|
|
982
|
+
COUNT(DISTINCT s.id) as total_collaborations,
|
|
983
|
+
COUNT(DISTINCT s.team_member_id) as unique_teamMembers,
|
|
984
|
+
COUNT(m.id) as total_messages,
|
|
985
|
+
AVG(EXTRACT(EPOCH FROM (s.ended_at - s.started_at))) as avg_duration_seconds,
|
|
986
|
+
COUNT(DISTINCT DATE(s.started_at)) as active_days,
|
|
987
|
+
MAX(s.started_at) as last_collaboration
|
|
988
|
+
FROM team_member_sessions s
|
|
989
|
+
LEFT JOIN team_member_messages m ON m.session_id = s.id
|
|
990
|
+
WHERE s.started_at >= NOW() - INTERVAL '30 days'
|
|
991
|
+
`;
|
|
992
|
+
const result = await this.db.query(collaborationQuery);
|
|
993
|
+
const stats = result.rows[0];
|
|
994
|
+
res.json({
|
|
995
|
+
total_collaborations: parseInt(stats.total_collaborations) || 0,
|
|
996
|
+
unique_teamMembers: parseInt(stats.unique_teamMembers) || 0,
|
|
997
|
+
total_messages: parseInt(stats.total_messages) || 0,
|
|
998
|
+
avg_duration_seconds: parseFloat(stats.avg_duration_seconds) || 0,
|
|
999
|
+
active_days: parseInt(stats.active_days) || 0,
|
|
1000
|
+
last_collaboration: stats.last_collaboration || null,
|
|
1001
|
+
period: '30_days'
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
catch (error) {
|
|
1005
|
+
logger.error({ error }, 'Error fetching collaboration stats');
|
|
1006
|
+
res.status(500).json({ error: 'Failed to fetch collaboration stats' });
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
// Team Member Deployment & Tracking Routes
|
|
1010
|
+
// BUG FIX (Team Member 2): Added discovery-based team members to the list
|
|
1011
|
+
this.app.get('/api/team-members/list', this.requireAuth.bind(this), async (req, res) => {
|
|
1012
|
+
try {
|
|
1013
|
+
// Get team members from deployment tracker
|
|
1014
|
+
const deployedTeamMembers = await this.teamMemberTracker?.getAllTeamMembers() || [];
|
|
1015
|
+
const deployedIds = new Set(deployedTeamMembers.map(a => a.id));
|
|
1016
|
+
// Also get team members from discovery service (SpecMem heartbeat-based)
|
|
1017
|
+
let discoveredTeamMembers = [];
|
|
1018
|
+
try {
|
|
1019
|
+
if (this.dashboardDiscovery) {
|
|
1020
|
+
const discovered = await this.dashboardDiscovery.getActiveTeamMembers(120000); // 2 min expiry
|
|
1021
|
+
// Convert discovered team members to the same format, excluding already-deployed ones
|
|
1022
|
+
discoveredTeamMembers = discovered
|
|
1023
|
+
.filter((d) => !deployedIds.has(d.teamMemberId))
|
|
1024
|
+
.map((d) => ({
|
|
1025
|
+
id: d.teamMemberId,
|
|
1026
|
+
name: d.teamMemberName || d.teamMemberId.substring(0, 8),
|
|
1027
|
+
type: d.teamMemberType || 'worker',
|
|
1028
|
+
status: d.status === 'active' || d.status === 'busy' ? 'running' : d.status === 'idle' ? 'pending' : 'stopped',
|
|
1029
|
+
tokensUsed: d.metadata?.tokensUsed || 0,
|
|
1030
|
+
tokensLimit: d.metadata?.tokensLimit || 20000,
|
|
1031
|
+
createdAt: d.registeredAt || d.lastHeartbeat,
|
|
1032
|
+
lastHeartbeat: d.lastHeartbeat,
|
|
1033
|
+
currentTask: d.metadata?.currentTask,
|
|
1034
|
+
metadata: { ...d.metadata, source: 'discovery' }
|
|
1035
|
+
}));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
catch (discErr) {
|
|
1039
|
+
logger.debug({ error: discErr }, 'Discovery service not available');
|
|
1040
|
+
}
|
|
1041
|
+
// ALSO get Task team members from team_member_sessions table
|
|
1042
|
+
let taskTeamMembers = [];
|
|
1043
|
+
if (this.db) {
|
|
1044
|
+
try {
|
|
1045
|
+
const result = await this.db.query(`
|
|
1046
|
+
SELECT
|
|
1047
|
+
team_member_id as id,
|
|
1048
|
+
team_member_name as name,
|
|
1049
|
+
team_member_type as type,
|
|
1050
|
+
status,
|
|
1051
|
+
current_task,
|
|
1052
|
+
started_at as created_at,
|
|
1053
|
+
tokens_used,
|
|
1054
|
+
metadata,
|
|
1055
|
+
last_heartbeat
|
|
1056
|
+
FROM team_member_sessions
|
|
1057
|
+
ORDER BY started_at DESC
|
|
1058
|
+
LIMIT 100
|
|
1059
|
+
`);
|
|
1060
|
+
taskTeamMembers = result.rows.map((row) => ({
|
|
1061
|
+
id: row.id,
|
|
1062
|
+
name: row.name || row.id.substring(0, 8),
|
|
1063
|
+
type: row.type || 'worker',
|
|
1064
|
+
status: row.status === 'terminated' ? 'completed' : row.status === 'error' ? 'failed' : row.status,
|
|
1065
|
+
tokensUsed: row.tokens_used || 0,
|
|
1066
|
+
tokensLimit: row.metadata?.tokensLimit || 128000,
|
|
1067
|
+
createdAt: row.created_at,
|
|
1068
|
+
lastHeartbeat: row.last_heartbeat,
|
|
1069
|
+
currentTask: row.current_task ? { name: row.current_task, progress: 0 } : undefined,
|
|
1070
|
+
metadata: { ...row.metadata, source: 'task-team-member' }
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
1073
|
+
catch (taskErr) {
|
|
1074
|
+
logger.debug({ error: taskErr }, 'Failed to get Task team members');
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const allTeamMembers = [...deployedTeamMembers, ...discoveredTeamMembers, ...taskTeamMembers];
|
|
1078
|
+
res.json({ success: true, teamMembers: allTeamMembers });
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
logger.error({ error }, 'Error fetching team member list');
|
|
1082
|
+
res.status(500).json({ success: false, error: 'Failed to fetch team members' });
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
// REMOVED: Duplicate route handlers - now using teamMemberDeployRouter instead
|
|
1086
|
+
this.app.post('/api/team-members/:id/restart', this.requireAuth.bind(this), async (req, res) => {
|
|
1087
|
+
try {
|
|
1088
|
+
const { id } = req.params;
|
|
1089
|
+
const result = await this.teamMemberDeployment?.restart(id);
|
|
1090
|
+
res.json({ success: result });
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
logger.error({ error }, 'Error restarting team member');
|
|
1094
|
+
res.status(500).json({ success: false, error: 'Failed to restart team member' });
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
this.app.get('/api/team-members/:id/logs', this.requireAuth.bind(this), async (req, res) => {
|
|
1098
|
+
try {
|
|
1099
|
+
const { id } = req.params;
|
|
1100
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
1101
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
1102
|
+
// Check if this is a Task team member (from team_member_sessions table)
|
|
1103
|
+
let isTaskTeamMember = false;
|
|
1104
|
+
let sessionDbId = null;
|
|
1105
|
+
if (this.db) {
|
|
1106
|
+
try {
|
|
1107
|
+
const sessionCheck = await this.db.query(`
|
|
1108
|
+
SELECT id, metadata FROM team_member_sessions WHERE team_member_id = $1 LIMIT 1
|
|
1109
|
+
`, [id]);
|
|
1110
|
+
if (sessionCheck.rows.length > 0) {
|
|
1111
|
+
isTaskTeamMember = true;
|
|
1112
|
+
sessionDbId = sessionCheck.rows[0].id;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
catch (checkErr) {
|
|
1116
|
+
logger.debug({ error: checkErr }, 'Could not check for Task team member session');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (isTaskTeamMember && sessionDbId && this.db) {
|
|
1120
|
+
// Fetch logs for Task team member from team_member_logs table using session's DB id
|
|
1121
|
+
try {
|
|
1122
|
+
const result = await this.db.query(`
|
|
1123
|
+
SELECT
|
|
1124
|
+
al.id,
|
|
1125
|
+
al.team_member_id,
|
|
1126
|
+
al.level,
|
|
1127
|
+
al.message,
|
|
1128
|
+
al.metadata,
|
|
1129
|
+
al.created_at as timestamp
|
|
1130
|
+
FROM team_member_logs al
|
|
1131
|
+
WHERE al.team_member_id = $1
|
|
1132
|
+
ORDER BY al.created_at DESC
|
|
1133
|
+
LIMIT $2 OFFSET $3
|
|
1134
|
+
`, [sessionDbId, limit, offset]);
|
|
1135
|
+
const logs = result.rows.map((row) => ({
|
|
1136
|
+
id: row.id,
|
|
1137
|
+
teamMemberId: id,
|
|
1138
|
+
level: row.level || 'info',
|
|
1139
|
+
message: row.message,
|
|
1140
|
+
metadata: row.metadata,
|
|
1141
|
+
timestamp: row.timestamp
|
|
1142
|
+
}));
|
|
1143
|
+
res.json({ success: true, logs, isTaskTeamMember: true });
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
catch (logErr) {
|
|
1147
|
+
logger.debug({ error: logErr }, 'Could not fetch Task team member logs, falling back');
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// Fall back to native team member logs from teamMemberTracker
|
|
1151
|
+
const logs = await this.teamMemberTracker?.getLogs(id, limit, offset) || [];
|
|
1152
|
+
res.json({ success: true, logs });
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
logger.error({ error }, 'Error fetching team member logs');
|
|
1156
|
+
res.status(500).json({ success: false, error: 'Failed to fetch logs' });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
this.app.get('/api/team-members/:id/stats', this.requireAuth.bind(this), async (req, res) => {
|
|
1160
|
+
try {
|
|
1161
|
+
const { id } = req.params;
|
|
1162
|
+
const teamMember = await this.teamMemberTracker?.getTeamMember(id);
|
|
1163
|
+
if (!teamMember) {
|
|
1164
|
+
res.status(404).json({ success: false, error: 'Team Member not found' });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
res.json({ success: true, teamMember });
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
logger.error({ error }, 'Error fetching team member stats');
|
|
1171
|
+
res.status(500).json({ success: false, error: 'Failed to fetch team member stats' });
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
// TeamMember limits endpoint
|
|
1175
|
+
this.app.get('/api/team-members/:id/limits', this.requireAuth.bind(this), async (req, res) => {
|
|
1176
|
+
try {
|
|
1177
|
+
const { id } = req.params;
|
|
1178
|
+
const limits = this.teamMemberDeployment?.getTeamMemberLimits(id);
|
|
1179
|
+
const status = this.teamMemberDeployment?.getTeamMemberLimitStatus(id);
|
|
1180
|
+
if (!limits) {
|
|
1181
|
+
res.status(404).json({ success: false, error: 'Team Member limits not found' });
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
res.json({ success: true, limits, status });
|
|
1185
|
+
}
|
|
1186
|
+
catch (error) {
|
|
1187
|
+
logger.error({ error }, 'Error fetching team member limits');
|
|
1188
|
+
res.status(500).json({ success: false, error: 'Failed to fetch team member limits' });
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
// ==================== TEAM_MEMBER COLLABORATION ROUTES ====================
|
|
1192
|
+
this.app.post('/api/team-members/:id/share-code', this.requireAuth.bind(this), async (req, res) => {
|
|
1193
|
+
try {
|
|
1194
|
+
const { id } = req.params;
|
|
1195
|
+
const { title, description, code, filePath, language, tags } = req.body;
|
|
1196
|
+
if (!title || !code) {
|
|
1197
|
+
res.status(400).json({ success: false, error: 'Title and code required' });
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const shared = await this.teamMemberTracker?.shareCode(id, { title, description, code, filePath, language, tags });
|
|
1201
|
+
this.broadcastUpdate('code_shared', shared);
|
|
1202
|
+
res.json({ success: true, sharedCode: shared });
|
|
1203
|
+
}
|
|
1204
|
+
catch (error) {
|
|
1205
|
+
logger.error({ error }, 'Error sharing code');
|
|
1206
|
+
res.status(500).json({ success: false, error: 'Failed to share code' });
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
this.app.get('/api/team-members/shared-code', this.requireAuth.bind(this), async (req, res) => {
|
|
1210
|
+
try {
|
|
1211
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 100);
|
|
1212
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
1213
|
+
const sharedCode = await this.teamMemberTracker?.getAllSharedCode(limit, offset) || [];
|
|
1214
|
+
res.json({ success: true, sharedCode });
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
logger.error({ error }, 'Error fetching shared code');
|
|
1218
|
+
res.status(500).json({ success: false, error: 'Failed to fetch shared code' });
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
this.app.get('/api/team-members/shared-code/:codeId', this.requireAuth.bind(this), async (req, res) => {
|
|
1222
|
+
try {
|
|
1223
|
+
const { codeId } = req.params;
|
|
1224
|
+
const code = await this.teamMemberTracker?.getSharedCode(codeId);
|
|
1225
|
+
if (!code) {
|
|
1226
|
+
res.status(404).json({ success: false, error: 'Shared code not found' });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
res.json({ success: true, sharedCode: code });
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
logger.error({ error }, 'Error fetching shared code');
|
|
1233
|
+
res.status(500).json({ success: false, error: 'Failed to fetch shared code' });
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
this.app.get('/api/team-members/:id/shared-code', this.requireAuth.bind(this), async (req, res) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const { id } = req.params;
|
|
1239
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 100);
|
|
1240
|
+
const sharedCode = await this.teamMemberTracker?.getSharedCodeByTeamMember(id, limit) || [];
|
|
1241
|
+
res.json({ success: true, sharedCode });
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
logger.error({ error }, 'Error fetching team member shared code');
|
|
1245
|
+
res.status(500).json({ success: false, error: 'Failed to fetch shared code' });
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
this.app.get('/api/team-members/shared-code/:codeId/chunk/:index', this.requireAuth.bind(this), async (req, res) => {
|
|
1249
|
+
try {
|
|
1250
|
+
const { codeId, index } = req.params;
|
|
1251
|
+
const chunkIndex = parseInt(index, 10);
|
|
1252
|
+
if (isNaN(chunkIndex) || chunkIndex < 0) {
|
|
1253
|
+
res.status(400).json({ success: false, error: 'Invalid chunk index' });
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const chunk = await this.teamMemberTracker?.getCodeChunk(codeId, chunkIndex);
|
|
1257
|
+
if (!chunk) {
|
|
1258
|
+
res.status(404).json({ success: false, error: 'Chunk not found' });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
res.json({ success: true, chunk });
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
logger.error({ error }, 'Error fetching code chunk');
|
|
1265
|
+
res.status(500).json({ success: false, error: 'Failed to fetch code chunk' });
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
this.app.get('/api/team-members/shared-code/:codeId/download', this.requireAuth.bind(this), async (req, res) => {
|
|
1269
|
+
try {
|
|
1270
|
+
const { codeId } = req.params;
|
|
1271
|
+
const code = await this.teamMemberTracker?.getSharedCode(codeId);
|
|
1272
|
+
if (!code) {
|
|
1273
|
+
res.status(404).json({ success: false, error: 'Shared code not found' });
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const fullCode = await this.teamMemberTracker?.getFullCode(codeId);
|
|
1277
|
+
if (!fullCode) {
|
|
1278
|
+
res.status(404).json({ success: false, error: 'Code content not found' });
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const ext = this.getFileExtension(code.language);
|
|
1282
|
+
const filename = code.filePath ? code.filePath.split('/').pop() : `${code.title.replace(/[^a-z0-9]/gi, '_')}.${ext}`;
|
|
1283
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
1284
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
1285
|
+
res.setHeader('Content-Length', Buffer.byteLength(fullCode, 'utf-8'));
|
|
1286
|
+
res.send(fullCode);
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
logger.error({ error }, 'Error downloading code');
|
|
1290
|
+
res.status(500).json({ success: false, error: 'Failed to download code' });
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
this.app.post('/api/team-members/shared-code/:codeId/feedback', this.requireAuth.bind(this), async (req, res) => {
|
|
1294
|
+
try {
|
|
1295
|
+
const { codeId } = req.params;
|
|
1296
|
+
const { fromTeamMemberId, feedbackType, message } = req.body;
|
|
1297
|
+
if (!fromTeamMemberId || !feedbackType || !message) {
|
|
1298
|
+
res.status(400).json({ success: false, error: 'fromTeamMemberId, feedbackType, and message required' });
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (!['positive', 'negative', 'question', 'critique'].includes(feedbackType)) {
|
|
1302
|
+
res.status(400).json({ success: false, error: 'Invalid feedback type' });
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const feedback = await this.teamMemberTracker?.giveFeedback(fromTeamMemberId, codeId, feedbackType, message);
|
|
1306
|
+
this.broadcastUpdate('feedback_given', feedback);
|
|
1307
|
+
res.json({ success: true, feedback });
|
|
1308
|
+
}
|
|
1309
|
+
catch (error) {
|
|
1310
|
+
logger.error({ error }, 'Error giving feedback');
|
|
1311
|
+
res.status(500).json({ success: false, error: 'Failed to give feedback' });
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
this.app.get('/api/team-members/shared-code/:codeId/feedback', this.requireAuth.bind(this), async (req, res) => {
|
|
1315
|
+
try {
|
|
1316
|
+
const { codeId } = req.params;
|
|
1317
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 100);
|
|
1318
|
+
const feedback = await this.teamMemberTracker?.getFeedbackForCode(codeId, limit) || [];
|
|
1319
|
+
res.json({ success: true, feedback });
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
logger.error({ error }, 'Error fetching feedback');
|
|
1323
|
+
res.status(500).json({ success: false, error: 'Failed to fetch feedback' });
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
this.app.post('/api/team-members/:id/message', this.requireAuth.bind(this), async (req, res) => {
|
|
1327
|
+
try {
|
|
1328
|
+
const { id } = req.params;
|
|
1329
|
+
const { toTeamMemberId, message, metadata } = req.body;
|
|
1330
|
+
if (!toTeamMemberId || !message) {
|
|
1331
|
+
res.status(400).json({ success: false, error: 'toTeamMemberId and message required' });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const msg = await this.teamMemberTracker?.sendMessage(id, toTeamMemberId, message, metadata);
|
|
1335
|
+
this.broadcastUpdate('message_sent', msg);
|
|
1336
|
+
res.json({ success: true, message: msg });
|
|
1337
|
+
}
|
|
1338
|
+
catch (error) {
|
|
1339
|
+
logger.error({ error }, 'Error sending message');
|
|
1340
|
+
res.status(500).json({ success: false, error: 'Failed to send message' });
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
this.app.get('/api/team-members/:id/messages', this.requireAuth.bind(this), async (req, res) => {
|
|
1344
|
+
try {
|
|
1345
|
+
const { id } = req.params;
|
|
1346
|
+
const limit = Math.min(parseInt(req.query.limit) || 50, 100);
|
|
1347
|
+
const unreadOnly = req.query.unreadOnly === 'true';
|
|
1348
|
+
const messages = await this.teamMemberTracker?.getMessagesForTeamMember(id, limit, unreadOnly) || [];
|
|
1349
|
+
res.json({ success: true, messages });
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
logger.error({ error }, 'Error fetching messages');
|
|
1353
|
+
res.status(500).json({ success: false, error: 'Failed to fetch messages' });
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
this.app.post('/api/team-members/messages/:messageId/read', this.requireAuth.bind(this), async (req, res) => {
|
|
1357
|
+
try {
|
|
1358
|
+
const { messageId } = req.params;
|
|
1359
|
+
await this.teamMemberTracker?.markMessageRead(messageId);
|
|
1360
|
+
res.json({ success: true });
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
logger.error({ error }, 'Error marking message read');
|
|
1364
|
+
res.status(500).json({ success: false, error: 'Failed to mark message read' });
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
this.app.get('/api/team-members/:id/unread-count', this.requireAuth.bind(this), async (req, res) => {
|
|
1368
|
+
try {
|
|
1369
|
+
const { id } = req.params;
|
|
1370
|
+
const count = await this.teamMemberTracker?.getUnreadMessageCount(id) || 0;
|
|
1371
|
+
res.json({ success: true, count });
|
|
1372
|
+
}
|
|
1373
|
+
catch (error) {
|
|
1374
|
+
logger.error({ error }, 'Error fetching unread count');
|
|
1375
|
+
res.status(500).json({ success: false, error: 'Failed to fetch unread count' });
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
this.app.get('/api/team-members/:id/pending-reviews', this.requireAuth.bind(this), async (req, res) => {
|
|
1379
|
+
try {
|
|
1380
|
+
const { id } = req.params;
|
|
1381
|
+
const pendingReviews = await this.teamMemberTracker?.getPendingReviewsForTeamMember(id) || [];
|
|
1382
|
+
res.json({ success: true, pendingReviews });
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
logger.error({ error }, 'Error fetching pending reviews');
|
|
1386
|
+
res.status(500).json({ success: false, error: 'Failed to fetch pending reviews' });
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
this.app.get('/api/team-members/collaboration/stats', this.requireAuth.bind(this), async (req, res) => {
|
|
1390
|
+
try {
|
|
1391
|
+
const stats = this.teamMemberTracker?.getCollaborationStats() || {
|
|
1392
|
+
totalSharedCode: 0, totalFeedback: 0, totalMessages: 0, positiveRatio: 0
|
|
1393
|
+
};
|
|
1394
|
+
res.json({ success: true, stats });
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
logger.error({ error }, 'Error fetching collaboration stats');
|
|
1398
|
+
res.status(500).json({ success: false, error: 'Failed to fetch collaboration stats' });
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
// ==================== TEAM_MEMBER COMMAND ROUTE ====================
|
|
1402
|
+
// POST /api/team-members/:id/command - Send command to team member stdin OR via SpecMem
|
|
1403
|
+
// BUG FIX (Team Member 2): Added fallback to SpecMem-based communication for discovered team members
|
|
1404
|
+
this.app.post('/api/team-members/:id/command', this.requireAuth.bind(this), async (req, res) => {
|
|
1405
|
+
try {
|
|
1406
|
+
const { id } = req.params;
|
|
1407
|
+
const { command } = req.body;
|
|
1408
|
+
if (!command || typeof command !== 'object') {
|
|
1409
|
+
res.status(400).json({ success: false, error: 'Command must be a valid object' });
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
// Validate command has a type
|
|
1413
|
+
if (!command.type || typeof command.type !== 'string') {
|
|
1414
|
+
res.status(400).json({ success: false, error: 'Command must have a type field' });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
// First try sending via deployment manager (for spawned processes)
|
|
1418
|
+
let result = await this.teamMemberDeployment?.sendCommand(id, command);
|
|
1419
|
+
// If deployment manager failed, try SpecMem-based communication
|
|
1420
|
+
if (!result && this.dashboardCommunicator) {
|
|
1421
|
+
try {
|
|
1422
|
+
// Check if team member exists in discovery
|
|
1423
|
+
const teamMemberOnline = this.dashboardDiscovery ?
|
|
1424
|
+
await this.dashboardDiscovery.isTeamMemberOnline(id) : false;
|
|
1425
|
+
if (teamMemberOnline) {
|
|
1426
|
+
// Send message via SpecMem
|
|
1427
|
+
const messageContent = JSON.stringify({
|
|
1428
|
+
type: 'command',
|
|
1429
|
+
command: command,
|
|
1430
|
+
from: 'dashboard',
|
|
1431
|
+
timestamp: new Date().toISOString()
|
|
1432
|
+
});
|
|
1433
|
+
const sent = await this.dashboardCommunicator.say(messageContent, id, { priority: 'high' });
|
|
1434
|
+
if (sent) {
|
|
1435
|
+
result = {
|
|
1436
|
+
success: true,
|
|
1437
|
+
response: { status: 'sent', via: 'specmem' },
|
|
1438
|
+
queued: true
|
|
1439
|
+
};
|
|
1440
|
+
logger.info({ teamMemberId: id }, 'Command sent via SpecMem communicator');
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
catch (specMemErr) {
|
|
1445
|
+
logger.debug({ error: specMemErr }, 'SpecMem communication failed');
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (!result) {
|
|
1449
|
+
res.status(404).json({ success: false, error: 'Team Member not found or not running' });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
// Log the command (truncate for safety)
|
|
1453
|
+
const commandStr = JSON.stringify(command);
|
|
1454
|
+
const truncatedCmd = commandStr.length > 200 ? commandStr.substring(0, 200) + '...' : commandStr;
|
|
1455
|
+
await this.teamMemberTracker?.addLog(id, 'info', `Command sent: ${truncatedCmd}`);
|
|
1456
|
+
// Broadcast command event via WebSocket
|
|
1457
|
+
this.broadcastUpdate('teamMember_command', {
|
|
1458
|
+
teamMemberId: id,
|
|
1459
|
+
command,
|
|
1460
|
+
timestamp: new Date().toISOString()
|
|
1461
|
+
});
|
|
1462
|
+
res.json({ success: true, response: result.response, queued: result.queued });
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
logger.error({ error }, 'Error sending command to team member');
|
|
1466
|
+
res.status(500).json({ success: false, error: 'Failed to send command to team member' });
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
// ==================== TEAM_MEMBER HISTORY ROUTES ====================
|
|
1470
|
+
// BUG FIX (Team Member 2): Also include discovered team members with active sessions
|
|
1471
|
+
this.app.get('/api/team-members/history/teamMembers', this.requireAuth.bind(this), async (req, res) => {
|
|
1472
|
+
try {
|
|
1473
|
+
// Get historical team members from session database
|
|
1474
|
+
const historicalTeamMembers = await this.teamMemberHistoryManager?.getTeamMembersWithSessionCounts() || [];
|
|
1475
|
+
const historicalIds = new Set(historicalTeamMembers.map(a => a.id));
|
|
1476
|
+
// Also include currently active discovered team members (they may have no DB sessions yet)
|
|
1477
|
+
let activeTeamMembers = [];
|
|
1478
|
+
try {
|
|
1479
|
+
if (this.dashboardDiscovery) {
|
|
1480
|
+
const discovered = await this.dashboardDiscovery.getActiveTeamMembers(300000); // 5 min window
|
|
1481
|
+
activeTeamMembers = discovered
|
|
1482
|
+
.filter((d) => !historicalIds.has(d.teamMemberId))
|
|
1483
|
+
.map((d) => ({
|
|
1484
|
+
id: d.teamMemberId,
|
|
1485
|
+
name: d.teamMemberName || d.teamMemberId.substring(0, 8),
|
|
1486
|
+
type: d.teamMemberType || 'worker',
|
|
1487
|
+
sessionCount: 1, // Current session counts as 1
|
|
1488
|
+
lastSessionDate: d.lastHeartbeat,
|
|
1489
|
+
totalTokensUsed: d.metadata?.tokensUsed || 0
|
|
1490
|
+
}));
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
catch (discErr) {
|
|
1494
|
+
logger.debug({ error: discErr }, 'Discovery not available for history');
|
|
1495
|
+
}
|
|
1496
|
+
const allTeamMembers = [...historicalTeamMembers, ...activeTeamMembers];
|
|
1497
|
+
res.json({ success: true, teamMembers: allTeamMembers });
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
logger.error({ error }, 'Error fetching team members with session counts');
|
|
1501
|
+
res.status(500).json({ success: false, error: 'Failed to fetch team member history list' });
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
// BUG FIX (Team Member 2): Also return synthetic session for currently active discovered team members
|
|
1505
|
+
this.app.get('/api/team-members/:id/sessions', this.requireAuth.bind(this), async (req, res) => {
|
|
1506
|
+
try {
|
|
1507
|
+
const { id } = req.params;
|
|
1508
|
+
const limit = Math.min(parseInt(req.query.limit) || 10, 50);
|
|
1509
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
1510
|
+
// Get sessions from history manager
|
|
1511
|
+
let sessions = await this.teamMemberHistoryManager?.getSessionsForTeamMember(id, limit, offset) || [];
|
|
1512
|
+
// If no sessions found, check if this is a currently active discovered teamMember
|
|
1513
|
+
if (sessions.length === 0 && this.dashboardDiscovery) {
|
|
1514
|
+
try {
|
|
1515
|
+
const teamMemberInfo = await this.dashboardDiscovery.getTeamMemberInfo(id);
|
|
1516
|
+
if (teamMemberInfo) {
|
|
1517
|
+
// Create a synthetic "current session" for this active teamMember
|
|
1518
|
+
sessions = [{
|
|
1519
|
+
id: `live-${id}`,
|
|
1520
|
+
teamMemberId: id,
|
|
1521
|
+
teamMemberName: teamMemberInfo.teamMemberName || id.substring(0, 8),
|
|
1522
|
+
teamMemberType: teamMemberInfo.teamMemberType || 'worker',
|
|
1523
|
+
sessionStart: teamMemberInfo.registeredAt || teamMemberInfo.lastHeartbeat,
|
|
1524
|
+
sessionEnd: null,
|
|
1525
|
+
taskCount: teamMemberInfo.metadata?.currentTask ? 1 : 0,
|
|
1526
|
+
codeCount: 0,
|
|
1527
|
+
feedbackCount: 0,
|
|
1528
|
+
messageCount: 0,
|
|
1529
|
+
tokensUsed: teamMemberInfo.metadata?.tokensUsed || 0,
|
|
1530
|
+
status: teamMemberInfo.status === 'active' || teamMemberInfo.status === 'busy' ? 'running' : 'completed',
|
|
1531
|
+
summary: `Active session - ${teamMemberInfo.status}`
|
|
1532
|
+
}];
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
catch (discErr) {
|
|
1536
|
+
logger.debug({ error: discErr }, 'Discovery not available for sessions');
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
res.json({ success: true, sessions });
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
logger.error({ error }, 'Error fetching sessions for team member');
|
|
1543
|
+
res.status(500).json({ success: false, error: 'Failed to fetch sessions' });
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
// BUG FIX (Team Member 2): Handle live sessions for discovered team members
|
|
1547
|
+
this.app.get('/api/team-members/sessions/:sessionId', this.requireAuth.bind(this), async (req, res) => {
|
|
1548
|
+
try {
|
|
1549
|
+
const { sessionId } = req.params;
|
|
1550
|
+
// Check for live session (synthetic session for discovered team members)
|
|
1551
|
+
if (sessionId.startsWith('live-') && this.dashboardDiscovery) {
|
|
1552
|
+
const teamMemberId = sessionId.substring(5); // Remove 'live-' prefix
|
|
1553
|
+
const teamMemberInfo = await this.dashboardDiscovery.getTeamMemberInfo(teamMemberId);
|
|
1554
|
+
if (teamMemberInfo) {
|
|
1555
|
+
const liveSession = {
|
|
1556
|
+
id: sessionId,
|
|
1557
|
+
teamMemberId: teamMemberId,
|
|
1558
|
+
teamMemberName: teamMemberInfo.teamMemberName || teamMemberId.substring(0, 8),
|
|
1559
|
+
teamMemberType: teamMemberInfo.teamMemberType || 'worker',
|
|
1560
|
+
sessionStart: teamMemberInfo.registeredAt || teamMemberInfo.lastHeartbeat,
|
|
1561
|
+
sessionEnd: null,
|
|
1562
|
+
tasksCompleted: teamMemberInfo.metadata?.currentTask ? [{
|
|
1563
|
+
id: 'current',
|
|
1564
|
+
name: teamMemberInfo.metadata.currentTask,
|
|
1565
|
+
status: 'running',
|
|
1566
|
+
startedAt: teamMemberInfo.lastHeartbeat
|
|
1567
|
+
}] : [],
|
|
1568
|
+
codeSharedIds: [],
|
|
1569
|
+
feedbackGivenIds: [],
|
|
1570
|
+
messagesSentIds: [],
|
|
1571
|
+
tokensUsed: teamMemberInfo.metadata?.tokensUsed || 0,
|
|
1572
|
+
status: 'running',
|
|
1573
|
+
summary: `Live session for ${teamMemberInfo.teamMemberName || teamMemberId}`
|
|
1574
|
+
};
|
|
1575
|
+
res.json({ success: true, session: liveSession });
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
// Fall back to database session
|
|
1580
|
+
const session = await this.teamMemberHistoryManager?.getSessionDetails(sessionId);
|
|
1581
|
+
if (!session) {
|
|
1582
|
+
res.status(404).json({ success: false, error: 'Session not found' });
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
res.json({ success: true, session });
|
|
1586
|
+
}
|
|
1587
|
+
catch (error) {
|
|
1588
|
+
logger.error({ error }, 'Error fetching session details');
|
|
1589
|
+
res.status(500).json({ success: false, error: 'Failed to fetch session details' });
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
// BUG FIX (Team Member 2): Handle live session logs from SpecMem messages
|
|
1593
|
+
this.app.get('/api/team-members/sessions/:sessionId/logs', this.requireAuth.bind(this), async (req, res) => {
|
|
1594
|
+
try {
|
|
1595
|
+
const { sessionId } = req.params;
|
|
1596
|
+
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
|
1597
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
1598
|
+
// Check for live session logs
|
|
1599
|
+
if (sessionId.startsWith('live-') && this.dashboardCommunicator) {
|
|
1600
|
+
const teamMemberId = sessionId.substring(5);
|
|
1601
|
+
try {
|
|
1602
|
+
// Try to get recent messages from/to this team member as "logs"
|
|
1603
|
+
const messages = await this.dashboardCommunicator.getMessages(new Date(Date.now() - 3600000) // Last hour
|
|
1604
|
+
);
|
|
1605
|
+
const teamMemberLogs = messages
|
|
1606
|
+
.filter(m => m.from === teamMemberId || m.to === teamMemberId)
|
|
1607
|
+
.map((m, i) => ({
|
|
1608
|
+
id: m.messageId || `msg-${i}`,
|
|
1609
|
+
teamMemberId: m.from,
|
|
1610
|
+
timestamp: m.timestamp,
|
|
1611
|
+
level: m.messageType === 'status' ? 'info' : 'debug',
|
|
1612
|
+
message: m.content
|
|
1613
|
+
}))
|
|
1614
|
+
.slice(offset, offset + limit);
|
|
1615
|
+
res.json({
|
|
1616
|
+
success: true,
|
|
1617
|
+
logs: teamMemberLogs,
|
|
1618
|
+
totalCount: teamMemberLogs.length,
|
|
1619
|
+
limit,
|
|
1620
|
+
offset
|
|
1621
|
+
});
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
catch (msgErr) {
|
|
1625
|
+
logger.debug({ error: msgErr }, 'Could not get messages for live session');
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
// Fall back to database logs
|
|
1629
|
+
const logs = await this.teamMemberHistoryManager?.getSessionLogs(sessionId, limit, offset) || [];
|
|
1630
|
+
const totalCount = await this.teamMemberHistoryManager?.getSessionLogCount(sessionId) || 0;
|
|
1631
|
+
res.json({ success: true, logs, totalCount, limit, offset });
|
|
1632
|
+
}
|
|
1633
|
+
catch (error) {
|
|
1634
|
+
logger.error({ error }, 'Error fetching session logs');
|
|
1635
|
+
res.status(500).json({ success: false, error: 'Failed to fetch session logs' });
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
// Configuration routes
|
|
1639
|
+
this.app.get('/api/config', this.requireAuth.bind(this), (req, res) => {
|
|
1640
|
+
res.json({
|
|
1641
|
+
coordinationPort: this.config.coordinationPort,
|
|
1642
|
+
dashboardPort: this.config.port
|
|
1643
|
+
});
|
|
1644
|
+
});
|
|
1645
|
+
this.app.post('/api/config/password', this.requireAuth.bind(this), async (req, res) => {
|
|
1646
|
+
const { currentPassword, newPassword } = req.body;
|
|
1647
|
+
// Use centralized password change with team member notification
|
|
1648
|
+
// This handles: validation, runtime update, env persistence, hook update, and team member notification
|
|
1649
|
+
const result = await changePasswordWithTeamMemberNotification(currentPassword, newPassword, true);
|
|
1650
|
+
if (!result.success) {
|
|
1651
|
+
// Determine appropriate status code based on error
|
|
1652
|
+
const statusCode = result.message.includes('incorrect') ? 401 : 400;
|
|
1653
|
+
res.status(statusCode).json({ error: result.message });
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
// Also update this.config.password for backwards compatibility with any code
|
|
1657
|
+
// that still reads from config directly
|
|
1658
|
+
this.config.password = newPassword;
|
|
1659
|
+
logger.info({
|
|
1660
|
+
persisted: result.persisted,
|
|
1661
|
+
hookUpdated: result.hookUpdated,
|
|
1662
|
+
teamMembersNotified: result.teamMembersNotified
|
|
1663
|
+
}, 'Dashboard password changed via centralized system');
|
|
1664
|
+
res.json({
|
|
1665
|
+
success: true,
|
|
1666
|
+
message: result.message,
|
|
1667
|
+
persisted: result.persisted,
|
|
1668
|
+
hookUpdated: result.hookUpdated,
|
|
1669
|
+
teamMembersNotified: result.teamMembersNotified
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
// ==================== MEMORY MANAGEMENT ROUTES ====================
|
|
1673
|
+
// Get memory configuration
|
|
1674
|
+
this.app.get('/api/memory/config', this.requireAuth.bind(this), async (req, res) => {
|
|
1675
|
+
try {
|
|
1676
|
+
const memoryConfig = await this.getMemoryConfig();
|
|
1677
|
+
res.json(memoryConfig);
|
|
1678
|
+
}
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
logger.error({ error }, 'Error fetching memory config');
|
|
1681
|
+
res.status(500).json({ error: 'Failed to fetch memory configuration' });
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
// Update memory configuration
|
|
1685
|
+
this.app.post('/api/memory/config', this.requireAuth.bind(this), async (req, res) => {
|
|
1686
|
+
try {
|
|
1687
|
+
const { memoryLimit, overflowTime, cacheSize } = req.body;
|
|
1688
|
+
// Validate inputs
|
|
1689
|
+
if (memoryLimit !== undefined && (memoryLimit < 50 || memoryLimit > 200)) {
|
|
1690
|
+
res.status(400).json({ error: 'Memory limit must be between 50 and 200 MB' });
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (overflowTime !== undefined && (overflowTime < 0 || overflowTime > 72)) {
|
|
1694
|
+
res.status(400).json({ error: 'Overflow time must be between 0 and 72 hours' });
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (cacheSize !== undefined && (cacheSize < 100 || cacheSize > 1000)) {
|
|
1698
|
+
res.status(400).json({ error: 'Cache size must be between 100 and 1000 entries' });
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const persisted = await this.persistMemoryConfig({ memoryLimit, overflowTime, cacheSize });
|
|
1702
|
+
logger.info({ memoryLimit, overflowTime, cacheSize, persisted }, 'Memory configuration updated');
|
|
1703
|
+
res.json({
|
|
1704
|
+
success: true,
|
|
1705
|
+
message: persisted ? 'Memory configuration saved to specmem.env' : 'Memory configuration updated (in-memory only)',
|
|
1706
|
+
config: { memoryLimit, overflowTime, cacheSize }
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
catch (error) {
|
|
1710
|
+
logger.error({ error }, 'Error updating memory config');
|
|
1711
|
+
res.status(500).json({ error: 'Failed to update memory configuration' });
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
// Get memory statistics
|
|
1715
|
+
this.app.get('/api/memory/stats', this.requireAuth.bind(this), async (req, res) => {
|
|
1716
|
+
try {
|
|
1717
|
+
const stats = await this.getMemoryStats();
|
|
1718
|
+
res.json(stats);
|
|
1719
|
+
}
|
|
1720
|
+
catch (error) {
|
|
1721
|
+
logger.error({ error }, 'Error fetching memory stats');
|
|
1722
|
+
res.status(500).json({ error: 'Failed to fetch memory statistics' });
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
// Trigger overflow cleanup
|
|
1726
|
+
this.app.post('/api/memory/overflow', this.requireAuth.bind(this), async (req, res) => {
|
|
1727
|
+
try {
|
|
1728
|
+
const result = await this.triggerOverflowCleanup();
|
|
1729
|
+
this.broadcastUpdate('overflow_triggered', result);
|
|
1730
|
+
res.json(result);
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
logger.error({ error }, 'Error triggering overflow cleanup');
|
|
1734
|
+
res.status(500).json({ error: 'Failed to trigger overflow cleanup' });
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
// Emergency memory purge
|
|
1738
|
+
this.app.post('/api/memory/purge', this.requireAuth.bind(this), async (req, res) => {
|
|
1739
|
+
try {
|
|
1740
|
+
const result = await this.emergencyMemoryPurge();
|
|
1741
|
+
this.broadcastUpdate('emergency_purge', result);
|
|
1742
|
+
logger.warn({ result }, 'Emergency memory purge executed');
|
|
1743
|
+
res.json(result);
|
|
1744
|
+
}
|
|
1745
|
+
catch (error) {
|
|
1746
|
+
logger.error({ error }, 'Error executing emergency purge');
|
|
1747
|
+
res.status(500).json({ error: 'Failed to execute emergency purge' });
|
|
1748
|
+
}
|
|
1749
|
+
});
|
|
1750
|
+
// Clear specific cache
|
|
1751
|
+
this.app.delete('/api/memory/cache/:type', this.requireAuth.bind(this), async (req, res) => {
|
|
1752
|
+
try {
|
|
1753
|
+
const { type } = req.params;
|
|
1754
|
+
if (!['query', 'embedding'].includes(type)) {
|
|
1755
|
+
res.status(400).json({ error: 'Invalid cache type. Must be "query" or "embedding"' });
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const result = await this.clearCache(type);
|
|
1759
|
+
this.broadcastUpdate('cache_cleared', { type, ...result });
|
|
1760
|
+
res.json(result);
|
|
1761
|
+
}
|
|
1762
|
+
catch (error) {
|
|
1763
|
+
logger.error({ error }, 'Error clearing cache');
|
|
1764
|
+
res.status(500).json({ error: 'Failed to clear cache' });
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
// ==================== PHASE 4-6 ROUTES ====================
|
|
1768
|
+
// Phase 4: Direct Prompting API
|
|
1769
|
+
const promptSendRouter = createPromptSendRouter(this.db, this.requireAuth.bind(this));
|
|
1770
|
+
this.app.use('/api/prompt', promptSendRouter);
|
|
1771
|
+
// Phase 5: Terminal Output API
|
|
1772
|
+
const terminalRouter = createTerminalRouter(this.requireAuth.bind(this));
|
|
1773
|
+
this.app.use('/api/terminal', terminalRouter);
|
|
1774
|
+
// Terminal Injection API - Direct prompt injection into Code terminal!
|
|
1775
|
+
const terminalInjectRouter = createTerminalInjectRouter(this.requireAuth.bind(this));
|
|
1776
|
+
this.app.use('/api/terminal-inject', terminalInjectRouter);
|
|
1777
|
+
// Terminal Streaming API - PTY streaming with full ANSI support!
|
|
1778
|
+
const terminalStreamRouter = createTerminalStreamRouter(this.requireAuth.bind(this));
|
|
1779
|
+
this.app.use('/api/terminal-stream', terminalStreamRouter);
|
|
1780
|
+
// Phase 6: Control API
|
|
1781
|
+
const claudeControlRouter = createControlRouter(this.db, this.requireAuth.bind(this), this.broadcastUpdate.bind(this));
|
|
1782
|
+
this.app.use('/api/claude', claudeControlRouter);
|
|
1783
|
+
// Specmem Tools API - Expose MCP tools to team members via HTTP
|
|
1784
|
+
// Pass embedding provider GETTER so HTTP endpoints use REAL MCP tool semantic search!
|
|
1785
|
+
// NOTE: Using getter function because embeddingProvider is set AFTER server starts
|
|
1786
|
+
const specmemToolsRouter = createSpecmemToolsRouter(() => this.db, this.requireAuth.bind(this), () => this.embeddingProvider);
|
|
1787
|
+
// SECURITY: Localhost-only access for SpecMem API (internal bridge only)
|
|
1788
|
+
// This ensures SpecMem API is NEVER exposed to public internet
|
|
1789
|
+
this.app.use('/api/specmem', (req, res, next) => {
|
|
1790
|
+
const clientIP = req.ip || req.socket?.remoteAddress || '';
|
|
1791
|
+
const isLocalhost = ['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost'].some(ip => clientIP.includes(ip) || clientIP === ip);
|
|
1792
|
+
if (!isLocalhost) {
|
|
1793
|
+
console.warn(`[SPECMEM-API] BLOCKED non-localhost access attempt from: ${clientIP}`);
|
|
1794
|
+
return res.status(403).json({ error: 'Access denied - localhost only' });
|
|
1795
|
+
}
|
|
1796
|
+
next();
|
|
1797
|
+
});
|
|
1798
|
+
// SECURITY: Encrypted payload middleware for SpecMem API
|
|
1799
|
+
// Decrypts Serpent-32 encrypted payloads from SpecMemSecurityBridge
|
|
1800
|
+
this.app.use('/api/specmem', (req, res, next) => {
|
|
1801
|
+
// Check for encrypted payload header
|
|
1802
|
+
if (req.headers['x-specmem-encrypted'] === 'serpent-32' && req.body?._encrypted) {
|
|
1803
|
+
try {
|
|
1804
|
+
const { _payload, _timestamp, _nonce } = req.body;
|
|
1805
|
+
// Validate timestamp (prevent replay attacks - max 5 min old)
|
|
1806
|
+
const MAX_AGE_MS = 5 * 60 * 1000;
|
|
1807
|
+
if (Date.now() - _timestamp > MAX_AGE_MS) {
|
|
1808
|
+
console.warn('[SPECMEM-API] Rejected stale encrypted request (replay attack?)');
|
|
1809
|
+
return res.status(400).json({ error: 'Request expired' });
|
|
1810
|
+
}
|
|
1811
|
+
// Decrypt using shared secret + nonce
|
|
1812
|
+
const EncryptedDataCommunication = require('/server/serverModules/security/EncryptedDataCommunication');
|
|
1813
|
+
const apiSecret = process.env.SPECMEM_API_SECRET || 'specmem_serpent_key_2025_security';
|
|
1814
|
+
const decryptor = new EncryptedDataCommunication({ encryptionKey: apiSecret });
|
|
1815
|
+
// Reconstruct the key from shared secret + nonce (same as encryptData does)
|
|
1816
|
+
const decrypted = decryptor.decryptData({
|
|
1817
|
+
encrypted: _payload,
|
|
1818
|
+
nonce: _nonce
|
|
1819
|
+
}, 'specmem-bridge');
|
|
1820
|
+
req.body = decrypted;
|
|
1821
|
+
console.log('[SPECMEM-API] Decrypted incoming encrypted payload');
|
|
1822
|
+
}
|
|
1823
|
+
catch (err) {
|
|
1824
|
+
console.error('[SPECMEM-API] Decryption failed:', err.message);
|
|
1825
|
+
return res.status(400).json({ error: 'Decryption failed' });
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
next();
|
|
1829
|
+
});
|
|
1830
|
+
this.app.use('/api/specmem', specmemToolsRouter);
|
|
1831
|
+
// Task Team Members API - Track Code Task tool deployments
|
|
1832
|
+
const taskTeamMembersRouter = createTaskTeamMembersRouter();
|
|
1833
|
+
this.app.use('/api/task-team-members', taskTeamMembersRouter);
|
|
1834
|
+
// LIVE Session Streaming API - Team Member 2's real-time Code viewer!
|
|
1835
|
+
const liveSessionRouter = createLiveSessionRouter(this.requireAuth.bind(this));
|
|
1836
|
+
this.app.use('/api/live', liveSessionRouter);
|
|
1837
|
+
// File Manager API - FTP-style file browser for codebase management
|
|
1838
|
+
const fileManagerRouter = createFileManagerRouter(() => this.db, this.requireAuth.bind(this));
|
|
1839
|
+
this.app.use('/api/file-manager', fileManagerRouter);
|
|
1840
|
+
// Settings API - Password management and dashboard configuration
|
|
1841
|
+
const settingsRouter = createSettingsRouter(this.requireAuth.bind(this));
|
|
1842
|
+
this.app.use('/api/settings', settingsRouter);
|
|
1843
|
+
// Setup API - Dashboard mode switching and initial setup wizard
|
|
1844
|
+
// Note: Some endpoints are public (status), some require auth (mode switch to public)
|
|
1845
|
+
const setupRouter = createSetupRouter(this.requireAuth.bind(this));
|
|
1846
|
+
this.app.use('/api/setup', setupRouter);
|
|
1847
|
+
// Data Export API - Export PostgreSQL tables to JSON
|
|
1848
|
+
const dataExportRouter = createDataExportRouter(this.requireAuth.bind(this), this.db);
|
|
1849
|
+
this.app.use('/api/admin/export', dataExportRouter);
|
|
1850
|
+
// Hot Reload API - Dashboard control for hot reload system
|
|
1851
|
+
const hotReloadRouter = createHotReloadRouter(this.requireAuth.bind(this));
|
|
1852
|
+
this.app.use('/api/reload', hotReloadRouter);
|
|
1853
|
+
// Hooks Management API - User-manageable custom hooks
|
|
1854
|
+
this.app.use('/api/hooks', this.requireAuth.bind(this), hooksRouter);
|
|
1855
|
+
// Also alias the thinking stream to /api/stream/thinking for backwards compat
|
|
1856
|
+
this.app.get('/api/stream/thinking', this.requireAuth.bind(this), (req, res) => {
|
|
1857
|
+
// Redirect to the live session thinking endpoint
|
|
1858
|
+
res.redirect('/api/live/thinking');
|
|
1859
|
+
});
|
|
1860
|
+
// Serve prompt console page
|
|
1861
|
+
this.app.get('/prompt', (req, res) => {
|
|
1862
|
+
res.sendFile(path.join(__dirname, 'public', 'prompt-console.html'));
|
|
1863
|
+
});
|
|
1864
|
+
// Serve terminal output page (legacy)
|
|
1865
|
+
this.app.get('/terminal-output', (req, res) => {
|
|
1866
|
+
res.sendFile(path.join(__dirname, 'public', 'terminal-output.html'));
|
|
1867
|
+
});
|
|
1868
|
+
// Serve new terminal emulator page with full ANSI support
|
|
1869
|
+
this.app.get('/terminal', (req, res) => {
|
|
1870
|
+
res.sendFile(path.join(__dirname, 'public', 'terminal.html'));
|
|
1871
|
+
});
|
|
1872
|
+
// Serve data export page
|
|
1873
|
+
this.app.get('/data-export', (req, res) => {
|
|
1874
|
+
res.sendFile(path.join(__dirname, 'public', 'data-export.html'));
|
|
1875
|
+
});
|
|
1876
|
+
// SPA catch-all route: serve React app for all non-API routes (React Router handles routing)
|
|
1877
|
+
this.app.get('*', (req, res) => {
|
|
1878
|
+
// Don't catch API routes, WebSocket, or specific pages
|
|
1879
|
+
if (req.path.startsWith('/api/') || req.path.startsWith('/ws/') ||
|
|
1880
|
+
req.path === '/prompt' || req.path === '/terminal' ||
|
|
1881
|
+
req.path === '/terminal-output' || req.path === '/health' ||
|
|
1882
|
+
req.path === '/data-export') {
|
|
1883
|
+
return res.status(404).json({ error: 'Not found' });
|
|
1884
|
+
}
|
|
1885
|
+
res.sendFile(path.join(__dirname, 'public', 'react-dist', 'index.html'));
|
|
1886
|
+
});
|
|
1887
|
+
logger.info('Phase 4-6 routes initialized: /api/prompt, /api/terminal, /api/claude');
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Setup WebSocket for real-time updates
|
|
1891
|
+
*/
|
|
1892
|
+
setupWebSocket() {
|
|
1893
|
+
this.wss.on('connection', (ws, req) => {
|
|
1894
|
+
// Check if this is a team member-specific WebSocket connection
|
|
1895
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
1896
|
+
const isTeamMemberWs = url.pathname === '/ws/team-members';
|
|
1897
|
+
const isTerminalWs = url.pathname === '/ws/terminal';
|
|
1898
|
+
logger.info({
|
|
1899
|
+
pathname: url.pathname,
|
|
1900
|
+
isTerminalWs,
|
|
1901
|
+
isTeamMemberWs,
|
|
1902
|
+
readyState: ws.readyState,
|
|
1903
|
+
readyStateLabel: ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][ws.readyState] || 'UNKNOWN',
|
|
1904
|
+
remoteAddress: req.socket?.remoteAddress,
|
|
1905
|
+
headers: {
|
|
1906
|
+
host: req.headers.host,
|
|
1907
|
+
origin: req.headers.origin,
|
|
1908
|
+
upgrade: req.headers.upgrade
|
|
1909
|
+
}
|
|
1910
|
+
}, '[WEBSERVER-WS-DEBUG] WebSocket connection established');
|
|
1911
|
+
if (isTeamMemberWs) {
|
|
1912
|
+
logger.info('[WEBSERVER-WS-DEBUG] Routing to setupTeamMemberWebSocket');
|
|
1913
|
+
this.setupTeamMemberWebSocket(ws);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
// Phase 5: Terminal WebSocket
|
|
1917
|
+
if (isTerminalWs) {
|
|
1918
|
+
logger.info('[WEBSERVER-WS-DEBUG] Routing to setupTerminalWebSocket');
|
|
1919
|
+
this.setupTerminalWebSocket(ws);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
logger.info('Dashboard WebSocket client connected');
|
|
1923
|
+
this.connectedClients.add(ws);
|
|
1924
|
+
ws.on('close', () => {
|
|
1925
|
+
logger.info('Dashboard WebSocket client disconnected');
|
|
1926
|
+
this.connectedClients.delete(ws);
|
|
1927
|
+
});
|
|
1928
|
+
ws.on('error', (error) => {
|
|
1929
|
+
logger.error({ error }, 'Dashboard WebSocket error');
|
|
1930
|
+
this.connectedClients.delete(ws);
|
|
1931
|
+
});
|
|
1932
|
+
// Send initial stats AFTER a delay to let mobile proxies fully establish the connection
|
|
1933
|
+
// Mobile carriers often have transparent proxies that kill WebSocket connections if data is sent too quickly
|
|
1934
|
+
setTimeout(() => {
|
|
1935
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1936
|
+
this.getStats().then(stats => {
|
|
1937
|
+
ws.send(JSON.stringify({ type: 'stats', data: stats }));
|
|
1938
|
+
}).catch(err => {
|
|
1939
|
+
logger.error({ err }, 'Error sending initial stats');
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
}, 1000);
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* TeamMember-specific WebSocket clients for live message streaming
|
|
1947
|
+
*/
|
|
1948
|
+
teamMemberWsClients = new Set();
|
|
1949
|
+
teamMemberMessageSubscriptions = new Map();
|
|
1950
|
+
setupTeamMemberEventForwarding() {
|
|
1951
|
+
if (!this.teamMemberTracker)
|
|
1952
|
+
return;
|
|
1953
|
+
const events = ['teamMember:registered', 'teamMember:status', 'teamMember:log', 'teamMember:tokens', 'teamMember:task'];
|
|
1954
|
+
for (const event of events) {
|
|
1955
|
+
this.teamMemberTracker.on(event, (data) => {
|
|
1956
|
+
this.broadcastUpdate(event.replace(':', '_'), data);
|
|
1957
|
+
for (const client of this.connectedClients) {
|
|
1958
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1959
|
+
client.send(JSON.stringify({ type: event, ...data }));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
const collabEvents = ['code:shared', 'feedback:given', 'message:sent'];
|
|
1965
|
+
for (const event of collabEvents) {
|
|
1966
|
+
this.teamMemberTracker.on(event, (data) => {
|
|
1967
|
+
this.broadcastUpdate(event.replace(':', '_'), data);
|
|
1968
|
+
for (const client of this.connectedClients) {
|
|
1969
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1970
|
+
client.send(JSON.stringify({ type: event, data }));
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
// Forward limit warnings from TeamMemberDeployment
|
|
1976
|
+
if (this.teamMemberDeployment) {
|
|
1977
|
+
this.teamMemberDeployment.on('teamMember:limit_warning', (data) => {
|
|
1978
|
+
this.broadcastUpdate('teamMember_limit_warning', data);
|
|
1979
|
+
for (const client of this.connectedClients) {
|
|
1980
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1981
|
+
client.send(JSON.stringify({ type: 'teamMember:limit_warning', teamMemberId: data.teamMemberId, warning: data.warning }));
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
this.teamMemberDeployment.on('teamMember:limit_acknowledged', (data) => {
|
|
1986
|
+
this.broadcastUpdate('teamMember_limit_acknowledged', data);
|
|
1987
|
+
for (const client of this.connectedClients) {
|
|
1988
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1989
|
+
client.send(JSON.stringify({ type: 'teamMember:limit_acknowledged', teamMemberId: data.teamMemberId, limitType: data.type, action: data.action }));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
// Forward team member responses to connected clients
|
|
1994
|
+
this.teamMemberDeployment.on('teamMember:response', (data) => {
|
|
1995
|
+
for (const client of this.connectedClients) {
|
|
1996
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1997
|
+
client.send(JSON.stringify({ type: 'teamMember:response', teamMemberId: data.teamMemberId, response: data.response }));
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
// Forward team member command events
|
|
2002
|
+
this.teamMemberDeployment.on('teamMember:command_sent', (data) => {
|
|
2003
|
+
for (const client of this.connectedClients) {
|
|
2004
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2005
|
+
client.send(JSON.stringify({ type: 'teamMember:command', teamMemberId: data.teamMemberId, command: data.command }));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Setup WebSocket connection for team member message streaming
|
|
2013
|
+
*/
|
|
2014
|
+
setupTeamMemberWebSocket(ws) {
|
|
2015
|
+
logger.info('Team Member WebSocket client connected');
|
|
2016
|
+
this.teamMemberWsClients.add(ws);
|
|
2017
|
+
this.teamMemberMessageSubscriptions.set(ws, new Set());
|
|
2018
|
+
ws.on('message', (data) => {
|
|
2019
|
+
try {
|
|
2020
|
+
const message = JSON.parse(data.toString());
|
|
2021
|
+
this.handleTeamMemberWsMessage(ws, message);
|
|
2022
|
+
}
|
|
2023
|
+
catch (error) {
|
|
2024
|
+
logger.error({ error }, 'Error parsing team member WebSocket message');
|
|
2025
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON message' }));
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
ws.on('close', () => {
|
|
2029
|
+
logger.info('Team Member WebSocket client disconnected');
|
|
2030
|
+
this.teamMemberWsClients.delete(ws);
|
|
2031
|
+
this.teamMemberMessageSubscriptions.delete(ws);
|
|
2032
|
+
});
|
|
2033
|
+
ws.on('error', (error) => {
|
|
2034
|
+
logger.error({ error }, 'Team Member WebSocket error');
|
|
2035
|
+
this.teamMemberWsClients.delete(ws);
|
|
2036
|
+
this.teamMemberMessageSubscriptions.delete(ws);
|
|
2037
|
+
});
|
|
2038
|
+
// Send initial active team members list AFTER a delay to let mobile proxies fully establish the connection
|
|
2039
|
+
// Mobile carriers often have transparent proxies that kill WebSocket connections if data is sent too quickly
|
|
2040
|
+
setTimeout(() => {
|
|
2041
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2042
|
+
this.getActiveTeamMemberSessions().then(teamMembers => {
|
|
2043
|
+
ws.send(JSON.stringify({ type: 'active_teamMembers', data: teamMembers }));
|
|
2044
|
+
}).catch(err => {
|
|
2045
|
+
logger.error({ err }, 'Error sending initial team members list');
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}, 1000);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Setup WebSocket connection for terminal output streaming (Phase 5)
|
|
2052
|
+
* Uses PTY streaming with full ANSI support for colors, formatting, etc.
|
|
2053
|
+
*/
|
|
2054
|
+
setupTerminalWebSocket(ws) {
|
|
2055
|
+
logger.info({
|
|
2056
|
+
readyState: ws.readyState,
|
|
2057
|
+
readyStateLabel: ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][ws.readyState] || 'UNKNOWN'
|
|
2058
|
+
}, '[WEBSERVER-WS-DEBUG] setupTerminalWebSocket called');
|
|
2059
|
+
try {
|
|
2060
|
+
// Use the new PTY streaming system with full ANSI support
|
|
2061
|
+
logger.info('[WEBSERVER-WS-DEBUG] About to call handleTerminalWebSocket...');
|
|
2062
|
+
handleTerminalWebSocket(ws, {});
|
|
2063
|
+
logger.info('[WEBSERVER-WS-DEBUG] handleTerminalWebSocket returned successfully');
|
|
2064
|
+
}
|
|
2065
|
+
catch (error) {
|
|
2066
|
+
logger.error({
|
|
2067
|
+
error,
|
|
2068
|
+
stack: error?.stack,
|
|
2069
|
+
message: error?.message
|
|
2070
|
+
}, '[WEBSERVER-WS-DEBUG] Error in handleTerminalWebSocket call');
|
|
2071
|
+
// Try to send error to client
|
|
2072
|
+
try {
|
|
2073
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2074
|
+
ws.send(JSON.stringify({
|
|
2075
|
+
type: 'error',
|
|
2076
|
+
message: 'Server error initializing terminal stream'
|
|
2077
|
+
}));
|
|
2078
|
+
ws.close(1011, 'Internal server error');
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
catch (sendError) {
|
|
2082
|
+
logger.error({ error: sendError }, '[WEBSERVER-WS-DEBUG] Failed to send error or close ws');
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Handle incoming WebSocket messages for team member streaming
|
|
2088
|
+
*/
|
|
2089
|
+
handleTeamMemberWsMessage(ws, message) {
|
|
2090
|
+
switch (message.type) {
|
|
2091
|
+
case 'subscribe':
|
|
2092
|
+
// Subscribe to messages from a specific session
|
|
2093
|
+
if (message.sessionId) {
|
|
2094
|
+
const subs = this.teamMemberMessageSubscriptions.get(ws);
|
|
2095
|
+
if (subs) {
|
|
2096
|
+
subs.add(message.sessionId);
|
|
2097
|
+
ws.send(JSON.stringify({ type: 'subscribed', sessionId: message.sessionId }));
|
|
2098
|
+
logger.debug({ sessionId: message.sessionId }, 'Client subscribed to session');
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
break;
|
|
2102
|
+
case 'unsubscribe':
|
|
2103
|
+
// Unsubscribe from a specific session
|
|
2104
|
+
if (message.sessionId) {
|
|
2105
|
+
const subs = this.teamMemberMessageSubscriptions.get(ws);
|
|
2106
|
+
if (subs) {
|
|
2107
|
+
subs.delete(message.sessionId);
|
|
2108
|
+
ws.send(JSON.stringify({ type: 'unsubscribed', sessionId: message.sessionId }));
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
break;
|
|
2112
|
+
case 'subscribe_all':
|
|
2113
|
+
// Subscribe to all team member messages
|
|
2114
|
+
const allSubs = this.teamMemberMessageSubscriptions.get(ws);
|
|
2115
|
+
if (allSubs) {
|
|
2116
|
+
allSubs.add('*');
|
|
2117
|
+
ws.send(JSON.stringify({ type: 'subscribed_all' }));
|
|
2118
|
+
}
|
|
2119
|
+
break;
|
|
2120
|
+
case 'get_active':
|
|
2121
|
+
// Request current active team members
|
|
2122
|
+
this.getActiveTeamMemberSessions().then(teamMembers => {
|
|
2123
|
+
ws.send(JSON.stringify({ type: 'active_teamMembers', data: teamMembers }));
|
|
2124
|
+
}).catch(err => {
|
|
2125
|
+
logger.error({ err }, 'Error fetching active team members');
|
|
2126
|
+
});
|
|
2127
|
+
break;
|
|
2128
|
+
case 'get_session_messages':
|
|
2129
|
+
// Request recent messages for a session
|
|
2130
|
+
if (message.sessionId) {
|
|
2131
|
+
this.getTeamMemberSessionMessages(message.sessionId, 50, 0).then(messages => {
|
|
2132
|
+
ws.send(JSON.stringify({
|
|
2133
|
+
type: 'session_messages',
|
|
2134
|
+
sessionId: message.sessionId,
|
|
2135
|
+
data: messages
|
|
2136
|
+
}));
|
|
2137
|
+
}).catch(err => {
|
|
2138
|
+
logger.error({ err }, 'Error fetching session messages');
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
break;
|
|
2142
|
+
default:
|
|
2143
|
+
ws.send(JSON.stringify({ type: 'error', message: `Unknown message type: ${message.type}` }));
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Broadcast team member message to subscribed WebSocket clients
|
|
2148
|
+
*/
|
|
2149
|
+
broadcastTeamMemberMessage(sessionId, message) {
|
|
2150
|
+
const payload = JSON.stringify({
|
|
2151
|
+
type: 'team_member_message',
|
|
2152
|
+
sessionId,
|
|
2153
|
+
data: message,
|
|
2154
|
+
timestamp: new Date().toISOString()
|
|
2155
|
+
});
|
|
2156
|
+
for (const [ws, subs] of this.teamMemberMessageSubscriptions) {
|
|
2157
|
+
if (ws.readyState === WebSocket.OPEN && (subs.has(sessionId) || subs.has('*'))) {
|
|
2158
|
+
ws.send(payload);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Broadcast team member status update to all team member WebSocket clients
|
|
2164
|
+
*/
|
|
2165
|
+
broadcastTeamMemberStatusUpdate(sessionId, status, teamMember) {
|
|
2166
|
+
const payload = JSON.stringify({
|
|
2167
|
+
type: 'teamMember_status',
|
|
2168
|
+
sessionId,
|
|
2169
|
+
status,
|
|
2170
|
+
data: teamMember,
|
|
2171
|
+
timestamp: new Date().toISOString()
|
|
2172
|
+
});
|
|
2173
|
+
for (const ws of this.teamMemberWsClients) {
|
|
2174
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
2175
|
+
ws.send(payload);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Broadcast message to all connected WebSocket clients
|
|
2181
|
+
*/
|
|
2182
|
+
broadcastUpdate(type, data) {
|
|
2183
|
+
const message = JSON.stringify({ type, data });
|
|
2184
|
+
for (const client of this.connectedClients) {
|
|
2185
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
2186
|
+
client.send(message);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Get dashboard statistics
|
|
2192
|
+
*/
|
|
2193
|
+
async getStats() {
|
|
2194
|
+
let totalMemories = 0;
|
|
2195
|
+
let totalSessions = 0;
|
|
2196
|
+
let totalFiles = 0;
|
|
2197
|
+
let totalSkills = 0;
|
|
2198
|
+
let activeTeamMembers = 0;
|
|
2199
|
+
try {
|
|
2200
|
+
if (this.db) {
|
|
2201
|
+
// PROJECT ISOLATION: Only count memories from current project
|
|
2202
|
+
const projectPath = getProjectPathForInsert();
|
|
2203
|
+
// Get memory count
|
|
2204
|
+
const memoryResult = await this.db.query('SELECT COUNT(*) as count FROM memories WHERE project_path = $1', [projectPath]);
|
|
2205
|
+
totalMemories = parseInt(memoryResult.rows[0]?.count || '0', 10);
|
|
2206
|
+
// Get session count (from memories with session tags)
|
|
2207
|
+
const sessionResult = await this.db.query("SELECT COUNT(DISTINCT COALESCE(metadata->>'sessionId', metadata->>'session_id')) as count FROM memories WHERE project_path = $1 AND (metadata->>'sessionId' IS NOT NULL OR metadata->>'session_id' IS NOT NULL)", [projectPath]);
|
|
2208
|
+
totalSessions = parseInt(sessionResult.rows[0]?.count || '0', 10);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
catch (error) {
|
|
2212
|
+
logger.debug({ error }, 'Error fetching database stats');
|
|
2213
|
+
}
|
|
2214
|
+
try {
|
|
2215
|
+
if (this.codebaseIndexer) {
|
|
2216
|
+
const stats = this.codebaseIndexer.getStats();
|
|
2217
|
+
totalFiles = stats.totalFiles;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
catch (error) {
|
|
2221
|
+
logger.debug({ error }, 'Error fetching codebase stats');
|
|
2222
|
+
}
|
|
2223
|
+
try {
|
|
2224
|
+
if (this.skillScanner) {
|
|
2225
|
+
const skills = this.skillScanner.getAllSkills();
|
|
2226
|
+
totalSkills = skills.length;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
catch (error) {
|
|
2230
|
+
logger.debug({ error }, 'Error fetching skills stats');
|
|
2231
|
+
}
|
|
2232
|
+
// Try to get team member count from coordination server
|
|
2233
|
+
try {
|
|
2234
|
+
const response = await fetch(`http://localhost:${this.config.coordinationPort}/teamMembers`);
|
|
2235
|
+
if (response.ok) {
|
|
2236
|
+
const data = await response.json();
|
|
2237
|
+
activeTeamMembers = data.teamMembers?.length || 0;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
catch (error) {
|
|
2241
|
+
logger.debug({ error }, 'Error fetching team member stats');
|
|
2242
|
+
}
|
|
2243
|
+
// Get memory manager stats if available
|
|
2244
|
+
let memoryStats;
|
|
2245
|
+
try {
|
|
2246
|
+
if (this.memoryManager) {
|
|
2247
|
+
const memStats = this.memoryManager.getStats();
|
|
2248
|
+
memoryStats = {
|
|
2249
|
+
heapUsedMB: Math.round(memStats.heapUsed / 1024 / 1024 * 100) / 100,
|
|
2250
|
+
heapTotalMB: Math.round(memStats.heapTotal / 1024 / 1024 * 100) / 100,
|
|
2251
|
+
maxHeapMB: Math.round(memStats.maxHeap / 1024 / 1024 * 100) / 100,
|
|
2252
|
+
usagePercent: Math.round(memStats.usagePercent * 10000) / 100,
|
|
2253
|
+
pressureLevel: memStats.pressureLevel,
|
|
2254
|
+
embeddingCacheSize: memStats.embeddingCacheSize,
|
|
2255
|
+
totalEvictions: memStats.totalEvictions,
|
|
2256
|
+
totalOverflowed: memStats.totalOverflowed
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
catch (error) {
|
|
2261
|
+
logger.debug({ error }, 'Error fetching memory manager stats');
|
|
2262
|
+
}
|
|
2263
|
+
return {
|
|
2264
|
+
totalMemories,
|
|
2265
|
+
totalSessions,
|
|
2266
|
+
totalFiles,
|
|
2267
|
+
totalSkills,
|
|
2268
|
+
activeTeamMembers,
|
|
2269
|
+
uptime: this.isRunning ? Date.now() - this.startTime : 0,
|
|
2270
|
+
memory: memoryStats
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Persist password change to environment file
|
|
2275
|
+
*/
|
|
2276
|
+
async persistPasswordToEnv(newPassword) {
|
|
2277
|
+
// Try common env file locations
|
|
2278
|
+
const envPaths = [
|
|
2279
|
+
this.envFilePath,
|
|
2280
|
+
path.join(process.cwd(), 'specmem.env'),
|
|
2281
|
+
path.join(process.cwd(), '.env'),
|
|
2282
|
+
path.join(__dirname, '../../specmem.env'),
|
|
2283
|
+
path.join(__dirname, '../../.env')
|
|
2284
|
+
].filter(Boolean);
|
|
2285
|
+
for (const envPath of envPaths) {
|
|
2286
|
+
try {
|
|
2287
|
+
const content = await fs.readFile(envPath, 'utf-8');
|
|
2288
|
+
// Check if this file has the dashboard password setting
|
|
2289
|
+
if (content.includes('SPECMEM_DASHBOARD_PASSWORD')) {
|
|
2290
|
+
const updatedContent = content.replace(/SPECMEM_DASHBOARD_PASSWORD=.*/, `SPECMEM_DASHBOARD_PASSWORD=${newPassword}`);
|
|
2291
|
+
await fs.writeFile(envPath, updatedContent, 'utf-8');
|
|
2292
|
+
logger.info({ envPath }, 'Password persisted to env file');
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
catch (error) {
|
|
2297
|
+
// File doesn't exist or not readable, try next
|
|
2298
|
+
logger.debug({ error, envPath }, 'Could not update env file');
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Memory configuration state
|
|
2305
|
+
*/
|
|
2306
|
+
memoryConfig = {
|
|
2307
|
+
memoryLimit: 100, // MB
|
|
2308
|
+
overflowTime: 24, // hours
|
|
2309
|
+
cacheSize: 500 // entries
|
|
2310
|
+
};
|
|
2311
|
+
/**
|
|
2312
|
+
* Cache statistics tracking
|
|
2313
|
+
*/
|
|
2314
|
+
cacheStats = {
|
|
2315
|
+
queryCacheSize: 0,
|
|
2316
|
+
queryCacheHits: 0,
|
|
2317
|
+
queryCacheMisses: 0,
|
|
2318
|
+
embeddingCacheSize: 0,
|
|
2319
|
+
embeddingCacheHits: 0,
|
|
2320
|
+
embeddingCacheMisses: 0,
|
|
2321
|
+
lastOverflow: null
|
|
2322
|
+
};
|
|
2323
|
+
/**
|
|
2324
|
+
* Get memory configuration
|
|
2325
|
+
*/
|
|
2326
|
+
async getMemoryConfig() {
|
|
2327
|
+
// Try to load from env file first
|
|
2328
|
+
const envPaths = [
|
|
2329
|
+
this.envFilePath,
|
|
2330
|
+
path.join(process.cwd(), 'specmem.env'),
|
|
2331
|
+
path.join(__dirname, '../../specmem.env')
|
|
2332
|
+
].filter(Boolean);
|
|
2333
|
+
for (const envPath of envPaths) {
|
|
2334
|
+
try {
|
|
2335
|
+
const content = await fs.readFile(envPath, 'utf-8');
|
|
2336
|
+
const memoryLimitMatch = content.match(/SPECMEM_MEMORY_LIMIT=(\d+)/);
|
|
2337
|
+
const overflowTimeMatch = content.match(/SPECMEM_OVERFLOW_TIME=(\d+)/);
|
|
2338
|
+
const cacheSizeMatch = content.match(/SPECMEM_CACHE_SIZE=(\d+)/);
|
|
2339
|
+
if (memoryLimitMatch) {
|
|
2340
|
+
this.memoryConfig.memoryLimit = parseInt(memoryLimitMatch[1], 10);
|
|
2341
|
+
}
|
|
2342
|
+
if (overflowTimeMatch) {
|
|
2343
|
+
this.memoryConfig.overflowTime = parseInt(overflowTimeMatch[1], 10);
|
|
2344
|
+
}
|
|
2345
|
+
if (cacheSizeMatch) {
|
|
2346
|
+
this.memoryConfig.cacheSize = parseInt(cacheSizeMatch[1], 10);
|
|
2347
|
+
}
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
catch (e) {
|
|
2351
|
+
// Continue to next path - this is expected when file doesn't exist
|
|
2352
|
+
logger.debug({ envPath, error: e }, 'env file not found at this path, trying next');
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return this.memoryConfig;
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Persist memory configuration to env file
|
|
2359
|
+
*/
|
|
2360
|
+
async persistMemoryConfig(config) {
|
|
2361
|
+
// Update in-memory config
|
|
2362
|
+
if (config.memoryLimit !== undefined)
|
|
2363
|
+
this.memoryConfig.memoryLimit = config.memoryLimit;
|
|
2364
|
+
if (config.overflowTime !== undefined)
|
|
2365
|
+
this.memoryConfig.overflowTime = config.overflowTime;
|
|
2366
|
+
if (config.cacheSize !== undefined)
|
|
2367
|
+
this.memoryConfig.cacheSize = config.cacheSize;
|
|
2368
|
+
const envPaths = [
|
|
2369
|
+
this.envFilePath,
|
|
2370
|
+
path.join(process.cwd(), 'specmem.env'),
|
|
2371
|
+
path.join(__dirname, '../../specmem.env')
|
|
2372
|
+
].filter(Boolean);
|
|
2373
|
+
for (const envPath of envPaths) {
|
|
2374
|
+
try {
|
|
2375
|
+
let content = await fs.readFile(envPath, 'utf-8');
|
|
2376
|
+
// Update or add memory limit
|
|
2377
|
+
if (content.includes('SPECMEM_MEMORY_LIMIT=')) {
|
|
2378
|
+
content = content.replace(/SPECMEM_MEMORY_LIMIT=\d+/, `SPECMEM_MEMORY_LIMIT=${this.memoryConfig.memoryLimit}`);
|
|
2379
|
+
}
|
|
2380
|
+
else {
|
|
2381
|
+
content += `\nSPECMEM_MEMORY_LIMIT=${this.memoryConfig.memoryLimit}`;
|
|
2382
|
+
}
|
|
2383
|
+
// Update or add overflow time
|
|
2384
|
+
if (content.includes('SPECMEM_OVERFLOW_TIME=')) {
|
|
2385
|
+
content = content.replace(/SPECMEM_OVERFLOW_TIME=\d+/, `SPECMEM_OVERFLOW_TIME=${this.memoryConfig.overflowTime}`);
|
|
2386
|
+
}
|
|
2387
|
+
else {
|
|
2388
|
+
content += `\nSPECMEM_OVERFLOW_TIME=${this.memoryConfig.overflowTime}`;
|
|
2389
|
+
}
|
|
2390
|
+
// Update or add cache size
|
|
2391
|
+
if (content.includes('SPECMEM_CACHE_SIZE=')) {
|
|
2392
|
+
content = content.replace(/SPECMEM_CACHE_SIZE=\d+/, `SPECMEM_CACHE_SIZE=${this.memoryConfig.cacheSize}`);
|
|
2393
|
+
}
|
|
2394
|
+
else {
|
|
2395
|
+
content += `\nSPECMEM_CACHE_SIZE=${this.memoryConfig.cacheSize}`;
|
|
2396
|
+
}
|
|
2397
|
+
await fs.writeFile(envPath, content, 'utf-8');
|
|
2398
|
+
logger.info({ envPath, config: this.memoryConfig }, 'Memory configuration persisted');
|
|
2399
|
+
return true;
|
|
2400
|
+
}
|
|
2401
|
+
catch (e) {
|
|
2402
|
+
// File not found or not readable, try next path
|
|
2403
|
+
logger.debug({ envPath, error: e }, 'couldnt save to env file, trying next');
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Get memory statistics for the dashboard
|
|
2411
|
+
* Integrates with the MemoryManager for real heap stats
|
|
2412
|
+
*/
|
|
2413
|
+
async getMemoryStats() {
|
|
2414
|
+
let totalMemories = 0;
|
|
2415
|
+
let estimatedUsageMB = 0;
|
|
2416
|
+
try {
|
|
2417
|
+
if (this.db) {
|
|
2418
|
+
// PROJECT ISOLATION: Only count memories from current project
|
|
2419
|
+
const projectPath = getProjectPathForInsert();
|
|
2420
|
+
// Get memory count
|
|
2421
|
+
const countResult = await this.db.query('SELECT COUNT(*) as count FROM memories WHERE project_path = $1', [projectPath]);
|
|
2422
|
+
totalMemories = parseInt(countResult.rows[0]?.count || '0', 10);
|
|
2423
|
+
// Estimate memory usage based on content size
|
|
2424
|
+
const sizeResult = await this.db.query(`
|
|
2425
|
+
SELECT COALESCE(SUM(LENGTH(content)), 0) as total_size
|
|
2426
|
+
FROM memories WHERE project_path = $1
|
|
2427
|
+
`, [projectPath]);
|
|
2428
|
+
const totalBytes = parseInt(sizeResult.rows[0]?.total_size || '0', 10);
|
|
2429
|
+
estimatedUsageMB = Math.round((totalBytes / 1024 / 1024) * 100) / 100;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
catch (error) {
|
|
2433
|
+
logger.debug({ error }, 'Error calculating memory stats');
|
|
2434
|
+
}
|
|
2435
|
+
// Get real heap stats from memory manager
|
|
2436
|
+
let heapStats = {
|
|
2437
|
+
usedMB: 0,
|
|
2438
|
+
totalMB: 0,
|
|
2439
|
+
maxMB: 100,
|
|
2440
|
+
usagePercent: 0,
|
|
2441
|
+
pressureLevel: 'normal',
|
|
2442
|
+
rssMB: 0,
|
|
2443
|
+
externalMB: 0
|
|
2444
|
+
};
|
|
2445
|
+
let overflowStats = {
|
|
2446
|
+
totalEvictions: 0,
|
|
2447
|
+
totalOverflowed: 0,
|
|
2448
|
+
lastGC: null
|
|
2449
|
+
};
|
|
2450
|
+
let embeddingCacheSize = this.cacheStats.embeddingCacheSize;
|
|
2451
|
+
try {
|
|
2452
|
+
if (this.memoryManager) {
|
|
2453
|
+
const memStats = this.memoryManager.getStats();
|
|
2454
|
+
heapStats = {
|
|
2455
|
+
usedMB: Math.round(memStats.heapUsed / 1024 / 1024 * 100) / 100,
|
|
2456
|
+
totalMB: Math.round(memStats.heapTotal / 1024 / 1024 * 100) / 100,
|
|
2457
|
+
maxMB: Math.round(memStats.maxHeap / 1024 / 1024 * 100) / 100,
|
|
2458
|
+
usagePercent: Math.round(memStats.usagePercent * 10000) / 100,
|
|
2459
|
+
pressureLevel: memStats.pressureLevel,
|
|
2460
|
+
rssMB: Math.round(memStats.rss / 1024 / 1024 * 100) / 100,
|
|
2461
|
+
externalMB: Math.round(memStats.external / 1024 / 1024 * 100) / 100
|
|
2462
|
+
};
|
|
2463
|
+
overflowStats = {
|
|
2464
|
+
totalEvictions: memStats.totalEvictions,
|
|
2465
|
+
totalOverflowed: memStats.totalOverflowed,
|
|
2466
|
+
lastGC: memStats.lastGC?.toISOString() || null
|
|
2467
|
+
};
|
|
2468
|
+
embeddingCacheSize = memStats.embeddingCacheSize;
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
// Fallback to process.memoryUsage if no memory manager
|
|
2472
|
+
const mem = process.memoryUsage();
|
|
2473
|
+
heapStats = {
|
|
2474
|
+
usedMB: Math.round(mem.heapUsed / 1024 / 1024 * 100) / 100,
|
|
2475
|
+
totalMB: Math.round(mem.heapTotal / 1024 / 1024 * 100) / 100,
|
|
2476
|
+
maxMB: 100, // Default limit
|
|
2477
|
+
usagePercent: Math.round((mem.heapUsed / (100 * 1024 * 1024)) * 10000) / 100,
|
|
2478
|
+
pressureLevel: 'unknown',
|
|
2479
|
+
rssMB: Math.round(mem.rss / 1024 / 1024 * 100) / 100,
|
|
2480
|
+
externalMB: Math.round(mem.external / 1024 / 1024 * 100) / 100
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
catch (error) {
|
|
2485
|
+
logger.debug({ error }, 'Error getting heap stats');
|
|
2486
|
+
}
|
|
2487
|
+
// Calculate cache hit rate
|
|
2488
|
+
const totalHits = this.cacheStats.queryCacheHits + this.cacheStats.embeddingCacheHits;
|
|
2489
|
+
const totalMisses = this.cacheStats.queryCacheMisses + this.cacheStats.embeddingCacheMisses;
|
|
2490
|
+
const cacheHitRate = totalHits + totalMisses > 0
|
|
2491
|
+
? Math.round((totalHits / (totalHits + totalMisses)) * 100)
|
|
2492
|
+
: 0;
|
|
2493
|
+
// Determine trend based on pressure level
|
|
2494
|
+
let trend = 'STABLE';
|
|
2495
|
+
if (heapStats.pressureLevel === 'emergency') {
|
|
2496
|
+
trend = 'CRITICAL';
|
|
2497
|
+
}
|
|
2498
|
+
else if (heapStats.pressureLevel === 'critical') {
|
|
2499
|
+
trend = 'UP';
|
|
2500
|
+
}
|
|
2501
|
+
else if (heapStats.pressureLevel === 'warning') {
|
|
2502
|
+
trend = 'RISING';
|
|
2503
|
+
}
|
|
2504
|
+
return {
|
|
2505
|
+
currentUsage: estimatedUsageMB,
|
|
2506
|
+
totalMemories,
|
|
2507
|
+
cacheHitRate,
|
|
2508
|
+
lastOverflow: this.cacheStats.lastOverflow?.toISOString() || overflowStats.lastGC,
|
|
2509
|
+
peakUsage: heapStats.usedMB, // Real peak from actual usage
|
|
2510
|
+
avgUsage: Math.round(heapStats.usedMB * 0.8 * 100) / 100,
|
|
2511
|
+
trend,
|
|
2512
|
+
queryCacheSize: this.cacheStats.queryCacheSize,
|
|
2513
|
+
embeddingCacheSize: embeddingCacheSize,
|
|
2514
|
+
heap: heapStats,
|
|
2515
|
+
overflow: overflowStats
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Trigger overflow cleanup based on configuration
|
|
2520
|
+
* PROJECT ISOLATED: Only cleans up current project's memories
|
|
2521
|
+
*/
|
|
2522
|
+
async triggerOverflowCleanup() {
|
|
2523
|
+
if (!this.db) {
|
|
2524
|
+
return { deleted: 0, message: 'Database not available' };
|
|
2525
|
+
}
|
|
2526
|
+
try {
|
|
2527
|
+
const cutoffTime = new Date();
|
|
2528
|
+
cutoffTime.setHours(cutoffTime.getHours() - this.memoryConfig.overflowTime);
|
|
2529
|
+
const projectPath = getProjectPathForInsert();
|
|
2530
|
+
// Delete memories older than the overflow time that haven't been accessed recently
|
|
2531
|
+
// PROJECT ISOLATED: Only delete from current project
|
|
2532
|
+
const result = await this.db.query(`
|
|
2533
|
+
DELETE FROM memories
|
|
2534
|
+
WHERE updated_at < $1
|
|
2535
|
+
AND importance NOT IN ('critical', 'high')
|
|
2536
|
+
AND access_count < 5
|
|
2537
|
+
AND project_path = $2
|
|
2538
|
+
RETURNING id
|
|
2539
|
+
`, [cutoffTime.toISOString(), projectPath]);
|
|
2540
|
+
const deletedCount = result.rowCount ?? 0;
|
|
2541
|
+
this.cacheStats.lastOverflow = new Date();
|
|
2542
|
+
logger.info({ deletedCount, cutoffTime, projectPath }, 'Overflow cleanup completed');
|
|
2543
|
+
return {
|
|
2544
|
+
deleted: deletedCount,
|
|
2545
|
+
message: `Cleaned up ${deletedCount} memories older than ${this.memoryConfig.overflowTime} hours`
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
catch (error) {
|
|
2549
|
+
logger.error({ error }, 'Overflow cleanup failed');
|
|
2550
|
+
throw error;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Emergency memory purge - deletes ALL memories
|
|
2555
|
+
* PROJECT ISOLATED: Only purges current project's memories
|
|
2556
|
+
*/
|
|
2557
|
+
async emergencyMemoryPurge() {
|
|
2558
|
+
if (!this.db) {
|
|
2559
|
+
return { deleted: 0, message: 'Database not available' };
|
|
2560
|
+
}
|
|
2561
|
+
try {
|
|
2562
|
+
const projectPath = getProjectPathForInsert();
|
|
2563
|
+
// Get count before deletion - only for current project
|
|
2564
|
+
const countResult = await this.db.query('SELECT COUNT(*) as count FROM memories WHERE project_path = $1', [projectPath]);
|
|
2565
|
+
const totalBefore = parseInt(countResult.rows[0]?.count || '0', 10);
|
|
2566
|
+
// Delete all memories for current project only
|
|
2567
|
+
await this.db.query('DELETE FROM memories WHERE project_path = $1', [projectPath]);
|
|
2568
|
+
// Reset cache stats
|
|
2569
|
+
this.cacheStats.queryCacheSize = 0;
|
|
2570
|
+
this.cacheStats.embeddingCacheSize = 0;
|
|
2571
|
+
this.cacheStats.lastOverflow = new Date();
|
|
2572
|
+
logger.warn({ deletedCount: totalBefore, projectPath }, 'Emergency memory purge executed for project');
|
|
2573
|
+
return {
|
|
2574
|
+
deleted: totalBefore,
|
|
2575
|
+
message: `Emergency purge complete. ${totalBefore} memories permanently deleted from current project.`
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
catch (error) {
|
|
2579
|
+
logger.error({ error }, 'Emergency purge failed');
|
|
2580
|
+
throw error;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Clear specific cache type
|
|
2585
|
+
*/
|
|
2586
|
+
async clearCache(type) {
|
|
2587
|
+
try {
|
|
2588
|
+
if (type === 'query') {
|
|
2589
|
+
this.cacheStats.queryCacheSize = 0;
|
|
2590
|
+
this.cacheStats.queryCacheHits = 0;
|
|
2591
|
+
this.cacheStats.queryCacheMisses = 0;
|
|
2592
|
+
logger.info('Query cache cleared');
|
|
2593
|
+
return { cleared: true, message: 'Query cache yeeted, clean slate fr' };
|
|
2594
|
+
}
|
|
2595
|
+
else if (type === 'embedding') {
|
|
2596
|
+
this.cacheStats.embeddingCacheSize = 0;
|
|
2597
|
+
this.cacheStats.embeddingCacheHits = 0;
|
|
2598
|
+
this.cacheStats.embeddingCacheMisses = 0;
|
|
2599
|
+
logger.info('Embedding cache cleared');
|
|
2600
|
+
return { cleared: true, message: 'Embedding cache wiped clean, let\'s go' };
|
|
2601
|
+
}
|
|
2602
|
+
return { cleared: false, message: 'Unknown cache type' };
|
|
2603
|
+
}
|
|
2604
|
+
catch (error) {
|
|
2605
|
+
logger.error({ error, type }, 'Failed to clear cache');
|
|
2606
|
+
throw error;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
getFileExtension(language) {
|
|
2610
|
+
const extMap = {
|
|
2611
|
+
typescript: 'ts', javascript: 'js', python: 'py', rust: 'rs',
|
|
2612
|
+
go: 'go', java: 'java', cpp: 'cpp', c: 'c', ruby: 'rb',
|
|
2613
|
+
php: 'php', sql: 'sql', bash: 'sh', yaml: 'yaml', json: 'json',
|
|
2614
|
+
markdown: 'md', html: 'html', css: 'css', text: 'txt'
|
|
2615
|
+
};
|
|
2616
|
+
return extMap[language] || 'txt';
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Get memories with optional search (supports text and semantic search)
|
|
2620
|
+
*/
|
|
2621
|
+
async getMemories(search, limit = 50, offset = 0) {
|
|
2622
|
+
if (!this.db) {
|
|
2623
|
+
return { memories: [], total: 0 };
|
|
2624
|
+
}
|
|
2625
|
+
// PROJECT ISOLATION: Filter by project_path
|
|
2626
|
+
const projectPath = getProjectPathForInsert();
|
|
2627
|
+
// If no search, return paginated results
|
|
2628
|
+
if (!search) {
|
|
2629
|
+
const query = `
|
|
2630
|
+
SELECT id, content, tags, metadata, importance, memory_type, created_at, updated_at, access_count
|
|
2631
|
+
FROM memories
|
|
2632
|
+
WHERE project_path = $1
|
|
2633
|
+
ORDER BY created_at DESC
|
|
2634
|
+
LIMIT $2 OFFSET $3
|
|
2635
|
+
`;
|
|
2636
|
+
const countQuery = `SELECT COUNT(*) as count FROM memories WHERE project_path = $1`;
|
|
2637
|
+
const [memoriesResult, countResult] = await Promise.all([
|
|
2638
|
+
this.db.query(query, [projectPath, limit, offset]),
|
|
2639
|
+
this.db.query(countQuery, [projectPath])
|
|
2640
|
+
]);
|
|
2641
|
+
return {
|
|
2642
|
+
memories: memoriesResult.rows,
|
|
2643
|
+
total: parseInt(countResult.rows[0]?.count || '0', 10)
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
// Try semantic search first if embedding provider is available
|
|
2647
|
+
if (this.embeddingProvider) {
|
|
2648
|
+
try {
|
|
2649
|
+
const embedding = await this.embeddingProvider.generateEmbedding(search);
|
|
2650
|
+
const embeddingStr = `[${embedding.join(',')}]`;
|
|
2651
|
+
// Hybrid search: combine vector similarity with text matching
|
|
2652
|
+
// PROJECT ISOLATION: Filter by project_path
|
|
2653
|
+
const query = `
|
|
2654
|
+
WITH vector_matches AS (
|
|
2655
|
+
SELECT
|
|
2656
|
+
id, content, tags, metadata, importance, memory_type,
|
|
2657
|
+
created_at, updated_at, access_count,
|
|
2658
|
+
1 - (embedding <=> $1::vector) AS similarity
|
|
2659
|
+
FROM memories
|
|
2660
|
+
WHERE project_path = $6
|
|
2661
|
+
AND embedding IS NOT NULL
|
|
2662
|
+
AND (embedding <=> $1::vector) < 0.5
|
|
2663
|
+
ORDER BY embedding <=> $1::vector
|
|
2664
|
+
LIMIT $2
|
|
2665
|
+
),
|
|
2666
|
+
text_matches AS (
|
|
2667
|
+
SELECT
|
|
2668
|
+
id, content, tags, metadata, importance, memory_type,
|
|
2669
|
+
created_at, updated_at, access_count,
|
|
2670
|
+
ts_rank(content_tsv, plainto_tsquery('english', $3)) AS similarity
|
|
2671
|
+
FROM memories
|
|
2672
|
+
WHERE project_path = $6
|
|
2673
|
+
AND (content_tsv @@ plainto_tsquery('english', $3)
|
|
2674
|
+
OR content ILIKE $4
|
|
2675
|
+
OR $3 = ANY(tags))
|
|
2676
|
+
ORDER BY similarity DESC
|
|
2677
|
+
LIMIT $2
|
|
2678
|
+
),
|
|
2679
|
+
combined AS (
|
|
2680
|
+
SELECT * FROM vector_matches
|
|
2681
|
+
UNION
|
|
2682
|
+
SELECT * FROM text_matches
|
|
2683
|
+
)
|
|
2684
|
+
SELECT DISTINCT ON (id) *
|
|
2685
|
+
FROM combined
|
|
2686
|
+
ORDER BY id, similarity DESC
|
|
2687
|
+
LIMIT $2 OFFSET $5
|
|
2688
|
+
`;
|
|
2689
|
+
const countQuery = `
|
|
2690
|
+
SELECT COUNT(DISTINCT id) as count FROM (
|
|
2691
|
+
SELECT id FROM memories
|
|
2692
|
+
WHERE project_path = $4 AND embedding IS NOT NULL AND (embedding <=> $1::vector) < 0.5
|
|
2693
|
+
UNION
|
|
2694
|
+
SELECT id FROM memories
|
|
2695
|
+
WHERE project_path = $4
|
|
2696
|
+
AND (content_tsv @@ plainto_tsquery('english', $2)
|
|
2697
|
+
OR content ILIKE $3
|
|
2698
|
+
OR $2 = ANY(tags))
|
|
2699
|
+
) combined
|
|
2700
|
+
`;
|
|
2701
|
+
const [memoriesResult, countResult] = await Promise.all([
|
|
2702
|
+
this.db.query(query, [embeddingStr, limit, search, `%${search}%`, offset, projectPath]),
|
|
2703
|
+
this.db.query(countQuery, [embeddingStr, search, `%${search}%`, projectPath])
|
|
2704
|
+
]);
|
|
2705
|
+
return {
|
|
2706
|
+
memories: memoriesResult.rows,
|
|
2707
|
+
total: parseInt(countResult.rows[0]?.count || '0', 10),
|
|
2708
|
+
searchType: 'hybrid'
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
catch (error) {
|
|
2712
|
+
logger.warn({ error }, 'Semantic search failed, falling back to text search');
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
// Fallback to text search with full-text search support
|
|
2716
|
+
// PROJECT ISOLATION: Filter by project_path
|
|
2717
|
+
const query = `
|
|
2718
|
+
SELECT id, content, tags, metadata, importance, memory_type, created_at, updated_at, access_count,
|
|
2719
|
+
CASE
|
|
2720
|
+
WHEN content_tsv @@ plainto_tsquery('english', $1) THEN ts_rank(content_tsv, plainto_tsquery('english', $1))
|
|
2721
|
+
ELSE 0
|
|
2722
|
+
END AS rank
|
|
2723
|
+
FROM memories
|
|
2724
|
+
WHERE project_path = $5
|
|
2725
|
+
AND (content_tsv @@ plainto_tsquery('english', $1)
|
|
2726
|
+
OR content ILIKE $2
|
|
2727
|
+
OR $1 = ANY(tags))
|
|
2728
|
+
ORDER BY rank DESC, created_at DESC
|
|
2729
|
+
LIMIT $3 OFFSET $4
|
|
2730
|
+
`;
|
|
2731
|
+
const countQuery = `
|
|
2732
|
+
SELECT COUNT(*) as count FROM memories
|
|
2733
|
+
WHERE project_path = $3
|
|
2734
|
+
AND (content_tsv @@ plainto_tsquery('english', $1)
|
|
2735
|
+
OR content ILIKE $2
|
|
2736
|
+
OR $1 = ANY(tags))
|
|
2737
|
+
`;
|
|
2738
|
+
const [memoriesResult, countResult] = await Promise.all([
|
|
2739
|
+
this.db.query(query, [search, `%${search}%`, limit, offset, projectPath]),
|
|
2740
|
+
this.db.query(countQuery, [search, `%${search}%`, projectPath])
|
|
2741
|
+
]);
|
|
2742
|
+
return {
|
|
2743
|
+
memories: memoriesResult.rows,
|
|
2744
|
+
total: parseInt(countResult.rows[0]?.count || '0', 10),
|
|
2745
|
+
searchType: 'text'
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
/**
|
|
2749
|
+
* Get memories for Camera Roll mode with zoom-based similarity threshold
|
|
2750
|
+
* Returns results with similarity scores for drilldown functionality
|
|
2751
|
+
*/
|
|
2752
|
+
async getCameraRollMemories(search, similarityThreshold, limit, offset = 0) {
|
|
2753
|
+
if (!this.db) {
|
|
2754
|
+
return { memories: [], total: 0, searchType: 'none' };
|
|
2755
|
+
}
|
|
2756
|
+
// PROJECT ISOLATION: Filter by project_path
|
|
2757
|
+
const projectPath = getProjectPathForInsert();
|
|
2758
|
+
// If no search query, return recent memories with default similarity
|
|
2759
|
+
if (!search) {
|
|
2760
|
+
const query = `
|
|
2761
|
+
SELECT id, content, tags, metadata, importance, memory_type,
|
|
2762
|
+
created_at, updated_at, access_count,
|
|
2763
|
+
0.5 as similarity
|
|
2764
|
+
FROM memories
|
|
2765
|
+
WHERE project_path = $1
|
|
2766
|
+
ORDER BY created_at DESC
|
|
2767
|
+
LIMIT $2 OFFSET $3
|
|
2768
|
+
`;
|
|
2769
|
+
const countQuery = `SELECT COUNT(*) as count FROM memories WHERE project_path = $1`;
|
|
2770
|
+
const [memoriesResult, countResult] = await Promise.all([
|
|
2771
|
+
this.db.query(query, [projectPath, limit, offset]),
|
|
2772
|
+
this.db.query(countQuery, [projectPath])
|
|
2773
|
+
]);
|
|
2774
|
+
return {
|
|
2775
|
+
memories: memoriesResult.rows,
|
|
2776
|
+
total: parseInt(countResult.rows[0]?.count || '0', 10),
|
|
2777
|
+
searchType: 'recent'
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
// Try semantic search with zoom-appropriate threshold
|
|
2781
|
+
if (this.embeddingProvider) {
|
|
2782
|
+
try {
|
|
2783
|
+
const embedding = await this.embeddingProvider.generateEmbedding(search);
|
|
2784
|
+
const embeddingStr = `[${embedding.join(',')}]`;
|
|
2785
|
+
// Calculate distance threshold from similarity threshold
|
|
2786
|
+
// similarity = 1 - distance, so distance = 1 - similarity
|
|
2787
|
+
const distanceThreshold = 1 - similarityThreshold;
|
|
2788
|
+
// Vector search with zoom-based threshold
|
|
2789
|
+
// PROJECT ISOLATION: Filter by project_path
|
|
2790
|
+
const query = `
|
|
2791
|
+
SELECT
|
|
2792
|
+
id, content, tags, metadata, importance, memory_type,
|
|
2793
|
+
created_at, updated_at, access_count,
|
|
2794
|
+
1 - (embedding <=> $1::vector) AS similarity
|
|
2795
|
+
FROM memories
|
|
2796
|
+
WHERE project_path = $5
|
|
2797
|
+
AND embedding IS NOT NULL
|
|
2798
|
+
AND (embedding <=> $1::vector) < $2
|
|
2799
|
+
ORDER BY embedding <=> $1::vector
|
|
2800
|
+
LIMIT $3 OFFSET $4
|
|
2801
|
+
`;
|
|
2802
|
+
const countQuery = `
|
|
2803
|
+
SELECT COUNT(*) as count
|
|
2804
|
+
FROM memories
|
|
2805
|
+
WHERE project_path = $3
|
|
2806
|
+
AND embedding IS NOT NULL
|
|
2807
|
+
AND (embedding <=> $1::vector) < $2
|
|
2808
|
+
`;
|
|
2809
|
+
const [memoriesResult, countResult] = await Promise.all([
|
|
2810
|
+
this.db.query(query, [embeddingStr, distanceThreshold, limit, offset, projectPath]),
|
|
2811
|
+
this.db.query(countQuery, [embeddingStr, distanceThreshold, projectPath])
|
|
2812
|
+
]);
|
|
2813
|
+
return {
|
|
2814
|
+
memories: memoriesResult.rows,
|
|
2815
|
+
total: parseInt(countResult.rows[0]?.count || '0', 10),
|
|
2816
|
+
searchType: 'hybrid'
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
catch (error) {
|
|
2820
|
+
logger.warn({ error }, 'Camera roll semantic search failed, falling back to text search');
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
// Fallback to text search
|
|
2824
|
+
const query = `
|
|
2825
|
+
SELECT id, content, tags, metadata, importance, memory_type,
|
|
2826
|
+
created_at, updated_at, access_count,
|
|
2827
|
+
CASE
|
|
2828
|
+
WHEN content_tsv @@ plainto_tsquery('english', $1)
|
|
2829
|
+
THEN ts_rank(content_tsv, plainto_tsquery('english', $1))
|
|
2830
|
+
ELSE 0.3
|
|
2831
|
+
END AS similarity
|
|
2832
|
+
FROM memories
|
|
2833
|
+
WHERE content_tsv @@ plainto_tsquery('english', $1)
|
|
2834
|
+
OR content ILIKE $2
|
|
2835
|
+
OR $1 = ANY(tags)
|
|
2836
|
+
ORDER BY similarity DESC, created_at DESC
|
|
2837
|
+
LIMIT $3 OFFSET $4
|
|
2838
|
+
`;
|
|
2839
|
+
const countQuery = `
|
|
2840
|
+
SELECT COUNT(*) as count FROM memories
|
|
2841
|
+
WHERE content_tsv @@ plainto_tsquery('english', $1)
|
|
2842
|
+
OR content ILIKE $2
|
|
2843
|
+
OR $1 = ANY(tags)
|
|
2844
|
+
`;
|
|
2845
|
+
const [memoriesResult, countResult] = await Promise.all([
|
|
2846
|
+
this.db.query(query, [search, `%${search}%`, limit, offset]),
|
|
2847
|
+
this.db.query(countQuery, [search, `%${search}%`])
|
|
2848
|
+
]);
|
|
2849
|
+
return {
|
|
2850
|
+
memories: memoriesResult.rows,
|
|
2851
|
+
total: parseInt(countResult.rows[0]?.count || '0', 10),
|
|
2852
|
+
searchType: 'text'
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Get a single memory by ID with full details
|
|
2857
|
+
*/
|
|
2858
|
+
async getMemoryById(id) {
|
|
2859
|
+
if (!this.db) {
|
|
2860
|
+
return null;
|
|
2861
|
+
}
|
|
2862
|
+
const result = await this.db.query(`
|
|
2863
|
+
SELECT
|
|
2864
|
+
id,
|
|
2865
|
+
content,
|
|
2866
|
+
tags,
|
|
2867
|
+
metadata,
|
|
2868
|
+
importance,
|
|
2869
|
+
memory_type,
|
|
2870
|
+
created_at,
|
|
2871
|
+
updated_at,
|
|
2872
|
+
access_count,
|
|
2873
|
+
expires_at,
|
|
2874
|
+
embedding[1:5] as embedding_preview
|
|
2875
|
+
FROM memories
|
|
2876
|
+
WHERE id = $1
|
|
2877
|
+
`, [id]);
|
|
2878
|
+
if (result.rowCount === 0) {
|
|
2879
|
+
return null;
|
|
2880
|
+
}
|
|
2881
|
+
const row = result.rows[0];
|
|
2882
|
+
// Update access count for this memory
|
|
2883
|
+
await this.db.query(`
|
|
2884
|
+
UPDATE memories
|
|
2885
|
+
SET access_count = access_count + 1, updated_at = NOW()
|
|
2886
|
+
WHERE id = $1
|
|
2887
|
+
`, [id]);
|
|
2888
|
+
return {
|
|
2889
|
+
id: row.id,
|
|
2890
|
+
content: row.content,
|
|
2891
|
+
tags: row.tags || [],
|
|
2892
|
+
metadata: row.metadata || {},
|
|
2893
|
+
importance: row.importance,
|
|
2894
|
+
memory_type: row.memory_type,
|
|
2895
|
+
created_at: row.created_at,
|
|
2896
|
+
updated_at: row.updated_at,
|
|
2897
|
+
access_count: row.access_count + 1,
|
|
2898
|
+
expires_at: row.expires_at,
|
|
2899
|
+
embedding_preview: row.embedding_preview
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Delete a memory by ID
|
|
2904
|
+
* PROJECT ISOLATED: Only deletes from current project
|
|
2905
|
+
*/
|
|
2906
|
+
async deleteMemory(id) {
|
|
2907
|
+
if (!this.db) {
|
|
2908
|
+
throw new Error('Database not available');
|
|
2909
|
+
}
|
|
2910
|
+
const projectPath = getProjectPathForInsert();
|
|
2911
|
+
await this.db.query('DELETE FROM memories WHERE id = $1 AND project_path = $2', [id, projectPath]);
|
|
2912
|
+
this.broadcastUpdate('memory_deleted', { id });
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Bulk delete memories based on criteria
|
|
2916
|
+
* PROJECT ISOLATED: Only deletes from current project
|
|
2917
|
+
*/
|
|
2918
|
+
async bulkDeleteMemories(criteria) {
|
|
2919
|
+
if (!this.db) {
|
|
2920
|
+
throw new Error('Database not available');
|
|
2921
|
+
}
|
|
2922
|
+
const conditions = [];
|
|
2923
|
+
const values = [];
|
|
2924
|
+
let paramIndex = 1;
|
|
2925
|
+
// PROJECT ISOLATION: Always filter by project_path first
|
|
2926
|
+
const projectPath = getProjectPathForInsert();
|
|
2927
|
+
conditions.push(`project_path = $${paramIndex}`);
|
|
2928
|
+
values.push(projectPath);
|
|
2929
|
+
paramIndex++;
|
|
2930
|
+
// Delete by IDs
|
|
2931
|
+
if (criteria.ids && criteria.ids.length > 0) {
|
|
2932
|
+
conditions.push(`id = ANY($${paramIndex})`);
|
|
2933
|
+
values.push(criteria.ids);
|
|
2934
|
+
paramIndex++;
|
|
2935
|
+
}
|
|
2936
|
+
// Delete older than date
|
|
2937
|
+
if (criteria.olderThan) {
|
|
2938
|
+
conditions.push(`created_at < $${paramIndex}`);
|
|
2939
|
+
values.push(criteria.olderThan);
|
|
2940
|
+
paramIndex++;
|
|
2941
|
+
}
|
|
2942
|
+
// Delete by tags (memories having any of these tags)
|
|
2943
|
+
if (criteria.tags && criteria.tags.length > 0) {
|
|
2944
|
+
conditions.push(`tags && $${paramIndex}`);
|
|
2945
|
+
values.push(criteria.tags);
|
|
2946
|
+
paramIndex++;
|
|
2947
|
+
}
|
|
2948
|
+
// Delete only expired memories
|
|
2949
|
+
if (criteria.expiredOnly) {
|
|
2950
|
+
conditions.push('expires_at IS NOT NULL AND expires_at < NOW()');
|
|
2951
|
+
}
|
|
2952
|
+
// We always have project_path, but need at least one other criterion
|
|
2953
|
+
if (conditions.length === 1) {
|
|
2954
|
+
throw new Error('At least one deletion criterion required (besides project filter)');
|
|
2955
|
+
}
|
|
2956
|
+
const query = `DELETE FROM memories WHERE ${conditions.join(' AND ')} RETURNING id`;
|
|
2957
|
+
const result = await this.db.query(query, values);
|
|
2958
|
+
const deletedCount = result.rowCount ?? 0;
|
|
2959
|
+
if (deletedCount > 0) {
|
|
2960
|
+
this.broadcastUpdate('memories_bulk_deleted', {
|
|
2961
|
+
count: deletedCount,
|
|
2962
|
+
criteria
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
logger.info({ deletedCount, criteria, projectPath }, 'Bulk delete completed');
|
|
2966
|
+
return { deleted: deletedCount };
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Get sessions with detailed information
|
|
2970
|
+
*/
|
|
2971
|
+
async getSessions() {
|
|
2972
|
+
if (!this.db) {
|
|
2973
|
+
return [];
|
|
2974
|
+
}
|
|
2975
|
+
// Get detailed session information including memory types and importance distribution
|
|
2976
|
+
// Get unique sessions with deduplication
|
|
2977
|
+
// The DISTINCT ON ensures we get unique session_ids and avoid any edge cases
|
|
2978
|
+
const result = await this.db.query(`
|
|
2979
|
+
WITH session_memories AS (
|
|
2980
|
+
SELECT
|
|
2981
|
+
COALESCE(metadata->>'sessionId', metadata->>'session_id') as session_id,
|
|
2982
|
+
id,
|
|
2983
|
+
memory_type,
|
|
2984
|
+
importance,
|
|
2985
|
+
tags,
|
|
2986
|
+
created_at,
|
|
2987
|
+
content,
|
|
2988
|
+
metadata
|
|
2989
|
+
FROM memories
|
|
2990
|
+
WHERE (metadata->>'sessionId' IS NOT NULL AND metadata->>'sessionId' != '')
|
|
2991
|
+
OR (metadata->>'session_id' IS NOT NULL AND metadata->>'session_id' != '')
|
|
2992
|
+
),
|
|
2993
|
+
session_aggregates AS (
|
|
2994
|
+
SELECT
|
|
2995
|
+
session_id,
|
|
2996
|
+
MIN(created_at) as started_at,
|
|
2997
|
+
MAX(created_at) as last_activity,
|
|
2998
|
+
COUNT(*) as memory_count,
|
|
2999
|
+
COUNT(*) as message_count,
|
|
3000
|
+
COUNT(DISTINCT memory_type) as memory_types_used,
|
|
3001
|
+
ARRAY_AGG(DISTINCT memory_type) as memory_types,
|
|
3002
|
+
ARRAY_AGG(DISTINCT importance) as importance_levels,
|
|
3003
|
+
MAX(metadata->>'project') as project,
|
|
3004
|
+
MAX(metadata->>'workingDirectory') as working_directory,
|
|
3005
|
+
EXTRACT(EPOCH FROM (MAX(created_at) - MIN(created_at)))/60 as duration_minutes
|
|
3006
|
+
FROM session_memories
|
|
3007
|
+
WHERE session_id IS NOT NULL AND session_id != ''
|
|
3008
|
+
GROUP BY session_id
|
|
3009
|
+
)
|
|
3010
|
+
SELECT
|
|
3011
|
+
session_id,
|
|
3012
|
+
started_at,
|
|
3013
|
+
last_activity,
|
|
3014
|
+
memory_count,
|
|
3015
|
+
message_count,
|
|
3016
|
+
memory_types_used,
|
|
3017
|
+
memory_types,
|
|
3018
|
+
importance_levels,
|
|
3019
|
+
project,
|
|
3020
|
+
working_directory,
|
|
3021
|
+
duration_minutes
|
|
3022
|
+
FROM session_aggregates
|
|
3023
|
+
ORDER BY last_activity DESC
|
|
3024
|
+
LIMIT 100
|
|
3025
|
+
`);
|
|
3026
|
+
return result.rows;
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Get codebase files with content search support
|
|
3030
|
+
*/
|
|
3031
|
+
async getCodebaseFiles(filePath, search) {
|
|
3032
|
+
if (!this.codebaseIndexer) {
|
|
3033
|
+
return { files: [], stats: {} };
|
|
3034
|
+
}
|
|
3035
|
+
const stats = this.codebaseIndexer.getStats();
|
|
3036
|
+
// Get all files
|
|
3037
|
+
const allFiles = this.codebaseIndexer.getAllFiles();
|
|
3038
|
+
// Filter by search term or path
|
|
3039
|
+
let files = allFiles;
|
|
3040
|
+
let searchType;
|
|
3041
|
+
if (search) {
|
|
3042
|
+
const searchLower = search.toLowerCase();
|
|
3043
|
+
// Check if search should include file content
|
|
3044
|
+
const includeContent = search.startsWith('content:') || search.startsWith('code:');
|
|
3045
|
+
const searchTerm = includeContent ? search.replace(/^(content:|code:)/, '').trim() : search;
|
|
3046
|
+
const searchTermLower = searchTerm.toLowerCase();
|
|
3047
|
+
if (includeContent) {
|
|
3048
|
+
// Content search - search within file contents
|
|
3049
|
+
searchType = 'content';
|
|
3050
|
+
files = allFiles.filter(f => f.content.toLowerCase().includes(searchTermLower)).slice(0, 50);
|
|
3051
|
+
// Extract matching lines for context
|
|
3052
|
+
const filesWithMatches = files.map(f => {
|
|
3053
|
+
const lines = f.content.split('\n');
|
|
3054
|
+
const matchingLines = [];
|
|
3055
|
+
lines.forEach((line, index) => {
|
|
3056
|
+
if (line.toLowerCase().includes(searchTermLower)) {
|
|
3057
|
+
matchingLines.push({
|
|
3058
|
+
lineNumber: index + 1,
|
|
3059
|
+
content: line.trim().slice(0, 200)
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
});
|
|
3063
|
+
return {
|
|
3064
|
+
path: f.filePath,
|
|
3065
|
+
name: f.fileName,
|
|
3066
|
+
language: f.language,
|
|
3067
|
+
lines: f.lineCount,
|
|
3068
|
+
size: f.sizeBytes,
|
|
3069
|
+
lastModified: f.lastModified,
|
|
3070
|
+
matches: matchingLines.slice(0, 5), // Top 5 matches
|
|
3071
|
+
matchCount: matchingLines.length
|
|
3072
|
+
};
|
|
3073
|
+
});
|
|
3074
|
+
return {
|
|
3075
|
+
files: filesWithMatches,
|
|
3076
|
+
stats,
|
|
3077
|
+
searchType
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
else {
|
|
3081
|
+
// Path/name search
|
|
3082
|
+
searchType = 'path';
|
|
3083
|
+
files = allFiles.filter(f => f.filePath.toLowerCase().includes(searchLower) ||
|
|
3084
|
+
f.fileName.toLowerCase().includes(searchLower) ||
|
|
3085
|
+
f.language.toLowerCase().includes(searchLower)).slice(0, 100);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
else if (filePath) {
|
|
3089
|
+
// Filter by path prefix
|
|
3090
|
+
files = allFiles.filter(f => f.filePath.startsWith(filePath));
|
|
3091
|
+
}
|
|
3092
|
+
else {
|
|
3093
|
+
// Return most recently modified files
|
|
3094
|
+
files = [...allFiles]
|
|
3095
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime())
|
|
3096
|
+
.slice(0, 100);
|
|
3097
|
+
}
|
|
3098
|
+
// Map to simpler format for frontend
|
|
3099
|
+
const mappedFiles = files.map(f => ({
|
|
3100
|
+
path: f.filePath,
|
|
3101
|
+
name: f.fileName,
|
|
3102
|
+
language: f.language,
|
|
3103
|
+
lines: f.lineCount,
|
|
3104
|
+
size: f.sizeBytes,
|
|
3105
|
+
lastModified: f.lastModified
|
|
3106
|
+
}));
|
|
3107
|
+
return { files: mappedFiles, stats, searchType };
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Get file content from the codebase indexer
|
|
3111
|
+
*/
|
|
3112
|
+
async getFileContent(filePath) {
|
|
3113
|
+
if (!this.codebaseIndexer) {
|
|
3114
|
+
return null;
|
|
3115
|
+
}
|
|
3116
|
+
const file = this.codebaseIndexer.getFile(filePath);
|
|
3117
|
+
if (!file) {
|
|
3118
|
+
return null;
|
|
3119
|
+
}
|
|
3120
|
+
return {
|
|
3121
|
+
path: file.filePath,
|
|
3122
|
+
name: file.fileName,
|
|
3123
|
+
language: file.language,
|
|
3124
|
+
content: file.content,
|
|
3125
|
+
lines: file.lineCount,
|
|
3126
|
+
size: file.sizeBytes
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Get skills
|
|
3131
|
+
*/
|
|
3132
|
+
async getSkills() {
|
|
3133
|
+
if (!this.skillScanner) {
|
|
3134
|
+
return { skills: [], categories: [] };
|
|
3135
|
+
}
|
|
3136
|
+
const skills = this.skillScanner.getAllSkills();
|
|
3137
|
+
const categories = this.skillScanner.getCategories();
|
|
3138
|
+
return {
|
|
3139
|
+
skills: skills.map(s => ({
|
|
3140
|
+
id: s.id,
|
|
3141
|
+
name: s.name,
|
|
3142
|
+
category: s.category,
|
|
3143
|
+
description: s.description,
|
|
3144
|
+
path: s.filePath,
|
|
3145
|
+
size: s.content.length,
|
|
3146
|
+
content: s.content // Include content so frontend can display/edit it
|
|
3147
|
+
})),
|
|
3148
|
+
categories
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Reload skills
|
|
3153
|
+
*/
|
|
3154
|
+
async reloadSkills() {
|
|
3155
|
+
if (!this.skillScanner) {
|
|
3156
|
+
throw new Error('Skill scanner not available');
|
|
3157
|
+
}
|
|
3158
|
+
await this.skillScanner.scan();
|
|
3159
|
+
this.broadcastUpdate('skills_reloaded', await this.getSkills());
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Get active team members from discovery service (SpecMem-based)
|
|
3163
|
+
*/
|
|
3164
|
+
async getTeamMembers() {
|
|
3165
|
+
try {
|
|
3166
|
+
// Get team members from discovery service (SpecMem heartbeat-based)
|
|
3167
|
+
if (this.dashboardDiscovery) {
|
|
3168
|
+
const discovered = await this.dashboardDiscovery.getActiveTeamMembers(120000); // 2 min expiry
|
|
3169
|
+
return discovered.map((d) => ({
|
|
3170
|
+
id: d.teamMemberId,
|
|
3171
|
+
name: d.teamMemberId,
|
|
3172
|
+
type: d.teamMemberType,
|
|
3173
|
+
connected: true,
|
|
3174
|
+
lastSeen: d.lastHeartbeat
|
|
3175
|
+
}));
|
|
3176
|
+
}
|
|
3177
|
+
// Fallback to coordination server if discovery not available
|
|
3178
|
+
const response = await fetch(`http://localhost:${this.config.coordinationPort}/teamMembers`);
|
|
3179
|
+
if (response.ok) {
|
|
3180
|
+
const data = await response.json();
|
|
3181
|
+
return data.teamMembers || [];
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
catch (error) {
|
|
3185
|
+
logger.debug({ error }, 'Error fetching team members');
|
|
3186
|
+
}
|
|
3187
|
+
return [];
|
|
3188
|
+
}
|
|
3189
|
+
// ==================== TEAM_MEMBER COMMUNICATION DASHBOARD METHODS ====================
|
|
3190
|
+
/**
|
|
3191
|
+
* Get all currently active team member sessions from database
|
|
3192
|
+
*/
|
|
3193
|
+
async getActiveTeamMemberSessions() {
|
|
3194
|
+
if (!this.db) {
|
|
3195
|
+
return { sessions: [], count: 0 };
|
|
3196
|
+
}
|
|
3197
|
+
try {
|
|
3198
|
+
const result = await this.db.query(`
|
|
3199
|
+
SELECT
|
|
3200
|
+
id,
|
|
3201
|
+
team_member_id,
|
|
3202
|
+
team_member_name,
|
|
3203
|
+
team_member_type,
|
|
3204
|
+
status,
|
|
3205
|
+
started_at,
|
|
3206
|
+
last_heartbeat,
|
|
3207
|
+
current_task,
|
|
3208
|
+
working_directory,
|
|
3209
|
+
project_name,
|
|
3210
|
+
message_count,
|
|
3211
|
+
tool_calls,
|
|
3212
|
+
errors_count,
|
|
3213
|
+
tokens_used,
|
|
3214
|
+
metadata,
|
|
3215
|
+
capabilities
|
|
3216
|
+
FROM team_member_sessions
|
|
3217
|
+
WHERE status IN ('active', 'idle', 'busy')
|
|
3218
|
+
AND last_heartbeat > NOW() - INTERVAL '5 minutes'
|
|
3219
|
+
ORDER BY last_heartbeat DESC
|
|
3220
|
+
`);
|
|
3221
|
+
return {
|
|
3222
|
+
sessions: result.rows,
|
|
3223
|
+
count: result.rowCount ?? 0
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
catch (error) {
|
|
3227
|
+
logger.debug({ error }, 'Error fetching active team member sessions');
|
|
3228
|
+
return { sessions: [], count: 0 };
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Get team member session history with pagination and filtering
|
|
3233
|
+
*/
|
|
3234
|
+
async getTeamMemberSessionHistory(limit, offset, teamMemberType, status) {
|
|
3235
|
+
if (!this.db) {
|
|
3236
|
+
return { sessions: [], total: 0, limit, offset };
|
|
3237
|
+
}
|
|
3238
|
+
try {
|
|
3239
|
+
const conditions = [];
|
|
3240
|
+
const values = [];
|
|
3241
|
+
let paramIndex = 1;
|
|
3242
|
+
if (teamMemberType) {
|
|
3243
|
+
conditions.push(`team_member_type = $${paramIndex}`);
|
|
3244
|
+
values.push(teamMemberType);
|
|
3245
|
+
paramIndex++;
|
|
3246
|
+
}
|
|
3247
|
+
if (status) {
|
|
3248
|
+
conditions.push(`status = $${paramIndex}`);
|
|
3249
|
+
values.push(status);
|
|
3250
|
+
paramIndex++;
|
|
3251
|
+
}
|
|
3252
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
3253
|
+
// Get total count
|
|
3254
|
+
const countResult = await this.db.query(`SELECT COUNT(*) as count FROM team_member_sessions ${whereClause}`, values);
|
|
3255
|
+
const total = parseInt(countResult.rows[0]?.count || '0', 10);
|
|
3256
|
+
// Get paginated results
|
|
3257
|
+
values.push(limit, offset);
|
|
3258
|
+
const result = await this.db.query(`
|
|
3259
|
+
SELECT
|
|
3260
|
+
id,
|
|
3261
|
+
team_member_id,
|
|
3262
|
+
team_member_name,
|
|
3263
|
+
team_member_type,
|
|
3264
|
+
status,
|
|
3265
|
+
started_at,
|
|
3266
|
+
ended_at,
|
|
3267
|
+
last_heartbeat,
|
|
3268
|
+
current_task,
|
|
3269
|
+
project_name,
|
|
3270
|
+
message_count,
|
|
3271
|
+
tool_calls,
|
|
3272
|
+
errors_count,
|
|
3273
|
+
tokens_used,
|
|
3274
|
+
EXTRACT(EPOCH FROM (COALESCE(ended_at, NOW()) - started_at)) as duration_seconds
|
|
3275
|
+
FROM team_member_sessions
|
|
3276
|
+
${whereClause}
|
|
3277
|
+
ORDER BY started_at DESC
|
|
3278
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
3279
|
+
`, values);
|
|
3280
|
+
return {
|
|
3281
|
+
sessions: result.rows,
|
|
3282
|
+
total,
|
|
3283
|
+
limit,
|
|
3284
|
+
offset
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
catch (error) {
|
|
3288
|
+
logger.debug({ error }, 'Error fetching team member session history');
|
|
3289
|
+
return { sessions: [], total: 0, limit, offset };
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Get detailed information about a specific team member session
|
|
3294
|
+
*/
|
|
3295
|
+
async getTeamMemberSessionDetails(sessionId, includeMessages = true, messageLimit = 100) {
|
|
3296
|
+
if (!this.db) {
|
|
3297
|
+
return null;
|
|
3298
|
+
}
|
|
3299
|
+
try {
|
|
3300
|
+
// Get session details
|
|
3301
|
+
const sessionResult = await this.db.query(`
|
|
3302
|
+
SELECT
|
|
3303
|
+
s.*,
|
|
3304
|
+
EXTRACT(EPOCH FROM (COALESCE(ended_at, NOW()) - started_at)) as duration_seconds,
|
|
3305
|
+
(SELECT COUNT(*) FROM team_member_messages WHERE session_id = s.id) as total_messages,
|
|
3306
|
+
(SELECT COUNT(*) FROM team_member_messages WHERE session_id = s.id AND message_type = 'tool_call') as total_tool_calls,
|
|
3307
|
+
(SELECT COUNT(*) FROM team_member_messages WHERE session_id = s.id AND is_error = true) as total_errors
|
|
3308
|
+
FROM team_member_sessions s
|
|
3309
|
+
WHERE s.id = $1
|
|
3310
|
+
`, [sessionId]);
|
|
3311
|
+
if (sessionResult.rowCount === 0) {
|
|
3312
|
+
return null;
|
|
3313
|
+
}
|
|
3314
|
+
const session = sessionResult.rows[0];
|
|
3315
|
+
const response = { session };
|
|
3316
|
+
// Get messages if requested
|
|
3317
|
+
if (includeMessages) {
|
|
3318
|
+
const messagesResult = await this.db.query(`
|
|
3319
|
+
SELECT
|
|
3320
|
+
id,
|
|
3321
|
+
message_type,
|
|
3322
|
+
direction,
|
|
3323
|
+
sequence_number,
|
|
3324
|
+
content_preview as content,
|
|
3325
|
+
tool_name,
|
|
3326
|
+
tool_duration_ms,
|
|
3327
|
+
role,
|
|
3328
|
+
importance,
|
|
3329
|
+
input_tokens,
|
|
3330
|
+
output_tokens,
|
|
3331
|
+
is_error,
|
|
3332
|
+
error_message,
|
|
3333
|
+
timestamp
|
|
3334
|
+
FROM team_member_messages
|
|
3335
|
+
WHERE session_id = $1
|
|
3336
|
+
ORDER BY sequence_number DESC
|
|
3337
|
+
LIMIT $2
|
|
3338
|
+
`, [sessionId, messageLimit]);
|
|
3339
|
+
response.messages = messagesResult.rows;
|
|
3340
|
+
response.messageCount = messagesResult.rowCount ?? 0;
|
|
3341
|
+
}
|
|
3342
|
+
return response;
|
|
3343
|
+
}
|
|
3344
|
+
catch (error) {
|
|
3345
|
+
logger.debug({ error }, 'Error fetching session details');
|
|
3346
|
+
return null;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Get messages for a specific session with pagination
|
|
3351
|
+
*/
|
|
3352
|
+
async getTeamMemberSessionMessages(sessionId, limit = 50, offset = 0, messageType) {
|
|
3353
|
+
if (!this.db) {
|
|
3354
|
+
return { messages: [], total: 0, limit, offset };
|
|
3355
|
+
}
|
|
3356
|
+
try {
|
|
3357
|
+
const conditions = ['session_id = $1'];
|
|
3358
|
+
const values = [sessionId];
|
|
3359
|
+
let paramIndex = 2;
|
|
3360
|
+
if (messageType) {
|
|
3361
|
+
conditions.push(`message_type = $${paramIndex}`);
|
|
3362
|
+
values.push(messageType);
|
|
3363
|
+
paramIndex++;
|
|
3364
|
+
}
|
|
3365
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
3366
|
+
// Get total count
|
|
3367
|
+
const countResult = await this.db.query(`SELECT COUNT(*) as count FROM team_member_messages ${whereClause}`, values);
|
|
3368
|
+
const total = parseInt(countResult.rows[0]?.count || '0', 10);
|
|
3369
|
+
// Get paginated results
|
|
3370
|
+
values.push(limit, offset);
|
|
3371
|
+
const result = await this.db.query(`
|
|
3372
|
+
SELECT
|
|
3373
|
+
id,
|
|
3374
|
+
message_type,
|
|
3375
|
+
direction,
|
|
3376
|
+
sequence_number,
|
|
3377
|
+
content,
|
|
3378
|
+
tool_name,
|
|
3379
|
+
tool_input,
|
|
3380
|
+
tool_output,
|
|
3381
|
+
tool_error,
|
|
3382
|
+
tool_duration_ms,
|
|
3383
|
+
role,
|
|
3384
|
+
importance,
|
|
3385
|
+
input_tokens,
|
|
3386
|
+
output_tokens,
|
|
3387
|
+
estimated_cost_cents,
|
|
3388
|
+
is_error,
|
|
3389
|
+
error_code,
|
|
3390
|
+
error_message,
|
|
3391
|
+
timestamp
|
|
3392
|
+
FROM team_member_messages
|
|
3393
|
+
${whereClause}
|
|
3394
|
+
ORDER BY sequence_number DESC
|
|
3395
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
3396
|
+
`, values);
|
|
3397
|
+
return {
|
|
3398
|
+
messages: result.rows,
|
|
3399
|
+
total,
|
|
3400
|
+
limit,
|
|
3401
|
+
offset
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
catch (error) {
|
|
3405
|
+
logger.debug({ error }, 'Error fetching session messages');
|
|
3406
|
+
return { messages: [], total: 0, limit, offset };
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Get team member deployments with pagination and filtering
|
|
3411
|
+
*/
|
|
3412
|
+
async getTeamMemberDeployments(limit, offset, status, environment) {
|
|
3413
|
+
if (!this.db) {
|
|
3414
|
+
return { deployments: [], total: 0, limit, offset };
|
|
3415
|
+
}
|
|
3416
|
+
try {
|
|
3417
|
+
const conditions = [];
|
|
3418
|
+
const values = [];
|
|
3419
|
+
let paramIndex = 1;
|
|
3420
|
+
if (status) {
|
|
3421
|
+
conditions.push(`status = $${paramIndex}`);
|
|
3422
|
+
values.push(status);
|
|
3423
|
+
paramIndex++;
|
|
3424
|
+
}
|
|
3425
|
+
if (environment) {
|
|
3426
|
+
conditions.push(`environment = $${paramIndex}`);
|
|
3427
|
+
values.push(environment);
|
|
3428
|
+
paramIndex++;
|
|
3429
|
+
}
|
|
3430
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
3431
|
+
// Get total count
|
|
3432
|
+
const countResult = await this.db.query(`SELECT COUNT(*) as count FROM team_member_deployments ${whereClause}`, values);
|
|
3433
|
+
const total = parseInt(countResult.rows[0]?.count || '0', 10);
|
|
3434
|
+
// Get paginated results
|
|
3435
|
+
values.push(limit, offset);
|
|
3436
|
+
const result = await this.db.query(`
|
|
3437
|
+
SELECT
|
|
3438
|
+
id,
|
|
3439
|
+
deployment_name,
|
|
3440
|
+
deployment_type,
|
|
3441
|
+
environment,
|
|
3442
|
+
team_member_count,
|
|
3443
|
+
status,
|
|
3444
|
+
health,
|
|
3445
|
+
started_at,
|
|
3446
|
+
completed_at,
|
|
3447
|
+
task_description,
|
|
3448
|
+
success,
|
|
3449
|
+
result_summary,
|
|
3450
|
+
actual_tokens_used,
|
|
3451
|
+
actual_cost_cents,
|
|
3452
|
+
tags,
|
|
3453
|
+
EXTRACT(EPOCH FROM (COALESCE(completed_at, NOW()) - started_at)) as duration_seconds
|
|
3454
|
+
FROM team_member_deployments
|
|
3455
|
+
${whereClause}
|
|
3456
|
+
ORDER BY created_at DESC
|
|
3457
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
3458
|
+
`, values);
|
|
3459
|
+
return {
|
|
3460
|
+
deployments: result.rows,
|
|
3461
|
+
total,
|
|
3462
|
+
limit,
|
|
3463
|
+
offset
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
catch (error) {
|
|
3467
|
+
logger.debug({ error }, 'Error fetching deployments');
|
|
3468
|
+
return { deployments: [], total: 0, limit, offset };
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
/**
|
|
3472
|
+
* Get aggregate statistics for team members
|
|
3473
|
+
*/
|
|
3474
|
+
async getTeamMemberStats() {
|
|
3475
|
+
if (!this.db) {
|
|
3476
|
+
return {
|
|
3477
|
+
activeSessions: 0,
|
|
3478
|
+
totalSessions: 0,
|
|
3479
|
+
totalMessages: 0,
|
|
3480
|
+
totalToolCalls: 0,
|
|
3481
|
+
totalTokens: 0,
|
|
3482
|
+
avgSessionDuration: 0,
|
|
3483
|
+
errorRate: 0,
|
|
3484
|
+
topTeamMemberTypes: [],
|
|
3485
|
+
recentActivity: []
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
try {
|
|
3489
|
+
// Get basic counts
|
|
3490
|
+
const countsResult = await this.db.query(`
|
|
3491
|
+
SELECT
|
|
3492
|
+
(SELECT COUNT(*) FROM team_member_sessions WHERE status IN ('active', 'idle', 'busy')) as active_sessions,
|
|
3493
|
+
(SELECT COUNT(*) FROM team_member_sessions) as total_sessions,
|
|
3494
|
+
(SELECT COUNT(*) FROM team_member_messages) as total_messages,
|
|
3495
|
+
(SELECT COUNT(*) FROM team_member_messages WHERE message_type = 'tool_call') as total_tool_calls,
|
|
3496
|
+
(SELECT COALESCE(SUM(tokens_used), 0) FROM team_member_sessions) as total_tokens,
|
|
3497
|
+
(SELECT AVG(EXTRACT(EPOCH FROM (COALESCE(ended_at, NOW()) - started_at)))
|
|
3498
|
+
FROM team_member_sessions WHERE started_at IS NOT NULL) as avg_duration,
|
|
3499
|
+
(SELECT COUNT(*)::FLOAT / NULLIF(COUNT(*) FILTER (WHERE NOT is_error), 0) * 100
|
|
3500
|
+
FROM team_member_messages) as error_rate
|
|
3501
|
+
`);
|
|
3502
|
+
const counts = countsResult.rows[0] || {};
|
|
3503
|
+
// Get top team member types
|
|
3504
|
+
const typesResult = await this.db.query(`
|
|
3505
|
+
SELECT team_member_type as type, COUNT(*) as count
|
|
3506
|
+
FROM team_member_sessions
|
|
3507
|
+
GROUP BY team_member_type
|
|
3508
|
+
ORDER BY count DESC
|
|
3509
|
+
LIMIT 5
|
|
3510
|
+
`);
|
|
3511
|
+
// Get recent activity (last 24 hours by hour)
|
|
3512
|
+
const activityResult = await this.db.query(`
|
|
3513
|
+
SELECT
|
|
3514
|
+
DATE_TRUNC('hour', s.started_at) as hour,
|
|
3515
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
3516
|
+
COUNT(m.id) as messages
|
|
3517
|
+
FROM team_member_sessions s
|
|
3518
|
+
LEFT JOIN team_member_messages m ON m.session_id = s.id
|
|
3519
|
+
AND m.timestamp >= NOW() - INTERVAL '24 hours'
|
|
3520
|
+
WHERE s.started_at >= NOW() - INTERVAL '24 hours'
|
|
3521
|
+
GROUP BY DATE_TRUNC('hour', s.started_at)
|
|
3522
|
+
ORDER BY hour DESC
|
|
3523
|
+
LIMIT 24
|
|
3524
|
+
`);
|
|
3525
|
+
return {
|
|
3526
|
+
activeSessions: parseInt(counts.active_sessions || '0', 10),
|
|
3527
|
+
totalSessions: parseInt(counts.total_sessions || '0', 10),
|
|
3528
|
+
totalMessages: parseInt(counts.total_messages || '0', 10),
|
|
3529
|
+
totalToolCalls: parseInt(counts.total_tool_calls || '0', 10),
|
|
3530
|
+
totalTokens: parseInt(counts.total_tokens || '0', 10),
|
|
3531
|
+
avgSessionDuration: parseFloat(counts.avg_duration || '0'),
|
|
3532
|
+
errorRate: parseFloat(counts.error_rate || '0'),
|
|
3533
|
+
topTeamMemberTypes: typesResult.rows.map((r) => ({
|
|
3534
|
+
type: r.type,
|
|
3535
|
+
count: parseInt(r.count, 10)
|
|
3536
|
+
})),
|
|
3537
|
+
recentActivity: activityResult.rows.map((r) => ({
|
|
3538
|
+
hour: r.hour,
|
|
3539
|
+
sessions: parseInt(r.sessions, 10),
|
|
3540
|
+
messages: parseInt(r.messages, 10)
|
|
3541
|
+
}))
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
catch (error) {
|
|
3545
|
+
logger.debug({ error }, 'Error fetching team member stats');
|
|
3546
|
+
return {
|
|
3547
|
+
activeSessions: 0,
|
|
3548
|
+
totalSessions: 0,
|
|
3549
|
+
totalMessages: 0,
|
|
3550
|
+
totalToolCalls: 0,
|
|
3551
|
+
totalTokens: 0,
|
|
3552
|
+
avgSessionDuration: 0,
|
|
3553
|
+
errorRate: 0,
|
|
3554
|
+
topTeamMemberTypes: [],
|
|
3555
|
+
recentActivity: []
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Set database manager
|
|
3561
|
+
*/
|
|
3562
|
+
setDatabase(db) {
|
|
3563
|
+
this.db = db;
|
|
3564
|
+
}
|
|
3565
|
+
/**
|
|
3566
|
+
* Set skill scanner
|
|
3567
|
+
*/
|
|
3568
|
+
setSkillScanner(scanner) {
|
|
3569
|
+
this.skillScanner = scanner;
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Set codebase indexer
|
|
3573
|
+
*/
|
|
3574
|
+
setCodebaseIndexer(indexer) {
|
|
3575
|
+
this.codebaseIndexer = indexer;
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* Set embedding provider for semantic search
|
|
3579
|
+
*/
|
|
3580
|
+
setEmbeddingProvider(provider) {
|
|
3581
|
+
this.embeddingProvider = provider;
|
|
3582
|
+
}
|
|
3583
|
+
/**
|
|
3584
|
+
* Set memory manager for heap monitoring
|
|
3585
|
+
*/
|
|
3586
|
+
setMemoryManager(manager) {
|
|
3587
|
+
this.memoryManager = manager;
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* Set embedding overflow handler
|
|
3591
|
+
*/
|
|
3592
|
+
setEmbeddingOverflowHandler(handler) {
|
|
3593
|
+
this.embeddingOverflowHandler = handler;
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Set path to env file for password persistence
|
|
3597
|
+
*/
|
|
3598
|
+
setEnvFilePath(envPath) {
|
|
3599
|
+
this.envFilePath = envPath;
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Start the server with port retry logic
|
|
3603
|
+
* Will try multiple ports if the base port is in use
|
|
3604
|
+
*/
|
|
3605
|
+
async start() {
|
|
3606
|
+
if (this.isRunning) {
|
|
3607
|
+
logger.warn('Dashboard server already running');
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
// Try to get database
|
|
3611
|
+
try {
|
|
3612
|
+
// Build database config from environment variables
|
|
3613
|
+
const dbConfig = {
|
|
3614
|
+
host: process.env['SPECMEM_DB_HOST'] || 'localhost',
|
|
3615
|
+
port: parseInt(process.env['SPECMEM_DB_PORT'] || '5432', 10),
|
|
3616
|
+
database: process.env['SPECMEM_DB_NAME'] || 'specmem_westayunprofessional',
|
|
3617
|
+
user: process.env['SPECMEM_DB_USER'] || 'specmem_westayunprofessional',
|
|
3618
|
+
password: process.env['SPECMEM_DB_PASSWORD'] || '',
|
|
3619
|
+
maxConnections: parseInt(process.env['SPECMEM_DB_MAX_CONNECTIONS'] || '20', 10),
|
|
3620
|
+
idleTimeout: 30000,
|
|
3621
|
+
connectionTimeout: 5000
|
|
3622
|
+
};
|
|
3623
|
+
this.db = getDatabase(dbConfig);
|
|
3624
|
+
if (this.db) {
|
|
3625
|
+
await this.db.initialize(); // sets up schema isolation
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
catch (error) {
|
|
3629
|
+
logger.warn('Database not available for dashboard');
|
|
3630
|
+
}
|
|
3631
|
+
// Initialize PostgreSQL session store if database is available
|
|
3632
|
+
if (this.db) {
|
|
3633
|
+
try {
|
|
3634
|
+
const store = await createSessionStore(this.db, {
|
|
3635
|
+
tableName: 'dashboard_sessions',
|
|
3636
|
+
cleanupIntervalMs: 15 * 60 * 1000, // 15 minutes
|
|
3637
|
+
pruneOnStart: true
|
|
3638
|
+
});
|
|
3639
|
+
if (store) {
|
|
3640
|
+
this.sessionStore = store;
|
|
3641
|
+
// Reconfigure session middleware with PostgreSQL store
|
|
3642
|
+
const cookieSecure = process.env.SPECMEM_COOKIE_SECURE === 'true';
|
|
3643
|
+
this.app.use(session({
|
|
3644
|
+
store: this.sessionStore,
|
|
3645
|
+
secret: this.config.sessionSecret,
|
|
3646
|
+
resave: false,
|
|
3647
|
+
saveUninitialized: false,
|
|
3648
|
+
cookie: {
|
|
3649
|
+
secure: cookieSecure,
|
|
3650
|
+
httpOnly: true,
|
|
3651
|
+
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
3652
|
+
}
|
|
3653
|
+
}));
|
|
3654
|
+
logger.info('PostgreSQL session store initialized - no more memory leaks!');
|
|
3655
|
+
}
|
|
3656
|
+
else {
|
|
3657
|
+
logger.warn('Using in-memory session store - sessions will not persist across restarts');
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
catch (error) {
|
|
3661
|
+
logger.warn({ error }, 'Failed to initialize PostgreSQL session store - using in-memory store');
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
else {
|
|
3665
|
+
logger.warn('Using in-memory session store - database not available');
|
|
3666
|
+
}
|
|
3667
|
+
// Try to get skill scanner
|
|
3668
|
+
try {
|
|
3669
|
+
this.skillScanner = getSkillScanner();
|
|
3670
|
+
}
|
|
3671
|
+
catch (error) {
|
|
3672
|
+
logger.warn('Skill scanner not available for dashboard');
|
|
3673
|
+
}
|
|
3674
|
+
// Try to get codebase indexer
|
|
3675
|
+
try {
|
|
3676
|
+
this.codebaseIndexer = getCodebaseIndexer();
|
|
3677
|
+
}
|
|
3678
|
+
catch (error) {
|
|
3679
|
+
logger.warn('Codebase indexer not available for dashboard');
|
|
3680
|
+
}
|
|
3681
|
+
// Try to get memory manager
|
|
3682
|
+
try {
|
|
3683
|
+
this.memoryManager = getMemoryManager();
|
|
3684
|
+
logger.info('Memory manager connected to dashboard');
|
|
3685
|
+
}
|
|
3686
|
+
catch (error) {
|
|
3687
|
+
logger.warn('Memory manager not available for dashboard');
|
|
3688
|
+
}
|
|
3689
|
+
// Initialize team member tracker, deployment, and history manager
|
|
3690
|
+
try {
|
|
3691
|
+
this.teamMemberTracker = getTeamMemberTracker();
|
|
3692
|
+
if (this.db) {
|
|
3693
|
+
this.teamMemberTracker.setDatabase(this.db.pool);
|
|
3694
|
+
}
|
|
3695
|
+
this.teamMemberDeployment = getTeamMemberDeployment();
|
|
3696
|
+
this.teamMemberHistoryManager = getTeamMemberHistoryManager();
|
|
3697
|
+
// Initialize Task team member logger
|
|
3698
|
+
if (this.db) {
|
|
3699
|
+
initializeTaskTeamMemberLogger(this.db);
|
|
3700
|
+
logger.info('Task team member logger initialized - Code team members will be tracked');
|
|
3701
|
+
}
|
|
3702
|
+
if (this.db) {
|
|
3703
|
+
this.teamMemberHistoryManager.setDatabase(this.db.pool);
|
|
3704
|
+
}
|
|
3705
|
+
this.setupTeamMemberEventForwarding();
|
|
3706
|
+
logger.info('Team Member tracker, deployment, and history manager initialized');
|
|
3707
|
+
}
|
|
3708
|
+
catch (error) {
|
|
3709
|
+
logger.warn({ error }, 'Team Member tracker/deployment/history not available');
|
|
3710
|
+
}
|
|
3711
|
+
// BUG FIX (Team Member 2): Initialize dashboard communicator and discovery for SpecMem-based team members
|
|
3712
|
+
try {
|
|
3713
|
+
const dashboardTeamMemberId = `dashboard-${crypto.randomUUID().substring(0, 8)}`;
|
|
3714
|
+
this.dashboardCommunicator = createTeamMemberCommunicator(dashboardTeamMemberId);
|
|
3715
|
+
this.dashboardDiscovery = createTeamMemberDiscovery(dashboardTeamMemberId, 'dashboard', 'overseer', {
|
|
3716
|
+
heartbeatIntervalMs: 60000, // Dashboard heartbeats less frequently
|
|
3717
|
+
teamMemberExpiryMs: 120000 // 2 minute expiry for discovered team members
|
|
3718
|
+
});
|
|
3719
|
+
// Start discovery but don't block on it
|
|
3720
|
+
this.dashboardDiscovery.start().catch((err) => {
|
|
3721
|
+
logger.debug({ error: err }, 'Dashboard discovery start warning (non-critical)');
|
|
3722
|
+
});
|
|
3723
|
+
logger.info({ teamMemberId: dashboardTeamMemberId }, 'Dashboard communicator and discovery initialized');
|
|
3724
|
+
}
|
|
3725
|
+
catch (error) {
|
|
3726
|
+
logger.debug({ error }, 'Dashboard communicator/discovery not available (SpecMem may not be running)');
|
|
3727
|
+
}
|
|
3728
|
+
// Initialize Memory Recall API routes
|
|
3729
|
+
if (this.db) {
|
|
3730
|
+
try {
|
|
3731
|
+
const memoryRecallRouter = createMemoryRecallRouter(this.db);
|
|
3732
|
+
this.app.use('/api/memory', (req, res, next) => {
|
|
3733
|
+
// Apply auth middleware to memory recall routes
|
|
3734
|
+
const sess = req.session;
|
|
3735
|
+
if (sess?.authenticated) {
|
|
3736
|
+
next();
|
|
3737
|
+
}
|
|
3738
|
+
else {
|
|
3739
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
3740
|
+
}
|
|
3741
|
+
}, memoryRecallRouter);
|
|
3742
|
+
logger.info('Memory Recall API routes initialized');
|
|
3743
|
+
}
|
|
3744
|
+
catch (error) {
|
|
3745
|
+
logger.warn({ error }, 'Failed to initialize Memory Recall API');
|
|
3746
|
+
}
|
|
3747
|
+
// Initialize Team Member History API routes
|
|
3748
|
+
try {
|
|
3749
|
+
const teamMemberHistoryRouter = createTeamMemberHistoryRouter(this.db);
|
|
3750
|
+
this.app.use('/api/teamMembers', (req, res, next) => {
|
|
3751
|
+
// Apply auth middleware to team member history routes
|
|
3752
|
+
const sess = req.session;
|
|
3753
|
+
if (sess?.authenticated) {
|
|
3754
|
+
next();
|
|
3755
|
+
}
|
|
3756
|
+
else {
|
|
3757
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
3758
|
+
}
|
|
3759
|
+
}, teamMemberHistoryRouter);
|
|
3760
|
+
logger.info('Team Member History API routes initialized');
|
|
3761
|
+
}
|
|
3762
|
+
catch (error) {
|
|
3763
|
+
logger.warn({ error }, 'Failed to initialize Team Member History API');
|
|
3764
|
+
}
|
|
3765
|
+
// Initialize TeamMember Deploy API routes
|
|
3766
|
+
try {
|
|
3767
|
+
const teamMemberDeployRouter = createTeamMemberDeployRouter();
|
|
3768
|
+
this.app.use('/api/teamMembers', (req, res, next) => {
|
|
3769
|
+
// Apply auth middleware to team member deploy routes
|
|
3770
|
+
const sess = req.session;
|
|
3771
|
+
if (sess?.authenticated) {
|
|
3772
|
+
next();
|
|
3773
|
+
}
|
|
3774
|
+
else {
|
|
3775
|
+
res.status(401).json({ error: 'Authentication required' });
|
|
3776
|
+
}
|
|
3777
|
+
}, teamMemberDeployRouter);
|
|
3778
|
+
logger.info('Team Member Deploy API routes initialized');
|
|
3779
|
+
}
|
|
3780
|
+
catch (error) {
|
|
3781
|
+
logger.warn({ error }, 'Failed to initialize TeamMember Deploy API');
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
// Initialize TeamMember Stream WebSocket
|
|
3785
|
+
try {
|
|
3786
|
+
this.teamMemberStreamManager = initializeTeamMemberStream(this.server, '/ws/team-members/live');
|
|
3787
|
+
logger.info('Team Member Stream WebSocket initialized');
|
|
3788
|
+
}
|
|
3789
|
+
catch (error) {
|
|
3790
|
+
logger.warn({ error }, 'Failed to initialize TeamMember Stream WebSocket');
|
|
3791
|
+
}
|
|
3792
|
+
// Auto-detect env file path for password persistence
|
|
3793
|
+
if (!this.envFilePath) {
|
|
3794
|
+
const envPaths = [
|
|
3795
|
+
path.join(process.cwd(), 'specmem.env'),
|
|
3796
|
+
path.join(process.cwd(), '.env'),
|
|
3797
|
+
path.join(__dirname, '../../specmem.env'),
|
|
3798
|
+
path.join(__dirname, '../../.env')
|
|
3799
|
+
];
|
|
3800
|
+
for (const envPath of envPaths) {
|
|
3801
|
+
try {
|
|
3802
|
+
await fs.access(envPath);
|
|
3803
|
+
this.envFilePath = envPath;
|
|
3804
|
+
logger.debug({ envPath }, 'Found env file for password persistence');
|
|
3805
|
+
break;
|
|
3806
|
+
}
|
|
3807
|
+
catch (e) {
|
|
3808
|
+
// File doesn't exist, try next - expected behavior
|
|
3809
|
+
logger.debug({ envPath, error: e }, 'env file not found at this path, checking next');
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
const { maxPortAttempts, maxStartupRetries, retryDelayMs } = this.config;
|
|
3814
|
+
for (let portOffset = 0; portOffset < maxPortAttempts; portOffset++) {
|
|
3815
|
+
const port = this.config.port + portOffset;
|
|
3816
|
+
// Check port availability first
|
|
3817
|
+
const available = await isPortAvailable(port, this.config.host);
|
|
3818
|
+
if (!available) {
|
|
3819
|
+
logger.debug({ port, host: this.config.host }, 'Dashboard port already in use, trying next');
|
|
3820
|
+
continue;
|
|
3821
|
+
}
|
|
3822
|
+
// Try to start the server with retries
|
|
3823
|
+
for (let retry = 0; retry < maxStartupRetries; retry++) {
|
|
3824
|
+
try {
|
|
3825
|
+
await this.startOnPort(port);
|
|
3826
|
+
this.actualPort = port;
|
|
3827
|
+
return; // Success!
|
|
3828
|
+
}
|
|
3829
|
+
catch (err) {
|
|
3830
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3831
|
+
// Check if it's a port-in-use error (race condition)
|
|
3832
|
+
if (error.message.includes('EADDRINUSE') || error.code === 'EADDRINUSE') {
|
|
3833
|
+
logger.warn({ port }, 'Dashboard port became unavailable during startup, trying next');
|
|
3834
|
+
break; // Try next port
|
|
3835
|
+
}
|
|
3836
|
+
logger.warn({
|
|
3837
|
+
port,
|
|
3838
|
+
retry: retry + 1,
|
|
3839
|
+
maxRetries: maxStartupRetries,
|
|
3840
|
+
error: error.message
|
|
3841
|
+
}, 'Dashboard server startup failed, retrying');
|
|
3842
|
+
// Wait before retry with exponential backoff
|
|
3843
|
+
if (retry < maxStartupRetries - 1) {
|
|
3844
|
+
await sleep(retryDelayMs * Math.pow(2, retry));
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
// All attempts failed
|
|
3850
|
+
const errorMsg = `Failed to start dashboard server on any port in range ${this.config.port}-${this.config.port + maxPortAttempts - 1}`;
|
|
3851
|
+
logger.error({ basePort: this.config.port, maxPortAttempts }, errorMsg);
|
|
3852
|
+
throw new Error(errorMsg);
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Internal method to start server on a specific port
|
|
3856
|
+
*/
|
|
3857
|
+
startOnPort(port) {
|
|
3858
|
+
return new Promise((resolve, reject) => {
|
|
3859
|
+
// Set up error handler before listening to catch EADDRINUSE
|
|
3860
|
+
const errorHandler = (error) => {
|
|
3861
|
+
// Remove error handler to prevent memory leak
|
|
3862
|
+
this.server.removeListener('error', errorHandler);
|
|
3863
|
+
logger.error({ error, port }, 'Dashboard server error during startup');
|
|
3864
|
+
reject(error);
|
|
3865
|
+
};
|
|
3866
|
+
this.server.once('error', errorHandler);
|
|
3867
|
+
this.server.listen(port, this.config.host, () => {
|
|
3868
|
+
// Remove error handler on success
|
|
3869
|
+
this.server.removeListener('error', errorHandler);
|
|
3870
|
+
this.isRunning = true;
|
|
3871
|
+
this.startTime = Date.now();
|
|
3872
|
+
this.actualPort = port;
|
|
3873
|
+
// Set up server timeouts
|
|
3874
|
+
setServerTimeouts(this.server, {
|
|
3875
|
+
keepAliveTimeout: parseInt(process.env['SPECMEM_KEEP_ALIVE_TIMEOUT'] || '5000', 10),
|
|
3876
|
+
headersTimeout: parseInt(process.env['SPECMEM_HEADERS_TIMEOUT'] || '60000', 10),
|
|
3877
|
+
requestTimeout: parseInt(process.env['SPECMEM_SERVER_REQUEST_TIMEOUT'] || '120000', 10)
|
|
3878
|
+
});
|
|
3879
|
+
// Set up persistent error handler for runtime errors
|
|
3880
|
+
this.server.on('error', (error) => {
|
|
3881
|
+
logger.error({ error, port: this.actualPort }, 'Dashboard server runtime error');
|
|
3882
|
+
// Don't crash - just log
|
|
3883
|
+
});
|
|
3884
|
+
logger.info({
|
|
3885
|
+
port,
|
|
3886
|
+
configuredPort: this.config.port,
|
|
3887
|
+
host: this.config.host,
|
|
3888
|
+
mode: this.config.mode,
|
|
3889
|
+
url: `http://${this.config.host}:${port}`,
|
|
3890
|
+
envFilePath: this.envFilePath || 'none'
|
|
3891
|
+
}, 'Dashboard server started - CSGO VIBES ACTIVATED');
|
|
3892
|
+
// Security warnings for public mode
|
|
3893
|
+
if (this.config.mode === 'public') {
|
|
3894
|
+
logger.warn('========================================');
|
|
3895
|
+
logger.warn(' SECURITY WARNING: PUBLIC MODE ACTIVE ');
|
|
3896
|
+
logger.warn('========================================');
|
|
3897
|
+
logger.warn(`Dashboard is accessible on the network at ${this.config.host}:${port}`);
|
|
3898
|
+
logger.warn('Ensure SPECMEM_DASHBOARD_PASSWORD is set to a strong password!');
|
|
3899
|
+
logger.warn('Consider using a reverse proxy (nginx/caddy) with HTTPS for production.');
|
|
3900
|
+
// Check for weak/default password
|
|
3901
|
+
if (isUsingDefaultPassword()) {
|
|
3902
|
+
logger.error('========================================');
|
|
3903
|
+
logger.error(' CRITICAL: DEFAULT PASSWORD IN USE! ');
|
|
3904
|
+
logger.error('========================================');
|
|
3905
|
+
logger.error('You are running in PUBLIC mode with the default password.');
|
|
3906
|
+
logger.error('This is a MAJOR security risk! Anyone on your network can access the dashboard.');
|
|
3907
|
+
logger.error('Set SPECMEM_DASHBOARD_PASSWORD to a strong, unique password immediately.');
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
else {
|
|
3911
|
+
logger.info({ mode: 'private' }, 'Dashboard running in private mode (localhost only)');
|
|
3912
|
+
}
|
|
3913
|
+
resolve();
|
|
3914
|
+
});
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Stop the server
|
|
3919
|
+
*/
|
|
3920
|
+
async stop() {
|
|
3921
|
+
if (!this.isRunning) {
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
// Close all WebSocket connections
|
|
3925
|
+
for (const client of this.connectedClients) {
|
|
3926
|
+
client.close(1001, 'Server shutting down');
|
|
3927
|
+
}
|
|
3928
|
+
this.connectedClients.clear();
|
|
3929
|
+
// Shutdown session store
|
|
3930
|
+
if (this.sessionStore) {
|
|
3931
|
+
await this.sessionStore.shutdown();
|
|
3932
|
+
this.sessionStore = null;
|
|
3933
|
+
}
|
|
3934
|
+
// Shutdown terminal stream manager
|
|
3935
|
+
if (this.terminalStreamManager) {
|
|
3936
|
+
this.terminalStreamManager.stop();
|
|
3937
|
+
this.terminalStreamManager = null;
|
|
3938
|
+
}
|
|
3939
|
+
// Shutdown team member stream manager and reset global singleton
|
|
3940
|
+
await shutdownTeamMemberStream();
|
|
3941
|
+
return new Promise((resolve) => {
|
|
3942
|
+
this.server.close(() => {
|
|
3943
|
+
this.isRunning = false;
|
|
3944
|
+
logger.info('Dashboard server stopped');
|
|
3945
|
+
resolve();
|
|
3946
|
+
});
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Get server status
|
|
3951
|
+
*/
|
|
3952
|
+
getStatus() {
|
|
3953
|
+
return {
|
|
3954
|
+
running: this.isRunning,
|
|
3955
|
+
port: this.actualPort || this.config.port,
|
|
3956
|
+
configuredPort: this.config.port,
|
|
3957
|
+
uptime: this.isRunning ? Date.now() - this.startTime : 0
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
/**
|
|
3961
|
+
* Get the actual port the server is bound to
|
|
3962
|
+
*/
|
|
3963
|
+
getActualPort() {
|
|
3964
|
+
return this.actualPort || this.config.port;
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Get current dashboard mode
|
|
3968
|
+
*/
|
|
3969
|
+
getMode() {
|
|
3970
|
+
return this.config.mode;
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3973
|
+
* Get current host binding
|
|
3974
|
+
*/
|
|
3975
|
+
getHost() {
|
|
3976
|
+
return this.config.host;
|
|
3977
|
+
}
|
|
3978
|
+
// ============================================================================
|
|
3979
|
+
// Config Persistence and Hot Reload
|
|
3980
|
+
// ============================================================================
|
|
3981
|
+
/**
|
|
3982
|
+
* RebindResult - Result of a server rebind operation
|
|
3983
|
+
*/
|
|
3984
|
+
pendingRebind = false;
|
|
3985
|
+
/**
|
|
3986
|
+
* updateConfig - Update server configuration with optional rebind
|
|
3987
|
+
*
|
|
3988
|
+
* Some changes (mode, host, port) require rebinding the server.
|
|
3989
|
+
* Password changes can be applied without restart (hot reload).
|
|
3990
|
+
*
|
|
3991
|
+
* @param newConfig - Partial config to update
|
|
3992
|
+
* @returns Object indicating success and whether restart is needed
|
|
3993
|
+
*/
|
|
3994
|
+
async updateConfig(newConfig) {
|
|
3995
|
+
const appliedChanges = [];
|
|
3996
|
+
let requiresRebind = false;
|
|
3997
|
+
// Detect changes that require rebind
|
|
3998
|
+
if (newConfig.mode !== undefined && newConfig.mode !== this.config.mode) {
|
|
3999
|
+
requiresRebind = true;
|
|
4000
|
+
appliedChanges.push(`mode: ${this.config.mode} -> ${newConfig.mode}`);
|
|
4001
|
+
}
|
|
4002
|
+
if (newConfig.host !== undefined && newConfig.host !== this.config.host) {
|
|
4003
|
+
requiresRebind = true;
|
|
4004
|
+
appliedChanges.push(`host: ${this.config.host} -> ${newConfig.host}`);
|
|
4005
|
+
}
|
|
4006
|
+
if (newConfig.port !== undefined && newConfig.port !== this.config.port) {
|
|
4007
|
+
requiresRebind = true;
|
|
4008
|
+
appliedChanges.push(`port: ${this.config.port} -> ${newConfig.port}`);
|
|
4009
|
+
}
|
|
4010
|
+
// Password can be hot-reloaded (it's checked on each login)
|
|
4011
|
+
if (newConfig.password !== undefined && newConfig.password !== this.config.password) {
|
|
4012
|
+
this.config.password = newConfig.password;
|
|
4013
|
+
appliedChanges.push('password: (updated)');
|
|
4014
|
+
logger.info('Dashboard password updated via hot reload');
|
|
4015
|
+
}
|
|
4016
|
+
// Session secret can be hot-reloaded but existing sessions will be invalidated
|
|
4017
|
+
if (newConfig.sessionSecret !== undefined && newConfig.sessionSecret !== this.config.sessionSecret) {
|
|
4018
|
+
this.config.sessionSecret = newConfig.sessionSecret;
|
|
4019
|
+
appliedChanges.push('sessionSecret: (updated - existing sessions invalidated)');
|
|
4020
|
+
logger.warn('Session secret changed - all existing sessions will be invalidated on next request');
|
|
4021
|
+
}
|
|
4022
|
+
if (!requiresRebind) {
|
|
4023
|
+
return {
|
|
4024
|
+
success: true,
|
|
4025
|
+
message: 'Configuration updated (hot reload)',
|
|
4026
|
+
requiresRebind: false,
|
|
4027
|
+
appliedChanges
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
// Apply config changes that will take effect on rebind
|
|
4031
|
+
if (newConfig.mode !== undefined)
|
|
4032
|
+
this.config.mode = newConfig.mode;
|
|
4033
|
+
if (newConfig.host !== undefined)
|
|
4034
|
+
this.config.host = newConfig.host;
|
|
4035
|
+
if (newConfig.port !== undefined)
|
|
4036
|
+
this.config.port = newConfig.port;
|
|
4037
|
+
return {
|
|
4038
|
+
success: true,
|
|
4039
|
+
message: 'Configuration updated. Server rebind required to apply binding changes.',
|
|
4040
|
+
requiresRebind: true,
|
|
4041
|
+
appliedChanges
|
|
4042
|
+
};
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* rebind - Gracefully rebind the server to a new host/port
|
|
4046
|
+
*
|
|
4047
|
+
* This performs a graceful restart:
|
|
4048
|
+
* 1. Mark server as stopping
|
|
4049
|
+
* 2. Stop accepting new connections
|
|
4050
|
+
* 3. Close existing WebSocket connections with notice
|
|
4051
|
+
* 4. Close HTTP server
|
|
4052
|
+
* 5. Restart with new configuration
|
|
4053
|
+
*
|
|
4054
|
+
* @param notifyClients - Whether to notify WebSocket clients before restart
|
|
4055
|
+
* @returns Promise<boolean> - true if rebind successful
|
|
4056
|
+
*/
|
|
4057
|
+
async rebind(notifyClients = true) {
|
|
4058
|
+
if (this.pendingRebind) {
|
|
4059
|
+
return {
|
|
4060
|
+
success: false,
|
|
4061
|
+
message: 'Rebind already in progress',
|
|
4062
|
+
oldBinding: { host: this.config.host, port: this.actualPort },
|
|
4063
|
+
newBinding: { host: this.config.host, port: this.config.port }
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
this.pendingRebind = true;
|
|
4067
|
+
const oldBinding = { host: this.config.host, port: this.actualPort };
|
|
4068
|
+
try {
|
|
4069
|
+
logger.info({
|
|
4070
|
+
oldHost: oldBinding.host,
|
|
4071
|
+
oldPort: oldBinding.port,
|
|
4072
|
+
newHost: this.config.host,
|
|
4073
|
+
newPort: this.config.port,
|
|
4074
|
+
mode: this.config.mode
|
|
4075
|
+
}, 'initiating graceful server rebind');
|
|
4076
|
+
// Notify WebSocket clients about impending restart
|
|
4077
|
+
if (notifyClients && this.connectedClients.size > 0) {
|
|
4078
|
+
const restartNotice = JSON.stringify({
|
|
4079
|
+
type: 'server_restart',
|
|
4080
|
+
message: 'Server is restarting to apply configuration changes',
|
|
4081
|
+
reconnectIn: 3000
|
|
4082
|
+
});
|
|
4083
|
+
for (const client of this.connectedClients) {
|
|
4084
|
+
try {
|
|
4085
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
4086
|
+
client.send(restartNotice);
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
catch (err) {
|
|
4090
|
+
// Ignore individual client errors
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
// Give clients time to receive the message
|
|
4094
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
4095
|
+
}
|
|
4096
|
+
// Stop the server (closes connections)
|
|
4097
|
+
await this.stop();
|
|
4098
|
+
// Re-apply host based on mode
|
|
4099
|
+
if (this.config.mode === 'private') {
|
|
4100
|
+
this.config.host = '127.0.0.1';
|
|
4101
|
+
}
|
|
4102
|
+
else if (!this.config.host || this.config.host === '127.0.0.1') {
|
|
4103
|
+
this.config.host = '0.0.0.0';
|
|
4104
|
+
}
|
|
4105
|
+
// Create new HTTP server instance
|
|
4106
|
+
this.server = createServer(this.app);
|
|
4107
|
+
// Re-setup WebSocket server
|
|
4108
|
+
this.wss = new WebSocketServer({
|
|
4109
|
+
noServer: true,
|
|
4110
|
+
perMessageDeflate: false,
|
|
4111
|
+
clientTracking: true,
|
|
4112
|
+
maxPayload: 100 * 1024 * 1024
|
|
4113
|
+
});
|
|
4114
|
+
// Re-setup upgrade handler
|
|
4115
|
+
this.server.on('upgrade', (request, socket, head) => {
|
|
4116
|
+
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
|
4117
|
+
const pathname = url.pathname;
|
|
4118
|
+
if (pathname === '/ws/team-members/live') {
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
4122
|
+
this.wss.emit('connection', ws, request);
|
|
4123
|
+
});
|
|
4124
|
+
});
|
|
4125
|
+
// Start on new binding
|
|
4126
|
+
await this.start();
|
|
4127
|
+
const newBinding = { host: this.config.host, port: this.actualPort };
|
|
4128
|
+
logger.info({
|
|
4129
|
+
oldBinding,
|
|
4130
|
+
newBinding,
|
|
4131
|
+
mode: this.config.mode
|
|
4132
|
+
}, 'server rebind completed successfully');
|
|
4133
|
+
return {
|
|
4134
|
+
success: true,
|
|
4135
|
+
message: `Server rebound from ${oldBinding.host}:${oldBinding.port} to ${newBinding.host}:${newBinding.port}`,
|
|
4136
|
+
oldBinding,
|
|
4137
|
+
newBinding
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
catch (err) {
|
|
4141
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
4142
|
+
logger.error({ error, oldBinding }, 'server rebind failed - attempting rollback');
|
|
4143
|
+
// ROLLBACK: Try to restore server on old binding
|
|
4144
|
+
try {
|
|
4145
|
+
// Restore config to old values
|
|
4146
|
+
this.config.host = oldBinding.host;
|
|
4147
|
+
this.config.port = oldBinding.port;
|
|
4148
|
+
// Recreate server if needed
|
|
4149
|
+
if (!this.isRunning) {
|
|
4150
|
+
this.server = createServer(this.app);
|
|
4151
|
+
this.wss = new WebSocketServer({
|
|
4152
|
+
noServer: true,
|
|
4153
|
+
perMessageDeflate: false,
|
|
4154
|
+
clientTracking: true,
|
|
4155
|
+
maxPayload: 100 * 1024 * 1024
|
|
4156
|
+
});
|
|
4157
|
+
this.server.on('upgrade', (request, socket, head) => {
|
|
4158
|
+
const url = new URL(request.url || '/', `http://${request.headers.host}`);
|
|
4159
|
+
const pathname = url.pathname;
|
|
4160
|
+
if (pathname === '/ws/team-members/live')
|
|
4161
|
+
return;
|
|
4162
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
4163
|
+
this.wss.emit('connection', ws, request);
|
|
4164
|
+
});
|
|
4165
|
+
});
|
|
4166
|
+
await this.start();
|
|
4167
|
+
logger.info({ oldBinding }, 'ROLLBACK SUCCESS: Server restored to previous binding');
|
|
4168
|
+
return {
|
|
4169
|
+
success: false,
|
|
4170
|
+
message: `Rebind failed: ${error.message}. Server rolled back to ${oldBinding.host}:${oldBinding.port}`,
|
|
4171
|
+
oldBinding,
|
|
4172
|
+
newBinding: oldBinding,
|
|
4173
|
+
rolledBack: true
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
catch (rollbackErr) {
|
|
4178
|
+
const rollbackError = rollbackErr instanceof Error ? rollbackErr : new Error(String(rollbackErr));
|
|
4179
|
+
logger.error({ error: rollbackError, oldBinding }, 'ROLLBACK FAILED: Server may be in inconsistent state');
|
|
4180
|
+
return {
|
|
4181
|
+
success: false,
|
|
4182
|
+
message: `Rebind failed: ${error.message}. Rollback also failed: ${rollbackError.message}. Manual restart required.`,
|
|
4183
|
+
oldBinding,
|
|
4184
|
+
newBinding: { host: this.config.host, port: this.config.port },
|
|
4185
|
+
rollbackFailed: true
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
4188
|
+
return {
|
|
4189
|
+
success: false,
|
|
4190
|
+
message: `Rebind failed: ${error.message}`,
|
|
4191
|
+
oldBinding,
|
|
4192
|
+
newBinding: { host: this.config.host, port: this.config.port }
|
|
4193
|
+
};
|
|
4194
|
+
}
|
|
4195
|
+
finally {
|
|
4196
|
+
this.pendingRebind = false;
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
/**
|
|
4200
|
+
* scheduleRebind - Schedule a rebind after a delay
|
|
4201
|
+
*
|
|
4202
|
+
* Useful for giving clients time to prepare for restart
|
|
4203
|
+
*
|
|
4204
|
+
* @param delayMs - Delay before rebind in milliseconds
|
|
4205
|
+
* @returns Promise resolving when rebind is complete
|
|
4206
|
+
*/
|
|
4207
|
+
async scheduleRebind(delayMs = 2000) {
|
|
4208
|
+
logger.info({ delayMs }, 'scheduling server rebind');
|
|
4209
|
+
// Notify connected clients
|
|
4210
|
+
const scheduleNotice = JSON.stringify({
|
|
4211
|
+
type: 'server_restart_scheduled',
|
|
4212
|
+
message: `Server will restart in ${delayMs}ms to apply configuration changes`,
|
|
4213
|
+
restartTime: Date.now() + delayMs
|
|
4214
|
+
});
|
|
4215
|
+
for (const client of this.connectedClients) {
|
|
4216
|
+
try {
|
|
4217
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
4218
|
+
client.send(scheduleNotice);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
catch {
|
|
4222
|
+
// Ignore
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
// Wait for delay
|
|
4226
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
4227
|
+
// Perform rebind
|
|
4228
|
+
const result = await this.rebind(false); // Already notified
|
|
4229
|
+
return {
|
|
4230
|
+
success: result.success,
|
|
4231
|
+
message: result.message
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* reloadConfig - Reload configuration from environment/files
|
|
4236
|
+
*
|
|
4237
|
+
* Hot-reloads what can be reloaded without restart,
|
|
4238
|
+
* flags what requires restart
|
|
4239
|
+
*/
|
|
4240
|
+
async reloadConfig() {
|
|
4241
|
+
const hotReloaded = [];
|
|
4242
|
+
const requiresRestart = [];
|
|
4243
|
+
// Password can be hot-reloaded (centralized password module handles this)
|
|
4244
|
+
const newPassword = getPassword();
|
|
4245
|
+
if (newPassword !== this.config.password) {
|
|
4246
|
+
this.config.password = newPassword;
|
|
4247
|
+
hotReloaded.push('password');
|
|
4248
|
+
}
|
|
4249
|
+
// Mode changes require restart
|
|
4250
|
+
const newMode = getDashboardMode();
|
|
4251
|
+
if (newMode !== this.config.mode) {
|
|
4252
|
+
requiresRestart.push(`mode: ${this.config.mode} -> ${newMode}`);
|
|
4253
|
+
}
|
|
4254
|
+
// Host changes require restart
|
|
4255
|
+
const newHost = getDashboardHost();
|
|
4256
|
+
if (newHost !== this.config.host) {
|
|
4257
|
+
requiresRestart.push(`host: ${this.config.host} -> ${newHost}`);
|
|
4258
|
+
}
|
|
4259
|
+
// Port changes require restart
|
|
4260
|
+
const newPort = parseInt(process.env['SPECMEM_DASHBOARD_PORT'] || '8585', 10);
|
|
4261
|
+
if (newPort !== this.config.port) {
|
|
4262
|
+
requiresRestart.push(`port: ${this.config.port} -> ${newPort}`);
|
|
4263
|
+
}
|
|
4264
|
+
logger.info({
|
|
4265
|
+
hotReloaded,
|
|
4266
|
+
requiresRestart
|
|
4267
|
+
}, 'config reload check completed');
|
|
4268
|
+
return {
|
|
4269
|
+
success: true,
|
|
4270
|
+
hotReloaded,
|
|
4271
|
+
requiresRestart
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
// ============================================================================
|
|
4276
|
+
// Singleton Instance
|
|
4277
|
+
// ============================================================================
|
|
4278
|
+
let globalDashboard = null;
|
|
4279
|
+
/**
|
|
4280
|
+
* Get the global dashboard server
|
|
4281
|
+
*/
|
|
4282
|
+
export function getDashboardServer(config) {
|
|
4283
|
+
if (!globalDashboard) {
|
|
4284
|
+
globalDashboard = new DashboardWebServer(config);
|
|
4285
|
+
}
|
|
4286
|
+
return globalDashboard;
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Reset the global dashboard server (for testing)
|
|
4290
|
+
*/
|
|
4291
|
+
export async function resetDashboardServer() {
|
|
4292
|
+
if (globalDashboard) {
|
|
4293
|
+
await globalDashboard.stop();
|
|
4294
|
+
globalDashboard = null;
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
//# sourceMappingURL=webServer.js.map
|