@otto-assistant/bridge 0.4.92
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/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +302 -0
- package/dist/format-tables.test.js +308 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +483 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +263 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +144 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +335 -0
- package/src/format-tables.ts +445 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +640 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +314 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +68 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +212 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
package/src/database.ts
ADDED
|
@@ -0,0 +1,1876 @@
|
|
|
1
|
+
// SQLite database manager for persistent bot state using Prisma.
|
|
2
|
+
// Stores thread-session mappings, bot tokens, channel directories,
|
|
3
|
+
// API keys, and model preferences in <dataDir>/discord-sessions.db.
|
|
4
|
+
|
|
5
|
+
import { getPrisma, closePrisma } from './db.js'
|
|
6
|
+
import type { Prisma, session_events, BotMode, VerbosityLevel, WorktreeStatus, ChannelType as PrismaChannelType, ThreadSessionSource } from './generated/client.js'
|
|
7
|
+
import crypto from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
import { store } from './store.js'
|
|
10
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
11
|
+
|
|
12
|
+
const dbLogger = createLogger(LogPrefix.DB)
|
|
13
|
+
|
|
14
|
+
// Re-export Prisma utilities
|
|
15
|
+
export { getPrisma, closePrisma }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the database.
|
|
19
|
+
* Returns the Prisma client.
|
|
20
|
+
*/
|
|
21
|
+
export async function initDatabase() {
|
|
22
|
+
const prisma = await getPrisma()
|
|
23
|
+
dbLogger.log('Database initialized')
|
|
24
|
+
return prisma
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Close the database connection.
|
|
29
|
+
*/
|
|
30
|
+
export async function closeDatabase() {
|
|
31
|
+
await closePrisma()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Re-export enum types from generated Prisma client
|
|
35
|
+
export type { VerbosityLevel }
|
|
36
|
+
export type { WorktreeStatus }
|
|
37
|
+
export type { PrismaChannelType }
|
|
38
|
+
|
|
39
|
+
export type ThreadWorktree = {
|
|
40
|
+
thread_id: string
|
|
41
|
+
worktree_name: string
|
|
42
|
+
worktree_directory: string | null
|
|
43
|
+
project_directory: string
|
|
44
|
+
status: WorktreeStatus
|
|
45
|
+
error_message: string | null
|
|
46
|
+
created_at: Date | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ScheduledTaskStatus =
|
|
50
|
+
| 'planned'
|
|
51
|
+
| 'running'
|
|
52
|
+
| 'completed'
|
|
53
|
+
| 'cancelled'
|
|
54
|
+
| 'failed'
|
|
55
|
+
export type ScheduledTaskScheduleKind = 'at' | 'cron'
|
|
56
|
+
|
|
57
|
+
export type ScheduledTask = {
|
|
58
|
+
id: number
|
|
59
|
+
status: ScheduledTaskStatus
|
|
60
|
+
schedule_kind: ScheduledTaskScheduleKind
|
|
61
|
+
run_at: Date | null
|
|
62
|
+
cron_expr: string | null
|
|
63
|
+
timezone: string | null
|
|
64
|
+
next_run_at: Date
|
|
65
|
+
running_started_at: Date | null
|
|
66
|
+
last_run_at: Date | null
|
|
67
|
+
last_error: string | null
|
|
68
|
+
attempts: number
|
|
69
|
+
payload_json: string
|
|
70
|
+
prompt_preview: string
|
|
71
|
+
channel_id: string | null
|
|
72
|
+
thread_id: string | null
|
|
73
|
+
session_id: string | null
|
|
74
|
+
project_directory: string | null
|
|
75
|
+
created_at: Date | null
|
|
76
|
+
updated_at: Date | null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type SessionStartSource = {
|
|
80
|
+
session_id: string
|
|
81
|
+
schedule_kind: ScheduledTaskScheduleKind
|
|
82
|
+
scheduled_task_id: number | null
|
|
83
|
+
created_at: Date | null
|
|
84
|
+
updated_at: Date | null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toScheduledTask(row: {
|
|
88
|
+
id: number
|
|
89
|
+
status: string
|
|
90
|
+
schedule_kind: string
|
|
91
|
+
run_at: Date | null
|
|
92
|
+
cron_expr: string | null
|
|
93
|
+
timezone: string | null
|
|
94
|
+
next_run_at: Date
|
|
95
|
+
running_started_at: Date | null
|
|
96
|
+
last_run_at: Date | null
|
|
97
|
+
last_error: string | null
|
|
98
|
+
attempts: number
|
|
99
|
+
payload_json: string
|
|
100
|
+
prompt_preview: string
|
|
101
|
+
channel_id: string | null
|
|
102
|
+
thread_id: string | null
|
|
103
|
+
session_id: string | null
|
|
104
|
+
project_directory: string | null
|
|
105
|
+
created_at: Date | null
|
|
106
|
+
updated_at: Date | null
|
|
107
|
+
}): ScheduledTask {
|
|
108
|
+
return {
|
|
109
|
+
id: row.id,
|
|
110
|
+
status: row.status as ScheduledTaskStatus,
|
|
111
|
+
schedule_kind: row.schedule_kind as ScheduledTaskScheduleKind,
|
|
112
|
+
run_at: row.run_at,
|
|
113
|
+
cron_expr: row.cron_expr,
|
|
114
|
+
timezone: row.timezone,
|
|
115
|
+
next_run_at: row.next_run_at,
|
|
116
|
+
running_started_at: row.running_started_at,
|
|
117
|
+
last_run_at: row.last_run_at,
|
|
118
|
+
last_error: row.last_error,
|
|
119
|
+
attempts: row.attempts,
|
|
120
|
+
payload_json: row.payload_json,
|
|
121
|
+
prompt_preview: row.prompt_preview,
|
|
122
|
+
channel_id: row.channel_id,
|
|
123
|
+
thread_id: row.thread_id,
|
|
124
|
+
session_id: row.session_id,
|
|
125
|
+
project_directory: row.project_directory,
|
|
126
|
+
created_at: row.created_at,
|
|
127
|
+
updated_at: row.updated_at,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function toSessionStartSource(row: {
|
|
132
|
+
session_id: string
|
|
133
|
+
schedule_kind: string
|
|
134
|
+
scheduled_task_id: number | null
|
|
135
|
+
created_at: Date | null
|
|
136
|
+
updated_at: Date | null
|
|
137
|
+
}): SessionStartSource {
|
|
138
|
+
return {
|
|
139
|
+
session_id: row.session_id,
|
|
140
|
+
schedule_kind: row.schedule_kind as ScheduledTaskScheduleKind,
|
|
141
|
+
scheduled_task_id: row.scheduled_task_id,
|
|
142
|
+
created_at: row.created_at,
|
|
143
|
+
updated_at: row.updated_at,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Scheduled Task Functions
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
export async function createScheduledTask({
|
|
152
|
+
scheduleKind,
|
|
153
|
+
runAt,
|
|
154
|
+
cronExpr,
|
|
155
|
+
timezone,
|
|
156
|
+
nextRunAt,
|
|
157
|
+
payloadJson,
|
|
158
|
+
promptPreview,
|
|
159
|
+
channelId,
|
|
160
|
+
threadId,
|
|
161
|
+
sessionId,
|
|
162
|
+
projectDirectory,
|
|
163
|
+
}: {
|
|
164
|
+
scheduleKind: ScheduledTaskScheduleKind
|
|
165
|
+
runAt?: Date | null
|
|
166
|
+
cronExpr?: string | null
|
|
167
|
+
timezone?: string | null
|
|
168
|
+
nextRunAt: Date
|
|
169
|
+
payloadJson: string
|
|
170
|
+
promptPreview: string
|
|
171
|
+
channelId?: string | null
|
|
172
|
+
threadId?: string | null
|
|
173
|
+
sessionId?: string | null
|
|
174
|
+
projectDirectory?: string | null
|
|
175
|
+
}): Promise<number> {
|
|
176
|
+
const prisma = await getPrisma()
|
|
177
|
+
const row = await prisma.scheduled_tasks.create({
|
|
178
|
+
data: {
|
|
179
|
+
status: 'planned',
|
|
180
|
+
schedule_kind: scheduleKind,
|
|
181
|
+
run_at: runAt ?? null,
|
|
182
|
+
cron_expr: cronExpr ?? null,
|
|
183
|
+
timezone: timezone ?? null,
|
|
184
|
+
next_run_at: nextRunAt,
|
|
185
|
+
payload_json: payloadJson,
|
|
186
|
+
prompt_preview: promptPreview,
|
|
187
|
+
channel_id: channelId ?? null,
|
|
188
|
+
thread_id: threadId ?? null,
|
|
189
|
+
session_id: sessionId ?? null,
|
|
190
|
+
project_directory: projectDirectory ?? null,
|
|
191
|
+
},
|
|
192
|
+
select: { id: true },
|
|
193
|
+
})
|
|
194
|
+
return row.id
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function listScheduledTasks({
|
|
198
|
+
statuses,
|
|
199
|
+
}: {
|
|
200
|
+
statuses?: ScheduledTaskStatus[]
|
|
201
|
+
} = {}): Promise<ScheduledTask[]> {
|
|
202
|
+
const prisma = await getPrisma()
|
|
203
|
+
const rows = await prisma.scheduled_tasks.findMany({
|
|
204
|
+
where:
|
|
205
|
+
statuses && statuses.length > 0
|
|
206
|
+
? { status: { in: statuses } }
|
|
207
|
+
: undefined,
|
|
208
|
+
orderBy: [{ next_run_at: 'asc' }, { id: 'asc' }],
|
|
209
|
+
})
|
|
210
|
+
return rows.map((row) => toScheduledTask(row))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function getScheduledTask(
|
|
214
|
+
taskId: number,
|
|
215
|
+
): Promise<ScheduledTask | null> {
|
|
216
|
+
const prisma = await getPrisma()
|
|
217
|
+
const row = await prisma.scheduled_tasks.findUnique({
|
|
218
|
+
where: { id: taskId },
|
|
219
|
+
})
|
|
220
|
+
return row ? toScheduledTask(row) : null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function updateScheduledTask({
|
|
224
|
+
taskId,
|
|
225
|
+
payloadJson,
|
|
226
|
+
promptPreview,
|
|
227
|
+
scheduleKind,
|
|
228
|
+
runAt,
|
|
229
|
+
cronExpr,
|
|
230
|
+
timezone,
|
|
231
|
+
nextRunAt,
|
|
232
|
+
}: {
|
|
233
|
+
taskId: number
|
|
234
|
+
payloadJson: string
|
|
235
|
+
promptPreview: string
|
|
236
|
+
scheduleKind?: ScheduledTaskScheduleKind
|
|
237
|
+
runAt?: Date | null
|
|
238
|
+
cronExpr?: string | null
|
|
239
|
+
timezone?: string | null
|
|
240
|
+
nextRunAt?: Date
|
|
241
|
+
}): Promise<boolean> {
|
|
242
|
+
const prisma = await getPrisma()
|
|
243
|
+
const data: Record<string, unknown> = {
|
|
244
|
+
payload_json: payloadJson,
|
|
245
|
+
prompt_preview: promptPreview,
|
|
246
|
+
}
|
|
247
|
+
if (scheduleKind !== undefined) {
|
|
248
|
+
data.schedule_kind = scheduleKind
|
|
249
|
+
}
|
|
250
|
+
if (runAt !== undefined) {
|
|
251
|
+
data.run_at = runAt
|
|
252
|
+
}
|
|
253
|
+
if (cronExpr !== undefined) {
|
|
254
|
+
data.cron_expr = cronExpr
|
|
255
|
+
}
|
|
256
|
+
if (timezone !== undefined) {
|
|
257
|
+
data.timezone = timezone
|
|
258
|
+
}
|
|
259
|
+
if (nextRunAt !== undefined) {
|
|
260
|
+
data.next_run_at = nextRunAt
|
|
261
|
+
}
|
|
262
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
263
|
+
where: {
|
|
264
|
+
id: taskId,
|
|
265
|
+
status: 'planned',
|
|
266
|
+
},
|
|
267
|
+
data,
|
|
268
|
+
})
|
|
269
|
+
return result.count > 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function cancelScheduledTask(taskId: number): Promise<boolean> {
|
|
273
|
+
const prisma = await getPrisma()
|
|
274
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
275
|
+
where: {
|
|
276
|
+
id: taskId,
|
|
277
|
+
status: {
|
|
278
|
+
in: ['planned', 'running'],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
data: {
|
|
282
|
+
status: 'cancelled',
|
|
283
|
+
running_started_at: null,
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
return result.count > 0
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function getDuePlannedScheduledTasks({
|
|
290
|
+
now,
|
|
291
|
+
limit,
|
|
292
|
+
}: {
|
|
293
|
+
now: Date
|
|
294
|
+
limit: number
|
|
295
|
+
}): Promise<ScheduledTask[]> {
|
|
296
|
+
const prisma = await getPrisma()
|
|
297
|
+
const rows = await prisma.scheduled_tasks.findMany({
|
|
298
|
+
where: {
|
|
299
|
+
status: 'planned',
|
|
300
|
+
next_run_at: {
|
|
301
|
+
lte: now,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
orderBy: [{ next_run_at: 'asc' }, { id: 'asc' }],
|
|
305
|
+
take: limit,
|
|
306
|
+
})
|
|
307
|
+
return rows.map((row) => toScheduledTask(row))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function claimScheduledTaskRunning({
|
|
311
|
+
taskId,
|
|
312
|
+
startedAt,
|
|
313
|
+
}: {
|
|
314
|
+
taskId: number
|
|
315
|
+
startedAt: Date
|
|
316
|
+
}): Promise<boolean> {
|
|
317
|
+
const prisma = await getPrisma()
|
|
318
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
319
|
+
where: {
|
|
320
|
+
id: taskId,
|
|
321
|
+
status: 'planned',
|
|
322
|
+
},
|
|
323
|
+
data: {
|
|
324
|
+
status: 'running',
|
|
325
|
+
running_started_at: startedAt,
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
return result.count > 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function recoverStaleRunningScheduledTasks({
|
|
332
|
+
staleBefore,
|
|
333
|
+
}: {
|
|
334
|
+
staleBefore: Date
|
|
335
|
+
}): Promise<number> {
|
|
336
|
+
const prisma = await getPrisma()
|
|
337
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
338
|
+
where: {
|
|
339
|
+
status: 'running',
|
|
340
|
+
running_started_at: {
|
|
341
|
+
lte: staleBefore,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
data: {
|
|
345
|
+
status: 'planned',
|
|
346
|
+
running_started_at: null,
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
return result.count
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function markScheduledTaskOneShotCompleted({
|
|
353
|
+
taskId,
|
|
354
|
+
completedAt,
|
|
355
|
+
}: {
|
|
356
|
+
taskId: number
|
|
357
|
+
completedAt: Date
|
|
358
|
+
}): Promise<void> {
|
|
359
|
+
const prisma = await getPrisma()
|
|
360
|
+
await prisma.scheduled_tasks.update({
|
|
361
|
+
where: { id: taskId },
|
|
362
|
+
data: {
|
|
363
|
+
status: 'completed',
|
|
364
|
+
last_run_at: completedAt,
|
|
365
|
+
running_started_at: null,
|
|
366
|
+
last_error: null,
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function markScheduledTaskCronRescheduled({
|
|
372
|
+
taskId,
|
|
373
|
+
completedAt,
|
|
374
|
+
nextRunAt,
|
|
375
|
+
}: {
|
|
376
|
+
taskId: number
|
|
377
|
+
completedAt: Date
|
|
378
|
+
nextRunAt: Date
|
|
379
|
+
}): Promise<void> {
|
|
380
|
+
const prisma = await getPrisma()
|
|
381
|
+
await prisma.scheduled_tasks.update({
|
|
382
|
+
where: { id: taskId },
|
|
383
|
+
data: {
|
|
384
|
+
status: 'planned',
|
|
385
|
+
last_run_at: completedAt,
|
|
386
|
+
running_started_at: null,
|
|
387
|
+
last_error: null,
|
|
388
|
+
next_run_at: nextRunAt,
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function markScheduledTaskFailed({
|
|
394
|
+
taskId,
|
|
395
|
+
failedAt,
|
|
396
|
+
errorMessage,
|
|
397
|
+
}: {
|
|
398
|
+
taskId: number
|
|
399
|
+
failedAt: Date
|
|
400
|
+
errorMessage: string
|
|
401
|
+
}): Promise<void> {
|
|
402
|
+
const prisma = await getPrisma()
|
|
403
|
+
await prisma.scheduled_tasks.update({
|
|
404
|
+
where: { id: taskId },
|
|
405
|
+
data: {
|
|
406
|
+
status: 'failed',
|
|
407
|
+
last_run_at: failedAt,
|
|
408
|
+
running_started_at: null,
|
|
409
|
+
last_error: errorMessage,
|
|
410
|
+
attempts: {
|
|
411
|
+
increment: 1,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function markScheduledTaskCronRetry({
|
|
418
|
+
taskId,
|
|
419
|
+
failedAt,
|
|
420
|
+
errorMessage,
|
|
421
|
+
nextRunAt,
|
|
422
|
+
}: {
|
|
423
|
+
taskId: number
|
|
424
|
+
failedAt: Date
|
|
425
|
+
errorMessage: string
|
|
426
|
+
nextRunAt: Date
|
|
427
|
+
}): Promise<void> {
|
|
428
|
+
const prisma = await getPrisma()
|
|
429
|
+
await prisma.scheduled_tasks.update({
|
|
430
|
+
where: { id: taskId },
|
|
431
|
+
data: {
|
|
432
|
+
status: 'planned',
|
|
433
|
+
next_run_at: nextRunAt,
|
|
434
|
+
last_run_at: failedAt,
|
|
435
|
+
running_started_at: null,
|
|
436
|
+
last_error: errorMessage,
|
|
437
|
+
attempts: {
|
|
438
|
+
increment: 1,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function setSessionStartSource({
|
|
445
|
+
sessionId,
|
|
446
|
+
scheduleKind,
|
|
447
|
+
scheduledTaskId,
|
|
448
|
+
}: {
|
|
449
|
+
sessionId: string
|
|
450
|
+
scheduleKind: ScheduledTaskScheduleKind
|
|
451
|
+
scheduledTaskId?: number
|
|
452
|
+
}): Promise<void> {
|
|
453
|
+
const prisma = await getPrisma()
|
|
454
|
+
await prisma.session_start_sources.upsert({
|
|
455
|
+
where: { session_id: sessionId },
|
|
456
|
+
create: {
|
|
457
|
+
session_id: sessionId,
|
|
458
|
+
schedule_kind: scheduleKind,
|
|
459
|
+
scheduled_task_id: scheduledTaskId ?? null,
|
|
460
|
+
},
|
|
461
|
+
update: {
|
|
462
|
+
schedule_kind: scheduleKind,
|
|
463
|
+
scheduled_task_id: scheduledTaskId ?? null,
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function getSessionStartSourcesBySessionIds(
|
|
469
|
+
sessionIds: string[],
|
|
470
|
+
): Promise<Map<string, SessionStartSource>> {
|
|
471
|
+
if (sessionIds.length === 0) {
|
|
472
|
+
return new Map<string, SessionStartSource>()
|
|
473
|
+
}
|
|
474
|
+
const prisma = await getPrisma()
|
|
475
|
+
const chunkSize = 500
|
|
476
|
+
const chunks: string[][] = []
|
|
477
|
+
for (let index = 0; index < sessionIds.length; index += chunkSize) {
|
|
478
|
+
chunks.push(sessionIds.slice(index, index + chunkSize))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const rowGroups = await Promise.all(
|
|
482
|
+
chunks.map((chunkSessionIds) => {
|
|
483
|
+
return prisma.session_start_sources.findMany({
|
|
484
|
+
where: {
|
|
485
|
+
session_id: {
|
|
486
|
+
in: chunkSessionIds,
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
})
|
|
490
|
+
}),
|
|
491
|
+
)
|
|
492
|
+
const rows = rowGroups.flatMap((group) => group)
|
|
493
|
+
return new Map(rows.map((row) => [row.session_id, toSessionStartSource(row)]))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ============================================================================
|
|
497
|
+
// Channel Model Functions
|
|
498
|
+
// ============================================================================
|
|
499
|
+
|
|
500
|
+
export type ModelPreference = { modelId: string; variant: string | null }
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get the model preference for a channel.
|
|
504
|
+
* @returns Model ID in format "provider_id/model_id" + optional variant, or undefined
|
|
505
|
+
*/
|
|
506
|
+
export async function getChannelModel(
|
|
507
|
+
channelId: string,
|
|
508
|
+
): Promise<ModelPreference | undefined> {
|
|
509
|
+
const prisma = await getPrisma()
|
|
510
|
+
const row = await prisma.channel_models.findUnique({
|
|
511
|
+
where: { channel_id: channelId },
|
|
512
|
+
})
|
|
513
|
+
if (!row) {
|
|
514
|
+
return undefined
|
|
515
|
+
}
|
|
516
|
+
return { modelId: row.model_id, variant: row.variant }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Set the model preference for a channel.
|
|
521
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
522
|
+
* @param variant Optional thinking/reasoning variant name
|
|
523
|
+
*/
|
|
524
|
+
export async function setChannelModel({
|
|
525
|
+
channelId,
|
|
526
|
+
modelId,
|
|
527
|
+
variant,
|
|
528
|
+
}: {
|
|
529
|
+
channelId: string
|
|
530
|
+
modelId: string
|
|
531
|
+
variant?: string | null
|
|
532
|
+
}): Promise<void> {
|
|
533
|
+
const prisma = await getPrisma()
|
|
534
|
+
await prisma.channel_models.upsert({
|
|
535
|
+
where: { channel_id: channelId },
|
|
536
|
+
create: {
|
|
537
|
+
channel_id: channelId,
|
|
538
|
+
model_id: modelId,
|
|
539
|
+
variant: variant ?? null,
|
|
540
|
+
},
|
|
541
|
+
update: {
|
|
542
|
+
model_id: modelId,
|
|
543
|
+
variant: variant ?? null,
|
|
544
|
+
updated_at: new Date(),
|
|
545
|
+
},
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// Global Model Functions
|
|
551
|
+
// ============================================================================
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get the global default model for a bot.
|
|
555
|
+
* @returns Model ID in format "provider_id/model_id" + optional variant, or undefined
|
|
556
|
+
*/
|
|
557
|
+
export async function getGlobalModel(
|
|
558
|
+
appId: string,
|
|
559
|
+
): Promise<ModelPreference | undefined> {
|
|
560
|
+
const prisma = await getPrisma()
|
|
561
|
+
const row = await prisma.global_models.findUnique({
|
|
562
|
+
where: { app_id: appId },
|
|
563
|
+
})
|
|
564
|
+
if (!row) {
|
|
565
|
+
return undefined
|
|
566
|
+
}
|
|
567
|
+
return { modelId: row.model_id, variant: row.variant }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Set the global default model for a bot.
|
|
572
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
573
|
+
* @param variant Optional thinking/reasoning variant name
|
|
574
|
+
*/
|
|
575
|
+
export async function setGlobalModel({
|
|
576
|
+
appId,
|
|
577
|
+
modelId,
|
|
578
|
+
variant,
|
|
579
|
+
}: {
|
|
580
|
+
appId: string
|
|
581
|
+
modelId: string
|
|
582
|
+
variant?: string | null
|
|
583
|
+
}): Promise<void> {
|
|
584
|
+
const prisma = await getPrisma()
|
|
585
|
+
await prisma.global_models.upsert({
|
|
586
|
+
where: { app_id: appId },
|
|
587
|
+
create: { app_id: appId, model_id: modelId, variant: variant ?? null },
|
|
588
|
+
update: {
|
|
589
|
+
model_id: modelId,
|
|
590
|
+
variant: variant ?? null,
|
|
591
|
+
updated_at: new Date(),
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ============================================================================
|
|
597
|
+
// Session Model Functions
|
|
598
|
+
// ============================================================================
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Get the model preference for a session.
|
|
602
|
+
* @returns Model ID in format "provider_id/model_id" + optional variant, or undefined
|
|
603
|
+
*/
|
|
604
|
+
export async function getSessionModel(
|
|
605
|
+
sessionId: string,
|
|
606
|
+
): Promise<ModelPreference | undefined> {
|
|
607
|
+
const prisma = await getPrisma()
|
|
608
|
+
const row = await prisma.session_models.findUnique({
|
|
609
|
+
where: { session_id: sessionId },
|
|
610
|
+
})
|
|
611
|
+
if (!row) {
|
|
612
|
+
return undefined
|
|
613
|
+
}
|
|
614
|
+
return { modelId: row.model_id, variant: row.variant }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Set the model preference for a session.
|
|
619
|
+
* @param modelId Model ID in format "provider_id/model_id"
|
|
620
|
+
* @param variant Optional thinking/reasoning variant name
|
|
621
|
+
*/
|
|
622
|
+
export async function setSessionModel({
|
|
623
|
+
sessionId,
|
|
624
|
+
modelId,
|
|
625
|
+
variant,
|
|
626
|
+
}: {
|
|
627
|
+
sessionId: string
|
|
628
|
+
modelId: string
|
|
629
|
+
variant?: string | null
|
|
630
|
+
}): Promise<void> {
|
|
631
|
+
const prisma = await getPrisma()
|
|
632
|
+
await prisma.session_models.upsert({
|
|
633
|
+
where: { session_id: sessionId },
|
|
634
|
+
create: {
|
|
635
|
+
session_id: sessionId,
|
|
636
|
+
model_id: modelId,
|
|
637
|
+
variant: variant ?? null,
|
|
638
|
+
},
|
|
639
|
+
update: { model_id: modelId, variant: variant ?? null },
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Clear the model preference for a session.
|
|
645
|
+
* Used when switching agents so the agent's model takes effect.
|
|
646
|
+
*/
|
|
647
|
+
export async function clearSessionModel(sessionId: string): Promise<void> {
|
|
648
|
+
const prisma = await getPrisma()
|
|
649
|
+
await prisma.session_models.deleteMany({
|
|
650
|
+
where: { session_id: sessionId },
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ============================================================================
|
|
655
|
+
// Variant Cascade Resolution
|
|
656
|
+
// ============================================================================
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Resolve the variant (thinking level) using the session → channel → global cascade.
|
|
660
|
+
* Returns the first non-null variant found, or undefined if none set at any level.
|
|
661
|
+
*/
|
|
662
|
+
export async function getVariantCascade({
|
|
663
|
+
sessionId,
|
|
664
|
+
channelId,
|
|
665
|
+
appId,
|
|
666
|
+
}: {
|
|
667
|
+
sessionId?: string
|
|
668
|
+
channelId?: string
|
|
669
|
+
appId?: string
|
|
670
|
+
}): Promise<string | undefined> {
|
|
671
|
+
if (sessionId) {
|
|
672
|
+
const session = await getSessionModel(sessionId)
|
|
673
|
+
if (session?.variant) {
|
|
674
|
+
return session.variant
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (channelId) {
|
|
678
|
+
const channel = await getChannelModel(channelId)
|
|
679
|
+
if (channel?.variant) {
|
|
680
|
+
return channel.variant
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (appId) {
|
|
684
|
+
const global = await getGlobalModel(appId)
|
|
685
|
+
if (global?.variant) {
|
|
686
|
+
return global.variant
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return undefined
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ============================================================================
|
|
693
|
+
// Channel Agent Functions
|
|
694
|
+
// ============================================================================
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get the agent preference for a channel.
|
|
698
|
+
*/
|
|
699
|
+
export async function getChannelAgent(
|
|
700
|
+
channelId: string,
|
|
701
|
+
): Promise<string | undefined> {
|
|
702
|
+
const prisma = await getPrisma()
|
|
703
|
+
const row = await prisma.channel_agents.findUnique({
|
|
704
|
+
where: { channel_id: channelId },
|
|
705
|
+
})
|
|
706
|
+
return row?.agent_name
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Set the agent preference for a channel.
|
|
711
|
+
*/
|
|
712
|
+
export async function setChannelAgent(
|
|
713
|
+
channelId: string,
|
|
714
|
+
agentName: string,
|
|
715
|
+
): Promise<void> {
|
|
716
|
+
const prisma = await getPrisma()
|
|
717
|
+
await prisma.channel_agents.upsert({
|
|
718
|
+
where: { channel_id: channelId },
|
|
719
|
+
create: { channel_id: channelId, agent_name: agentName },
|
|
720
|
+
update: { agent_name: agentName, updated_at: new Date() },
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ============================================================================
|
|
725
|
+
// Session Agent Functions
|
|
726
|
+
// ============================================================================
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Get the agent preference for a session.
|
|
730
|
+
*/
|
|
731
|
+
export async function getSessionAgent(
|
|
732
|
+
sessionId: string,
|
|
733
|
+
): Promise<string | undefined> {
|
|
734
|
+
const prisma = await getPrisma()
|
|
735
|
+
const row = await prisma.session_agents.findUnique({
|
|
736
|
+
where: { session_id: sessionId },
|
|
737
|
+
})
|
|
738
|
+
return row?.agent_name
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Set the agent preference for a session.
|
|
743
|
+
*/
|
|
744
|
+
export async function setSessionAgent(
|
|
745
|
+
sessionId: string,
|
|
746
|
+
agentName: string,
|
|
747
|
+
): Promise<void> {
|
|
748
|
+
const prisma = await getPrisma()
|
|
749
|
+
await prisma.session_agents.upsert({
|
|
750
|
+
where: { session_id: sessionId },
|
|
751
|
+
create: { session_id: sessionId, agent_name: agentName },
|
|
752
|
+
update: { agent_name: agentName },
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ============================================================================
|
|
757
|
+
// Thread Worktree Functions
|
|
758
|
+
// ============================================================================
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Get the worktree info for a thread.
|
|
762
|
+
*/
|
|
763
|
+
export async function getThreadWorktree(
|
|
764
|
+
threadId: string,
|
|
765
|
+
): Promise<ThreadWorktree | undefined> {
|
|
766
|
+
const prisma = await getPrisma()
|
|
767
|
+
return (await prisma.thread_worktrees.findUnique({
|
|
768
|
+
where: { thread_id: threadId },
|
|
769
|
+
})) ?? undefined
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Create a pending worktree entry for a thread.
|
|
774
|
+
* Ensures the parent thread_sessions row exists first (with empty session_id)
|
|
775
|
+
* to satisfy the FK constraint. The real session_id is set later by setThreadSession().
|
|
776
|
+
*/
|
|
777
|
+
export async function createPendingWorktree({
|
|
778
|
+
threadId,
|
|
779
|
+
worktreeName,
|
|
780
|
+
projectDirectory,
|
|
781
|
+
}: {
|
|
782
|
+
threadId: string
|
|
783
|
+
worktreeName: string
|
|
784
|
+
projectDirectory: string
|
|
785
|
+
}): Promise<void> {
|
|
786
|
+
const prisma = await getPrisma()
|
|
787
|
+
await prisma.$transaction([
|
|
788
|
+
prisma.thread_sessions.upsert({
|
|
789
|
+
where: { thread_id: threadId },
|
|
790
|
+
create: { thread_id: threadId, session_id: '' },
|
|
791
|
+
update: {},
|
|
792
|
+
}),
|
|
793
|
+
prisma.thread_worktrees.upsert({
|
|
794
|
+
where: { thread_id: threadId },
|
|
795
|
+
create: {
|
|
796
|
+
thread_id: threadId,
|
|
797
|
+
worktree_name: worktreeName,
|
|
798
|
+
project_directory: projectDirectory,
|
|
799
|
+
status: 'pending',
|
|
800
|
+
},
|
|
801
|
+
update: {
|
|
802
|
+
worktree_name: worktreeName,
|
|
803
|
+
project_directory: projectDirectory,
|
|
804
|
+
status: 'pending',
|
|
805
|
+
worktree_directory: null,
|
|
806
|
+
error_message: null,
|
|
807
|
+
},
|
|
808
|
+
}),
|
|
809
|
+
])
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Mark a worktree as ready with its directory.
|
|
814
|
+
*/
|
|
815
|
+
export async function setWorktreeReady({
|
|
816
|
+
threadId,
|
|
817
|
+
worktreeDirectory,
|
|
818
|
+
}: {
|
|
819
|
+
threadId: string
|
|
820
|
+
worktreeDirectory: string
|
|
821
|
+
}): Promise<void> {
|
|
822
|
+
const prisma = await getPrisma()
|
|
823
|
+
await prisma.thread_worktrees.update({
|
|
824
|
+
where: { thread_id: threadId },
|
|
825
|
+
data: {
|
|
826
|
+
worktree_directory: worktreeDirectory,
|
|
827
|
+
status: 'ready',
|
|
828
|
+
},
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Mark a worktree as failed with error message.
|
|
834
|
+
*/
|
|
835
|
+
export async function setWorktreeError({
|
|
836
|
+
threadId,
|
|
837
|
+
errorMessage,
|
|
838
|
+
}: {
|
|
839
|
+
threadId: string
|
|
840
|
+
errorMessage: string
|
|
841
|
+
}): Promise<void> {
|
|
842
|
+
const prisma = await getPrisma()
|
|
843
|
+
await prisma.thread_worktrees.update({
|
|
844
|
+
where: { thread_id: threadId },
|
|
845
|
+
data: {
|
|
846
|
+
status: 'error',
|
|
847
|
+
error_message: errorMessage,
|
|
848
|
+
},
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Delete the worktree info for a thread.
|
|
854
|
+
*/
|
|
855
|
+
export async function deleteThreadWorktree(threadId: string): Promise<void> {
|
|
856
|
+
const prisma = await getPrisma()
|
|
857
|
+
await prisma.thread_worktrees.deleteMany({
|
|
858
|
+
where: { thread_id: threadId },
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ============================================================================
|
|
863
|
+
// Channel Verbosity Functions
|
|
864
|
+
// ============================================================================
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Get the verbosity setting for a channel.
|
|
868
|
+
* Falls back to the global default set via --verbosity CLI flag if no per-channel override exists.
|
|
869
|
+
*/
|
|
870
|
+
export async function getChannelVerbosity(
|
|
871
|
+
channelId: string,
|
|
872
|
+
): Promise<VerbosityLevel> {
|
|
873
|
+
const prisma = await getPrisma()
|
|
874
|
+
const row = await prisma.channel_verbosity.findUnique({
|
|
875
|
+
where: { channel_id: channelId },
|
|
876
|
+
})
|
|
877
|
+
if (row?.verbosity) {
|
|
878
|
+
return row.verbosity as VerbosityLevel
|
|
879
|
+
}
|
|
880
|
+
return store.getState().defaultVerbosity
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Set the verbosity setting for a channel.
|
|
885
|
+
*/
|
|
886
|
+
export async function setChannelVerbosity(
|
|
887
|
+
channelId: string,
|
|
888
|
+
verbosity: VerbosityLevel,
|
|
889
|
+
): Promise<void> {
|
|
890
|
+
const prisma = await getPrisma()
|
|
891
|
+
await prisma.channel_verbosity.upsert({
|
|
892
|
+
where: { channel_id: channelId },
|
|
893
|
+
create: { channel_id: channelId, verbosity },
|
|
894
|
+
update: { verbosity, updated_at: new Date() },
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ============================================================================
|
|
899
|
+
// Channel Mention Mode Functions
|
|
900
|
+
// ============================================================================
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Get the mention mode setting for a channel.
|
|
904
|
+
* Falls back to the global default set via --mention-mode CLI flag if no per-channel override exists.
|
|
905
|
+
*/
|
|
906
|
+
export async function getChannelMentionMode(
|
|
907
|
+
channelId: string,
|
|
908
|
+
): Promise<boolean> {
|
|
909
|
+
const prisma = await getPrisma()
|
|
910
|
+
const row = await prisma.channel_mention_mode.findUnique({
|
|
911
|
+
where: { channel_id: channelId },
|
|
912
|
+
})
|
|
913
|
+
if (row) {
|
|
914
|
+
return row.enabled === 1
|
|
915
|
+
}
|
|
916
|
+
return store.getState().defaultMentionMode
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Set the mention mode setting for a channel.
|
|
921
|
+
*/
|
|
922
|
+
export async function setChannelMentionMode(
|
|
923
|
+
channelId: string,
|
|
924
|
+
enabled: boolean,
|
|
925
|
+
): Promise<void> {
|
|
926
|
+
const prisma = await getPrisma()
|
|
927
|
+
await prisma.channel_mention_mode.upsert({
|
|
928
|
+
where: { channel_id: channelId },
|
|
929
|
+
create: { channel_id: channelId, enabled: enabled ? 1 : 0 },
|
|
930
|
+
update: { enabled: enabled ? 1 : 0, updated_at: new Date() },
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ============================================================================
|
|
935
|
+
// Channel Worktree Settings Functions
|
|
936
|
+
// ============================================================================
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Check if automatic worktree creation is enabled for a channel.
|
|
940
|
+
*/
|
|
941
|
+
export async function getChannelWorktreesEnabled(
|
|
942
|
+
channelId: string,
|
|
943
|
+
): Promise<boolean> {
|
|
944
|
+
const prisma = await getPrisma()
|
|
945
|
+
const row = await prisma.channel_worktrees.findUnique({
|
|
946
|
+
where: { channel_id: channelId },
|
|
947
|
+
})
|
|
948
|
+
return row?.enabled === 1
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Enable or disable automatic worktree creation for a channel.
|
|
953
|
+
*/
|
|
954
|
+
export async function setChannelWorktreesEnabled(
|
|
955
|
+
channelId: string,
|
|
956
|
+
enabled: boolean,
|
|
957
|
+
): Promise<void> {
|
|
958
|
+
const prisma = await getPrisma()
|
|
959
|
+
await prisma.channel_worktrees.upsert({
|
|
960
|
+
where: { channel_id: channelId },
|
|
961
|
+
create: { channel_id: channelId, enabled: enabled ? 1 : 0 },
|
|
962
|
+
update: { enabled: enabled ? 1 : 0, updated_at: new Date() },
|
|
963
|
+
})
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ============================================================================
|
|
967
|
+
// Channel Directory Functions
|
|
968
|
+
// ============================================================================
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Get the directory for a channel from the database.
|
|
972
|
+
* This is the single source of truth for channel-project mappings.
|
|
973
|
+
*/
|
|
974
|
+
export async function getChannelDirectory(channelId: string): Promise<
|
|
975
|
+
| {
|
|
976
|
+
directory: string
|
|
977
|
+
}
|
|
978
|
+
| undefined
|
|
979
|
+
> {
|
|
980
|
+
const prisma = await getPrisma()
|
|
981
|
+
const row = await prisma.channel_directories.findUnique({
|
|
982
|
+
where: { channel_id: channelId },
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
if (!row) {
|
|
986
|
+
return undefined
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return {
|
|
990
|
+
directory: row.directory,
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ============================================================================
|
|
995
|
+
// Thread Session Functions
|
|
996
|
+
// ============================================================================
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Get the session ID for a thread.
|
|
1000
|
+
*/
|
|
1001
|
+
export async function getThreadSession(
|
|
1002
|
+
threadId: string,
|
|
1003
|
+
): Promise<string | undefined> {
|
|
1004
|
+
const prisma = await getPrisma()
|
|
1005
|
+
const row = await prisma.thread_sessions.findUnique({
|
|
1006
|
+
where: { thread_id: threadId },
|
|
1007
|
+
})
|
|
1008
|
+
return row?.session_id
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Set the session ID for a thread.
|
|
1013
|
+
*/
|
|
1014
|
+
export async function setThreadSession(
|
|
1015
|
+
threadId: string,
|
|
1016
|
+
sessionId: string,
|
|
1017
|
+
): Promise<void> {
|
|
1018
|
+
await upsertThreadSession({
|
|
1019
|
+
threadId,
|
|
1020
|
+
sessionId,
|
|
1021
|
+
source: 'kimaki',
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export async function upsertThreadSession({
|
|
1026
|
+
threadId,
|
|
1027
|
+
sessionId,
|
|
1028
|
+
source,
|
|
1029
|
+
}: {
|
|
1030
|
+
threadId: string
|
|
1031
|
+
sessionId: string
|
|
1032
|
+
source: ThreadSessionSource
|
|
1033
|
+
}): Promise<void> {
|
|
1034
|
+
const prisma = await getPrisma()
|
|
1035
|
+
await prisma.thread_sessions.upsert({
|
|
1036
|
+
where: { thread_id: threadId },
|
|
1037
|
+
create: {
|
|
1038
|
+
thread_id: threadId,
|
|
1039
|
+
session_id: sessionId,
|
|
1040
|
+
source,
|
|
1041
|
+
},
|
|
1042
|
+
update: {
|
|
1043
|
+
session_id: sessionId,
|
|
1044
|
+
source,
|
|
1045
|
+
},
|
|
1046
|
+
})
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export async function getThreadSessionSource(
|
|
1050
|
+
threadId: string,
|
|
1051
|
+
): Promise<ThreadSessionSource | undefined> {
|
|
1052
|
+
const prisma = await getPrisma()
|
|
1053
|
+
const row = await prisma.thread_sessions.findUnique({
|
|
1054
|
+
where: { thread_id: threadId },
|
|
1055
|
+
select: { source: true },
|
|
1056
|
+
})
|
|
1057
|
+
return row?.source
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Get the thread ID for a session.
|
|
1062
|
+
*/
|
|
1063
|
+
export async function getThreadIdBySessionId(
|
|
1064
|
+
sessionId: string,
|
|
1065
|
+
): Promise<string | undefined> {
|
|
1066
|
+
const prisma = await getPrisma()
|
|
1067
|
+
const row = await prisma.thread_sessions.findFirst({
|
|
1068
|
+
where: { session_id: sessionId },
|
|
1069
|
+
})
|
|
1070
|
+
return row?.thread_id
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Get all session IDs that are associated with threads.
|
|
1075
|
+
*/
|
|
1076
|
+
export async function getAllThreadSessionIds(): Promise<string[]> {
|
|
1077
|
+
const prisma = await getPrisma()
|
|
1078
|
+
const rows = await prisma.thread_sessions.findMany({
|
|
1079
|
+
select: { session_id: true },
|
|
1080
|
+
})
|
|
1081
|
+
return rows.map((row) => row.session_id).filter((id) => id !== '')
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export async function appendSessionEventsSinceLastTimestamp({
|
|
1085
|
+
sessionId,
|
|
1086
|
+
events,
|
|
1087
|
+
}: {
|
|
1088
|
+
sessionId: string
|
|
1089
|
+
events: Prisma.session_eventsCreateManyInput[]
|
|
1090
|
+
}): Promise<number> {
|
|
1091
|
+
if (events.length === 0) {
|
|
1092
|
+
return 0
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const prisma = await getPrisma()
|
|
1096
|
+
const sortedEvents = [...events]
|
|
1097
|
+
.sort((a, b) => {
|
|
1098
|
+
if (a.timestamp < b.timestamp) {
|
|
1099
|
+
return -1
|
|
1100
|
+
}
|
|
1101
|
+
if (a.timestamp > b.timestamp) {
|
|
1102
|
+
return 1
|
|
1103
|
+
}
|
|
1104
|
+
if (a.event_index < b.event_index) {
|
|
1105
|
+
return -1
|
|
1106
|
+
}
|
|
1107
|
+
if (a.event_index > b.event_index) {
|
|
1108
|
+
return 1
|
|
1109
|
+
}
|
|
1110
|
+
return 0
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
const latestPersisted = await prisma.session_events.findFirst({
|
|
1114
|
+
where: {
|
|
1115
|
+
session_id: sessionId,
|
|
1116
|
+
},
|
|
1117
|
+
orderBy: [{ timestamp: 'desc' }, { event_index: 'desc' }, { id: 'desc' }],
|
|
1118
|
+
select: {
|
|
1119
|
+
timestamp: true,
|
|
1120
|
+
event_index: true,
|
|
1121
|
+
},
|
|
1122
|
+
})
|
|
1123
|
+
|
|
1124
|
+
const eventsToInsert = sortedEvents.filter((event) => {
|
|
1125
|
+
if (!latestPersisted) {
|
|
1126
|
+
return true
|
|
1127
|
+
}
|
|
1128
|
+
if (event.timestamp > latestPersisted.timestamp) {
|
|
1129
|
+
return true
|
|
1130
|
+
}
|
|
1131
|
+
if (event.timestamp < latestPersisted.timestamp) {
|
|
1132
|
+
return false
|
|
1133
|
+
}
|
|
1134
|
+
return event.event_index > latestPersisted.event_index
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
if (eventsToInsert.length === 0) {
|
|
1138
|
+
return 0
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
await prisma.$transaction(async (tx) => {
|
|
1142
|
+
await tx.session_events.createMany({
|
|
1143
|
+
data: eventsToInsert,
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
const staleRows = await tx.session_events.findMany({
|
|
1147
|
+
where: {
|
|
1148
|
+
session_id: sessionId,
|
|
1149
|
+
},
|
|
1150
|
+
orderBy: [{ timestamp: 'desc' }, { event_index: 'desc' }, { id: 'desc' }],
|
|
1151
|
+
skip: 1000,
|
|
1152
|
+
select: {
|
|
1153
|
+
id: true,
|
|
1154
|
+
},
|
|
1155
|
+
})
|
|
1156
|
+
if (staleRows.length === 0) {
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
await tx.session_events.deleteMany({
|
|
1161
|
+
where: {
|
|
1162
|
+
id: {
|
|
1163
|
+
in: staleRows.map((row) => {
|
|
1164
|
+
return row.id
|
|
1165
|
+
}),
|
|
1166
|
+
},
|
|
1167
|
+
},
|
|
1168
|
+
})
|
|
1169
|
+
})
|
|
1170
|
+
|
|
1171
|
+
return eventsToInsert.length
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export async function getSessionEventSnapshot({
|
|
1175
|
+
sessionId,
|
|
1176
|
+
}: {
|
|
1177
|
+
sessionId: string
|
|
1178
|
+
}): Promise<session_events[]> {
|
|
1179
|
+
const prisma = await getPrisma()
|
|
1180
|
+
return prisma.session_events.findMany({
|
|
1181
|
+
where: {
|
|
1182
|
+
session_id: sessionId,
|
|
1183
|
+
},
|
|
1184
|
+
orderBy: [{ timestamp: 'asc' }, { event_index: 'asc' }, { id: 'asc' }],
|
|
1185
|
+
take: 1000,
|
|
1186
|
+
})
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ============================================================================
|
|
1190
|
+
// Part Messages Functions
|
|
1191
|
+
// ============================================================================
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Get all part IDs for a thread.
|
|
1195
|
+
*/
|
|
1196
|
+
export async function getPartMessageIds(threadId: string): Promise<string[]> {
|
|
1197
|
+
const prisma = await getPrisma()
|
|
1198
|
+
const rows = await prisma.part_messages.findMany({
|
|
1199
|
+
where: { thread_id: threadId },
|
|
1200
|
+
select: { part_id: true },
|
|
1201
|
+
})
|
|
1202
|
+
return rows.map((row) => row.part_id)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Store a part-message mapping.
|
|
1207
|
+
* Note: The thread must already have a session (via setThreadSession) before calling this.
|
|
1208
|
+
*/
|
|
1209
|
+
export async function setPartMessage(
|
|
1210
|
+
partId: string,
|
|
1211
|
+
messageId: string,
|
|
1212
|
+
threadId: string,
|
|
1213
|
+
): Promise<void> {
|
|
1214
|
+
const prisma = await getPrisma()
|
|
1215
|
+
await prisma.part_messages.upsert({
|
|
1216
|
+
where: { part_id: partId },
|
|
1217
|
+
create: { part_id: partId, message_id: messageId, thread_id: threadId },
|
|
1218
|
+
update: { message_id: messageId, thread_id: threadId },
|
|
1219
|
+
})
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Store multiple part-message mappings in a transaction.
|
|
1224
|
+
* More efficient and atomic for batch operations.
|
|
1225
|
+
* Note: The thread must already have a session (via setThreadSession) before calling this.
|
|
1226
|
+
*/
|
|
1227
|
+
export async function setPartMessagesBatch(
|
|
1228
|
+
partMappings: Array<{ partId: string; messageId: string; threadId: string }>,
|
|
1229
|
+
): Promise<void> {
|
|
1230
|
+
if (partMappings.length === 0) {
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
const prisma = await getPrisma()
|
|
1234
|
+
await prisma.$transaction(
|
|
1235
|
+
partMappings.map(({ partId, messageId, threadId }) => {
|
|
1236
|
+
return prisma.part_messages.upsert({
|
|
1237
|
+
where: { part_id: partId },
|
|
1238
|
+
create: { part_id: partId, message_id: messageId, thread_id: threadId },
|
|
1239
|
+
update: { message_id: messageId, thread_id: threadId },
|
|
1240
|
+
})
|
|
1241
|
+
}),
|
|
1242
|
+
)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// ============================================================================
|
|
1246
|
+
// Bot Token Functions
|
|
1247
|
+
// ============================================================================
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Get the bot token to use, with mode info, in a single query.
|
|
1251
|
+
*
|
|
1252
|
+
* Selection logic (when multiple bot rows exist):
|
|
1253
|
+
* - If only one bot exists, use it regardless of mode.
|
|
1254
|
+
* - Picks the bot row with the most recent `last_used_at` timestamp, which is
|
|
1255
|
+
* set by the `run()` command when the bot starts. This ensures subcommands
|
|
1256
|
+
* in separate processes (send, project list, etc.) automatically use
|
|
1257
|
+
* whichever bot mode (gateway or self-hosted) was last started.
|
|
1258
|
+
* - Falls back to `created_at` ordering when no row has `last_used_at` set
|
|
1259
|
+
* (backward compat for existing DBs before this column was added).
|
|
1260
|
+
*
|
|
1261
|
+
* For gateway mode, the token is derived from client_id:client_secret
|
|
1262
|
+
* and REST routing is automatically enabled (idempotent env var set).
|
|
1263
|
+
* This ensures every code path that reads credentials gets correct routing
|
|
1264
|
+
* without needing to set discordBaseUrl separately.
|
|
1265
|
+
*/
|
|
1266
|
+
export async function getBotTokenWithMode(): Promise<
|
|
1267
|
+
| {
|
|
1268
|
+
appId: string
|
|
1269
|
+
token: string
|
|
1270
|
+
gatewayToken: string
|
|
1271
|
+
mode: BotMode
|
|
1272
|
+
clientId: string | null
|
|
1273
|
+
clientSecret: string | null
|
|
1274
|
+
proxyUrl: string | null
|
|
1275
|
+
}
|
|
1276
|
+
| undefined
|
|
1277
|
+
> {
|
|
1278
|
+
const prisma = await getPrisma()
|
|
1279
|
+
// Pick the bot that was most recently started via run(). last_used_at is the
|
|
1280
|
+
// cross-process source of truth — no in-memory flags needed.
|
|
1281
|
+
// Fall back to created_at for DBs that predate the last_used_at column.
|
|
1282
|
+
const allBots = await prisma.bot_tokens.findMany({
|
|
1283
|
+
orderBy: [{ last_used_at: 'desc' }, { created_at: 'desc' }],
|
|
1284
|
+
})
|
|
1285
|
+
const row = allBots[0]
|
|
1286
|
+
if (!row) {
|
|
1287
|
+
return undefined
|
|
1288
|
+
}
|
|
1289
|
+
const gatewayToken = await ensureServiceAuthToken({ appId: row.app_id })
|
|
1290
|
+
const serviceParts = splitServiceAuthToken({ token: gatewayToken })
|
|
1291
|
+
const mode: BotMode = row.bot_mode === 'gateway' ? 'gateway' : 'self_hosted'
|
|
1292
|
+
const token = (mode === 'gateway' && serviceParts)
|
|
1293
|
+
? gatewayToken
|
|
1294
|
+
: row.token
|
|
1295
|
+
// Always reset discordBaseUrl on every read so a mode switch within
|
|
1296
|
+
// the same process (e.g. DB has gateway row but user proceeds self-hosted)
|
|
1297
|
+
// doesn't leave a stale proxy URL in the store.
|
|
1298
|
+
const discordBaseUrl = (mode === 'gateway' && row.proxy_url)
|
|
1299
|
+
? row.proxy_url
|
|
1300
|
+
: 'https://discord.com'
|
|
1301
|
+
store.setState({ discordBaseUrl, gatewayToken })
|
|
1302
|
+
return {
|
|
1303
|
+
appId: row.app_id,
|
|
1304
|
+
token,
|
|
1305
|
+
gatewayToken,
|
|
1306
|
+
mode,
|
|
1307
|
+
clientId: serviceParts?.clientId || row.client_id,
|
|
1308
|
+
clientSecret: serviceParts?.clientSecret || row.client_secret,
|
|
1309
|
+
proxyUrl: row.proxy_url,
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function splitServiceAuthToken({ token }: { token: string }): { clientId: string; clientSecret: string } | null {
|
|
1314
|
+
const separatorIndex = token.indexOf(':')
|
|
1315
|
+
if (separatorIndex <= 0 || separatorIndex >= token.length - 1) {
|
|
1316
|
+
return null
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
clientId: token.slice(0, separatorIndex),
|
|
1320
|
+
clientSecret: token.slice(separatorIndex + 1),
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function createServiceCredentials(): { clientId: string; clientSecret: string } {
|
|
1325
|
+
return {
|
|
1326
|
+
clientId: crypto.randomUUID(),
|
|
1327
|
+
clientSecret: crypto.randomBytes(32).toString('hex'),
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
export async function ensureServiceAuthToken({
|
|
1332
|
+
appId,
|
|
1333
|
+
preferredGatewayToken,
|
|
1334
|
+
}: {
|
|
1335
|
+
appId: string
|
|
1336
|
+
preferredGatewayToken?: string
|
|
1337
|
+
}): Promise<string> {
|
|
1338
|
+
const prisma = await getPrisma()
|
|
1339
|
+
const row = await prisma.bot_tokens.findUnique({
|
|
1340
|
+
where: { app_id: appId },
|
|
1341
|
+
})
|
|
1342
|
+
if (!row) {
|
|
1343
|
+
throw new Error(`Bot token row not found for app_id ${appId}`)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const preferred = preferredGatewayToken
|
|
1347
|
+
? splitServiceAuthToken({ token: preferredGatewayToken })
|
|
1348
|
+
: null
|
|
1349
|
+
const existing = (row.client_id && row.client_secret)
|
|
1350
|
+
? { clientId: row.client_id, clientSecret: row.client_secret }
|
|
1351
|
+
: null
|
|
1352
|
+
const fromStoredToken = splitServiceAuthToken({ token: row.token })
|
|
1353
|
+
const resolved = preferred || existing || fromStoredToken || createServiceCredentials()
|
|
1354
|
+
|
|
1355
|
+
if (row.client_id !== resolved.clientId || row.client_secret !== resolved.clientSecret) {
|
|
1356
|
+
await prisma.bot_tokens.update({
|
|
1357
|
+
where: { app_id: appId },
|
|
1358
|
+
data: {
|
|
1359
|
+
client_id: resolved.clientId,
|
|
1360
|
+
client_secret: resolved.clientSecret,
|
|
1361
|
+
},
|
|
1362
|
+
})
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
return `${resolved.clientId}:${resolved.clientSecret}`
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Store a bot token.
|
|
1370
|
+
*/
|
|
1371
|
+
export async function setBotToken(appId: string, token: string): Promise<void> {
|
|
1372
|
+
const prisma = await getPrisma()
|
|
1373
|
+
const generated = createServiceCredentials()
|
|
1374
|
+
await prisma.bot_tokens.upsert({
|
|
1375
|
+
where: { app_id: appId },
|
|
1376
|
+
create: {
|
|
1377
|
+
app_id: appId,
|
|
1378
|
+
token,
|
|
1379
|
+
client_id: generated.clientId,
|
|
1380
|
+
client_secret: generated.clientSecret,
|
|
1381
|
+
},
|
|
1382
|
+
update: { token },
|
|
1383
|
+
})
|
|
1384
|
+
await ensureServiceAuthToken({ appId })
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
export type { BotMode }
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Persist gateway bot mode credentials.
|
|
1391
|
+
* Upserts the row so a prior setBotToken call is not needed.
|
|
1392
|
+
*/
|
|
1393
|
+
export async function setBotMode({
|
|
1394
|
+
appId,
|
|
1395
|
+
mode,
|
|
1396
|
+
clientId,
|
|
1397
|
+
clientSecret,
|
|
1398
|
+
proxyUrl,
|
|
1399
|
+
}: {
|
|
1400
|
+
appId: string
|
|
1401
|
+
mode: BotMode
|
|
1402
|
+
clientId?: string | null
|
|
1403
|
+
clientSecret?: string | null
|
|
1404
|
+
proxyUrl?: string | null
|
|
1405
|
+
}): Promise<void> {
|
|
1406
|
+
const prisma = await getPrisma()
|
|
1407
|
+
const data = {
|
|
1408
|
+
bot_mode: mode,
|
|
1409
|
+
client_id: clientId ?? null,
|
|
1410
|
+
client_secret: clientSecret ?? null,
|
|
1411
|
+
proxy_url: proxyUrl ?? null,
|
|
1412
|
+
}
|
|
1413
|
+
const createToken = (clientId && clientSecret) ? `${clientId}:${clientSecret}` : ''
|
|
1414
|
+
await prisma.bot_tokens.upsert({
|
|
1415
|
+
where: { app_id: appId },
|
|
1416
|
+
create: { app_id: appId, token: createToken, ...data },
|
|
1417
|
+
update: data,
|
|
1418
|
+
})
|
|
1419
|
+
await ensureServiceAuthToken({
|
|
1420
|
+
appId,
|
|
1421
|
+
preferredGatewayToken: (clientId && clientSecret) ? `${clientId}:${clientSecret}` : undefined,
|
|
1422
|
+
})
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
// ============================================================================
|
|
1428
|
+
// Bot API Keys Functions
|
|
1429
|
+
// ============================================================================
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Get the Gemini API key for a bot.
|
|
1433
|
+
*/
|
|
1434
|
+
export async function getGeminiApiKey(appId: string): Promise<string | null> {
|
|
1435
|
+
const prisma = await getPrisma()
|
|
1436
|
+
const row = await prisma.bot_api_keys.findUnique({
|
|
1437
|
+
where: { app_id: appId },
|
|
1438
|
+
})
|
|
1439
|
+
return row?.gemini_api_key ?? null
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Set the Gemini API key for a bot.
|
|
1444
|
+
* Note: The bot must already have a token (via setBotToken) before calling this.
|
|
1445
|
+
*/
|
|
1446
|
+
export async function setGeminiApiKey(
|
|
1447
|
+
appId: string,
|
|
1448
|
+
apiKey: string,
|
|
1449
|
+
): Promise<void> {
|
|
1450
|
+
const prisma = await getPrisma()
|
|
1451
|
+
await prisma.bot_api_keys.upsert({
|
|
1452
|
+
where: { app_id: appId },
|
|
1453
|
+
create: { app_id: appId, gemini_api_key: apiKey },
|
|
1454
|
+
update: { gemini_api_key: apiKey },
|
|
1455
|
+
})
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Get the OpenAI API key for a bot.
|
|
1460
|
+
*/
|
|
1461
|
+
export async function getOpenAIApiKey(appId: string): Promise<string | null> {
|
|
1462
|
+
const prisma = await getPrisma()
|
|
1463
|
+
const row = await prisma.bot_api_keys.findUnique({
|
|
1464
|
+
where: { app_id: appId },
|
|
1465
|
+
})
|
|
1466
|
+
return row?.openai_api_key ?? null
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Set the OpenAI API key for a bot.
|
|
1471
|
+
*/
|
|
1472
|
+
export async function setOpenAIApiKey(
|
|
1473
|
+
appId: string,
|
|
1474
|
+
apiKey: string,
|
|
1475
|
+
): Promise<void> {
|
|
1476
|
+
const prisma = await getPrisma()
|
|
1477
|
+
await prisma.bot_api_keys.upsert({
|
|
1478
|
+
where: { app_id: appId },
|
|
1479
|
+
create: { app_id: appId, openai_api_key: apiKey },
|
|
1480
|
+
update: { openai_api_key: apiKey },
|
|
1481
|
+
})
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* Get the best available transcription API key for a bot.
|
|
1486
|
+
* Prefers OpenAI, falls back to Gemini.
|
|
1487
|
+
*/
|
|
1488
|
+
export async function getTranscriptionApiKey(
|
|
1489
|
+
appId: string,
|
|
1490
|
+
): Promise<{ provider: 'openai' | 'gemini'; apiKey: string } | null> {
|
|
1491
|
+
const prisma = await getPrisma()
|
|
1492
|
+
const row = await prisma.bot_api_keys.findUnique({
|
|
1493
|
+
where: { app_id: appId },
|
|
1494
|
+
})
|
|
1495
|
+
if (!row) return null
|
|
1496
|
+
if (row.openai_api_key) {
|
|
1497
|
+
return { provider: 'openai', apiKey: row.openai_api_key }
|
|
1498
|
+
}
|
|
1499
|
+
if (row.gemini_api_key) {
|
|
1500
|
+
return { provider: 'gemini', apiKey: row.gemini_api_key }
|
|
1501
|
+
}
|
|
1502
|
+
return null
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// ============================================================================
|
|
1506
|
+
// Channel Directory CRUD Functions
|
|
1507
|
+
// ============================================================================
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Store a channel-directory mapping.
|
|
1511
|
+
* @param skipIfExists If true, behaves like INSERT OR IGNORE - skips if record exists.
|
|
1512
|
+
* If false (default), behaves like INSERT OR REPLACE - updates if exists.
|
|
1513
|
+
*/
|
|
1514
|
+
export async function setChannelDirectory({
|
|
1515
|
+
channelId,
|
|
1516
|
+
directory,
|
|
1517
|
+
channelType,
|
|
1518
|
+
skipIfExists = false,
|
|
1519
|
+
}: {
|
|
1520
|
+
channelId: string
|
|
1521
|
+
directory: string
|
|
1522
|
+
channelType: PrismaChannelType
|
|
1523
|
+
skipIfExists?: boolean
|
|
1524
|
+
}): Promise<void> {
|
|
1525
|
+
const prisma = await getPrisma()
|
|
1526
|
+
if (skipIfExists) {
|
|
1527
|
+
// INSERT OR IGNORE semantics - only insert if not exists
|
|
1528
|
+
const existing = await prisma.channel_directories.findUnique({
|
|
1529
|
+
where: { channel_id: channelId },
|
|
1530
|
+
})
|
|
1531
|
+
if (existing) {
|
|
1532
|
+
return
|
|
1533
|
+
}
|
|
1534
|
+
await prisma.channel_directories.create({
|
|
1535
|
+
data: {
|
|
1536
|
+
channel_id: channelId,
|
|
1537
|
+
directory,
|
|
1538
|
+
channel_type: channelType,
|
|
1539
|
+
},
|
|
1540
|
+
})
|
|
1541
|
+
} else {
|
|
1542
|
+
// INSERT OR REPLACE semantics - upsert
|
|
1543
|
+
await prisma.channel_directories.upsert({
|
|
1544
|
+
where: { channel_id: channelId },
|
|
1545
|
+
create: {
|
|
1546
|
+
channel_id: channelId,
|
|
1547
|
+
directory,
|
|
1548
|
+
channel_type: channelType,
|
|
1549
|
+
},
|
|
1550
|
+
update: {
|
|
1551
|
+
directory,
|
|
1552
|
+
channel_type: channelType,
|
|
1553
|
+
},
|
|
1554
|
+
})
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Find channels by directory path.
|
|
1560
|
+
*/
|
|
1561
|
+
export async function findChannelsByDirectory({
|
|
1562
|
+
directory,
|
|
1563
|
+
channelType,
|
|
1564
|
+
}: {
|
|
1565
|
+
directory?: string
|
|
1566
|
+
channelType?: PrismaChannelType
|
|
1567
|
+
}): Promise<
|
|
1568
|
+
Array<{ channel_id: string; directory: string; channel_type: string }>
|
|
1569
|
+
> {
|
|
1570
|
+
const prisma = await getPrisma()
|
|
1571
|
+
const where: {
|
|
1572
|
+
directory?: string
|
|
1573
|
+
channel_type?: PrismaChannelType
|
|
1574
|
+
} = {}
|
|
1575
|
+
if (directory) {
|
|
1576
|
+
where.directory = directory
|
|
1577
|
+
}
|
|
1578
|
+
if (channelType) {
|
|
1579
|
+
where.channel_type = channelType
|
|
1580
|
+
}
|
|
1581
|
+
const rows = await prisma.channel_directories.findMany({
|
|
1582
|
+
where,
|
|
1583
|
+
select: { channel_id: true, directory: true, channel_type: true },
|
|
1584
|
+
})
|
|
1585
|
+
return rows
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Get all distinct directories with text channels.
|
|
1590
|
+
*/
|
|
1591
|
+
export async function getAllTextChannelDirectories(): Promise<string[]> {
|
|
1592
|
+
const prisma = await getPrisma()
|
|
1593
|
+
const rows = await prisma.channel_directories.findMany({
|
|
1594
|
+
where: { channel_type: 'text' },
|
|
1595
|
+
select: { directory: true },
|
|
1596
|
+
distinct: ['directory'],
|
|
1597
|
+
})
|
|
1598
|
+
return rows.map((row) => row.directory)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
export async function listTrackedTextChannels(): Promise<
|
|
1602
|
+
Array<{ channel_id: string; directory: string; created_at: Date | null }>
|
|
1603
|
+
> {
|
|
1604
|
+
const prisma = await getPrisma()
|
|
1605
|
+
return prisma.channel_directories.findMany({
|
|
1606
|
+
where: { channel_type: 'text' },
|
|
1607
|
+
orderBy: [{ created_at: 'asc' }, { channel_id: 'asc' }],
|
|
1608
|
+
select: { channel_id: true, directory: true, created_at: true },
|
|
1609
|
+
})
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Delete all channel directories for a specific directory.
|
|
1614
|
+
*/
|
|
1615
|
+
export async function deleteChannelDirectoriesByDirectory(
|
|
1616
|
+
directory: string,
|
|
1617
|
+
): Promise<void> {
|
|
1618
|
+
const prisma = await getPrisma()
|
|
1619
|
+
await prisma.channel_directories.deleteMany({
|
|
1620
|
+
where: { directory },
|
|
1621
|
+
})
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Delete a single channel_directories row and all its child rows
|
|
1626
|
+
* (channel_models, channel_agents, channel_worktrees, channel_verbosity,
|
|
1627
|
+
* channel_mention_mode) in a single transaction. scheduled_tasks has
|
|
1628
|
+
* onDelete:SetNull so Prisma handles it automatically.
|
|
1629
|
+
*/
|
|
1630
|
+
export async function deleteChannelDirectoryById(
|
|
1631
|
+
channelId: string,
|
|
1632
|
+
): Promise<boolean> {
|
|
1633
|
+
const prisma = await getPrisma()
|
|
1634
|
+
const deletedCount = await prisma.$transaction(async (tx) => {
|
|
1635
|
+
await tx.channel_models.deleteMany({ where: { channel_id: channelId } })
|
|
1636
|
+
await tx.channel_agents.deleteMany({ where: { channel_id: channelId } })
|
|
1637
|
+
await tx.channel_worktrees.deleteMany({ where: { channel_id: channelId } })
|
|
1638
|
+
await tx.channel_verbosity.deleteMany({ where: { channel_id: channelId } })
|
|
1639
|
+
await tx.channel_mention_mode.deleteMany({ where: { channel_id: channelId } })
|
|
1640
|
+
const result = await tx.channel_directories.deleteMany({
|
|
1641
|
+
where: { channel_id: channelId },
|
|
1642
|
+
})
|
|
1643
|
+
return result.count
|
|
1644
|
+
})
|
|
1645
|
+
return deletedCount > 0
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Get the directory for a voice channel.
|
|
1650
|
+
*/
|
|
1651
|
+
export async function getVoiceChannelDirectory(
|
|
1652
|
+
channelId: string,
|
|
1653
|
+
): Promise<string | undefined> {
|
|
1654
|
+
const prisma = await getPrisma()
|
|
1655
|
+
const row = await prisma.channel_directories.findFirst({
|
|
1656
|
+
where: { channel_id: channelId, channel_type: 'voice' },
|
|
1657
|
+
})
|
|
1658
|
+
return row?.directory
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Find the text channel ID that shares the same directory as a voice channel.
|
|
1663
|
+
* Used to send error messages to text channels from voice handlers.
|
|
1664
|
+
*/
|
|
1665
|
+
export async function findTextChannelByVoiceChannel(
|
|
1666
|
+
voiceChannelId: string,
|
|
1667
|
+
): Promise<string | undefined> {
|
|
1668
|
+
const prisma = await getPrisma()
|
|
1669
|
+
// First get the directory for the voice channel
|
|
1670
|
+
const voiceChannel = await prisma.channel_directories.findFirst({
|
|
1671
|
+
where: { channel_id: voiceChannelId, channel_type: 'voice' },
|
|
1672
|
+
})
|
|
1673
|
+
if (!voiceChannel) {
|
|
1674
|
+
return undefined
|
|
1675
|
+
}
|
|
1676
|
+
// Then find the text channel with the same directory
|
|
1677
|
+
const textChannel = await prisma.channel_directories.findFirst({
|
|
1678
|
+
where: { directory: voiceChannel.directory, channel_type: 'text' },
|
|
1679
|
+
})
|
|
1680
|
+
return textChannel?.channel_id
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// ============================================================================
|
|
1684
|
+
// Forum Sync Config Functions
|
|
1685
|
+
// ============================================================================
|
|
1686
|
+
|
|
1687
|
+
export type ForumSyncConfigRow = {
|
|
1688
|
+
appId: string
|
|
1689
|
+
forumChannelId: string
|
|
1690
|
+
outputDir: string
|
|
1691
|
+
direction: string
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
export async function getForumSyncConfigs({
|
|
1695
|
+
appId,
|
|
1696
|
+
}: {
|
|
1697
|
+
appId: string
|
|
1698
|
+
}): Promise<ForumSyncConfigRow[]> {
|
|
1699
|
+
const prisma = await getPrisma()
|
|
1700
|
+
const rows = await prisma.forum_sync_configs.findMany({
|
|
1701
|
+
where: { app_id: appId },
|
|
1702
|
+
})
|
|
1703
|
+
return rows.map((row) => ({
|
|
1704
|
+
appId: row.app_id,
|
|
1705
|
+
forumChannelId: row.forum_channel_id,
|
|
1706
|
+
outputDir: row.output_dir,
|
|
1707
|
+
direction: row.direction,
|
|
1708
|
+
}))
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
export async function upsertForumSyncConfig({
|
|
1712
|
+
appId,
|
|
1713
|
+
forumChannelId,
|
|
1714
|
+
outputDir,
|
|
1715
|
+
direction = 'bidirectional',
|
|
1716
|
+
}: {
|
|
1717
|
+
appId: string
|
|
1718
|
+
forumChannelId: string
|
|
1719
|
+
outputDir: string
|
|
1720
|
+
direction?: string
|
|
1721
|
+
}) {
|
|
1722
|
+
const prisma = await getPrisma()
|
|
1723
|
+
await prisma.forum_sync_configs.upsert({
|
|
1724
|
+
where: {
|
|
1725
|
+
app_id_forum_channel_id: {
|
|
1726
|
+
app_id: appId,
|
|
1727
|
+
forum_channel_id: forumChannelId,
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1730
|
+
update: { output_dir: outputDir, direction },
|
|
1731
|
+
create: {
|
|
1732
|
+
app_id: appId,
|
|
1733
|
+
forum_channel_id: forumChannelId,
|
|
1734
|
+
output_dir: outputDir,
|
|
1735
|
+
direction,
|
|
1736
|
+
},
|
|
1737
|
+
})
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
export async function deleteForumSyncConfig({
|
|
1741
|
+
appId,
|
|
1742
|
+
forumChannelId,
|
|
1743
|
+
}: {
|
|
1744
|
+
appId: string
|
|
1745
|
+
forumChannelId: string
|
|
1746
|
+
}) {
|
|
1747
|
+
const prisma = await getPrisma()
|
|
1748
|
+
await prisma.forum_sync_configs.deleteMany({
|
|
1749
|
+
where: { app_id: appId, forum_channel_id: forumChannelId },
|
|
1750
|
+
})
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/** Delete forum sync configs that share the same outputDir but have a different forumChannelId.
|
|
1754
|
+
* This cleans up stale entries left behind when a forum channel is deleted and recreated. */
|
|
1755
|
+
export async function deleteStaleForumSyncConfigs({
|
|
1756
|
+
appId,
|
|
1757
|
+
forumChannelId,
|
|
1758
|
+
outputDir,
|
|
1759
|
+
}: {
|
|
1760
|
+
appId: string
|
|
1761
|
+
forumChannelId: string
|
|
1762
|
+
outputDir: string
|
|
1763
|
+
}) {
|
|
1764
|
+
const prisma = await getPrisma()
|
|
1765
|
+
await prisma.forum_sync_configs.deleteMany({
|
|
1766
|
+
where: {
|
|
1767
|
+
app_id: appId,
|
|
1768
|
+
output_dir: outputDir,
|
|
1769
|
+
NOT: { forum_channel_id: forumChannelId },
|
|
1770
|
+
},
|
|
1771
|
+
})
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1775
|
+
// IPC REQUESTS - plugin <-> bot communication via DB polling
|
|
1776
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1777
|
+
|
|
1778
|
+
export async function createIpcRequest({
|
|
1779
|
+
type,
|
|
1780
|
+
sessionId,
|
|
1781
|
+
threadId,
|
|
1782
|
+
payload,
|
|
1783
|
+
}: {
|
|
1784
|
+
type: import('./generated/client.js').ipc_request_type
|
|
1785
|
+
sessionId: string
|
|
1786
|
+
threadId: string
|
|
1787
|
+
payload: string
|
|
1788
|
+
}) {
|
|
1789
|
+
const prisma = await getPrisma()
|
|
1790
|
+
return prisma.ipc_requests.create({
|
|
1791
|
+
data: {
|
|
1792
|
+
type,
|
|
1793
|
+
session_id: sessionId,
|
|
1794
|
+
thread_id: threadId,
|
|
1795
|
+
payload,
|
|
1796
|
+
},
|
|
1797
|
+
})
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Atomically claim pending IPC requests by updating status to 'processing'
|
|
1802
|
+
* only for rows that are still 'pending'. Returns the claimed rows.
|
|
1803
|
+
* This prevents duplicate dispatch when poll ticks overlap.
|
|
1804
|
+
*/
|
|
1805
|
+
export async function claimPendingIpcRequests() {
|
|
1806
|
+
const prisma = await getPrisma()
|
|
1807
|
+
const pending = await prisma.ipc_requests.findMany({
|
|
1808
|
+
where: { status: 'pending' },
|
|
1809
|
+
orderBy: { created_at: 'asc' },
|
|
1810
|
+
})
|
|
1811
|
+
if (pending.length === 0) return pending
|
|
1812
|
+
|
|
1813
|
+
// Atomically claim each one (updateMany with status guard)
|
|
1814
|
+
const claimed: typeof pending = []
|
|
1815
|
+
for (const req of pending) {
|
|
1816
|
+
const result = await prisma.ipc_requests.updateMany({
|
|
1817
|
+
where: { id: req.id, status: 'pending' },
|
|
1818
|
+
data: { status: 'processing' },
|
|
1819
|
+
})
|
|
1820
|
+
if (result.count > 0) {
|
|
1821
|
+
claimed.push(req)
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return claimed
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
export async function completeIpcRequest({
|
|
1828
|
+
id,
|
|
1829
|
+
response,
|
|
1830
|
+
}: {
|
|
1831
|
+
id: string
|
|
1832
|
+
response: string
|
|
1833
|
+
}) {
|
|
1834
|
+
const prisma = await getPrisma()
|
|
1835
|
+
return prisma.ipc_requests.update({
|
|
1836
|
+
where: { id },
|
|
1837
|
+
data: { response, status: 'completed' as const },
|
|
1838
|
+
})
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
export async function getIpcRequestById({ id }: { id: string }) {
|
|
1842
|
+
const prisma = await getPrisma()
|
|
1843
|
+
return prisma.ipc_requests.findUnique({ where: { id } })
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** Cancel IPC requests stuck in 'processing' longer than the TTL (e.g. hung file upload). */
|
|
1847
|
+
export async function cancelStaleProcessingRequests({
|
|
1848
|
+
ttlMs,
|
|
1849
|
+
}: {
|
|
1850
|
+
ttlMs: number
|
|
1851
|
+
}) {
|
|
1852
|
+
const prisma = await getPrisma()
|
|
1853
|
+
const cutoff = new Date(Date.now() - ttlMs)
|
|
1854
|
+
return prisma.ipc_requests.updateMany({
|
|
1855
|
+
where: {
|
|
1856
|
+
status: 'processing',
|
|
1857
|
+
updated_at: { lt: cutoff },
|
|
1858
|
+
},
|
|
1859
|
+
data: {
|
|
1860
|
+
status: 'cancelled' as const,
|
|
1861
|
+
response: JSON.stringify({ error: 'Request timed out' }),
|
|
1862
|
+
},
|
|
1863
|
+
})
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/** Cancel all pending IPC requests (on startup cleanup and shutdown). */
|
|
1867
|
+
export async function cancelAllPendingIpcRequests() {
|
|
1868
|
+
const prisma = await getPrisma()
|
|
1869
|
+
await prisma.ipc_requests.updateMany({
|
|
1870
|
+
where: { status: { in: ['pending', 'processing'] } },
|
|
1871
|
+
data: {
|
|
1872
|
+
status: 'cancelled' as const,
|
|
1873
|
+
response: JSON.stringify({ error: 'Bot shutting down' }),
|
|
1874
|
+
},
|
|
1875
|
+
})
|
|
1876
|
+
}
|