@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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// File upload tool handler - Shows Discord modal with FileUploadBuilder.
|
|
2
|
+
// When the AI uses the kimaki_file_upload tool, the plugin inserts a row into
|
|
3
|
+
// the ipc_requests DB table. The bot polls this table, picks up the request,
|
|
4
|
+
// and shows a button in the thread. User clicks it to open a modal with a
|
|
5
|
+
// native file picker. Uploaded files are downloaded to the project directory.
|
|
6
|
+
// The bot writes file paths back to ipc_requests.response, and the plugin
|
|
7
|
+
// polls until the response appears.
|
|
8
|
+
import { ButtonBuilder, ButtonStyle, ActionRowBuilder, ModalBuilder, FileUploadBuilder, LabelBuilder, ComponentType, MessageFlags, } from 'discord.js';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
13
|
+
import { notifyError } from '../sentry.js';
|
|
14
|
+
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
15
|
+
const logger = createLogger(LogPrefix.FILE_UPLOAD);
|
|
16
|
+
// 5 minute TTL for pending contexts - if user doesn't click within this time,
|
|
17
|
+
// clean up the context and resolve with empty array to unblock the plugin tool
|
|
18
|
+
const PENDING_TTL_MS = 5 * 60 * 1000;
|
|
19
|
+
export const pendingFileUploadContexts = new Map();
|
|
20
|
+
/**
|
|
21
|
+
* Sanitize an attachment filename to prevent path traversal.
|
|
22
|
+
* Strips directory separators, .., and null bytes from the name.
|
|
23
|
+
* Prepends a short random prefix to avoid collisions between uploads.
|
|
24
|
+
*/
|
|
25
|
+
function sanitizeFilename(name) {
|
|
26
|
+
// Extract just the base name (strips any directory components)
|
|
27
|
+
let sanitized = path.basename(name);
|
|
28
|
+
// Remove null bytes and other dangerous characters
|
|
29
|
+
sanitized = sanitized.replace(/[\x00]/g, '');
|
|
30
|
+
// If somehow still empty or just dots, give it a safe name
|
|
31
|
+
if (!sanitized || sanitized === '.' || sanitized === '..') {
|
|
32
|
+
sanitized = 'upload';
|
|
33
|
+
}
|
|
34
|
+
// Prefix with short random id to avoid collisions
|
|
35
|
+
const prefix = crypto.randomBytes(4).toString('hex');
|
|
36
|
+
return `${prefix}-${sanitized}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Safely resolve a pending context exactly once. Prevents double-resolve from
|
|
40
|
+
* cancel/submit races by checking the `resolved` flag.
|
|
41
|
+
*/
|
|
42
|
+
function resolveContext(context, filePaths) {
|
|
43
|
+
if (context.resolved) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
context.resolved = true;
|
|
47
|
+
clearTimeout(context.timer);
|
|
48
|
+
pendingFileUploadContexts.delete(context.contextHash);
|
|
49
|
+
context.resolve(filePaths);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Show a button in the thread that opens a file upload modal when clicked.
|
|
54
|
+
* Returns a promise that resolves with the downloaded file paths.
|
|
55
|
+
*/
|
|
56
|
+
export function showFileUploadButton({ thread, sessionId, directory, prompt, maxFiles, }) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const contextHash = crypto.randomBytes(8).toString('hex');
|
|
59
|
+
// TTL timer: auto-cleanup if user never clicks the button
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
const ctx = pendingFileUploadContexts.get(contextHash);
|
|
62
|
+
if (ctx && !ctx.resolved) {
|
|
63
|
+
logger.log(`File upload timed out for session ${sessionId}, hash=${contextHash}`);
|
|
64
|
+
resolveContext(ctx, []);
|
|
65
|
+
// Remove button from message
|
|
66
|
+
if (ctx.messageId) {
|
|
67
|
+
ctx.thread.messages
|
|
68
|
+
.fetch(ctx.messageId)
|
|
69
|
+
.then((msg) => {
|
|
70
|
+
return msg.edit({
|
|
71
|
+
content: `**File Upload Requested**\n${prompt.slice(0, 1900)}\n_Timed out_`,
|
|
72
|
+
components: [],
|
|
73
|
+
});
|
|
74
|
+
})
|
|
75
|
+
.catch(() => { });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, PENDING_TTL_MS);
|
|
79
|
+
const context = {
|
|
80
|
+
sessionId,
|
|
81
|
+
directory,
|
|
82
|
+
thread,
|
|
83
|
+
prompt,
|
|
84
|
+
maxFiles,
|
|
85
|
+
contextHash,
|
|
86
|
+
resolve,
|
|
87
|
+
reject,
|
|
88
|
+
resolved: false,
|
|
89
|
+
timer,
|
|
90
|
+
};
|
|
91
|
+
pendingFileUploadContexts.set(contextHash, context);
|
|
92
|
+
const uploadButton = new ButtonBuilder()
|
|
93
|
+
.setCustomId(`file_upload_btn:${contextHash}`)
|
|
94
|
+
.setLabel('Upload Files')
|
|
95
|
+
.setStyle(ButtonStyle.Primary);
|
|
96
|
+
const actionRow = new ActionRowBuilder().addComponents(uploadButton);
|
|
97
|
+
thread
|
|
98
|
+
.send({
|
|
99
|
+
content: `**File Upload Requested**\n${prompt.slice(0, 1900)}`,
|
|
100
|
+
components: [actionRow],
|
|
101
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
102
|
+
})
|
|
103
|
+
.then((msg) => {
|
|
104
|
+
context.messageId = msg.id;
|
|
105
|
+
logger.log(`Showed file upload button for session ${sessionId}, hash=${contextHash}`);
|
|
106
|
+
})
|
|
107
|
+
.catch((err) => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
pendingFileUploadContexts.delete(contextHash);
|
|
110
|
+
reject(new Error('Failed to send file upload button', { cause: err }));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Handle the file upload button click - opens a modal with FileUploadBuilder.
|
|
116
|
+
*/
|
|
117
|
+
export async function handleFileUploadButton(interaction) {
|
|
118
|
+
const customId = interaction.customId;
|
|
119
|
+
if (!customId.startsWith('file_upload_btn:')) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const contextHash = customId.replace('file_upload_btn:', '');
|
|
123
|
+
const context = pendingFileUploadContexts.get(contextHash);
|
|
124
|
+
if (!context || context.resolved) {
|
|
125
|
+
await interaction.reply({
|
|
126
|
+
content: 'This file upload request has expired.',
|
|
127
|
+
flags: MessageFlags.Ephemeral,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const fileUpload = new FileUploadBuilder()
|
|
132
|
+
.setCustomId('uploaded_files')
|
|
133
|
+
.setMinValues(1)
|
|
134
|
+
.setMaxValues(context.maxFiles);
|
|
135
|
+
const label = new LabelBuilder()
|
|
136
|
+
.setLabel('Files')
|
|
137
|
+
.setDescription(context.prompt.slice(0, 100))
|
|
138
|
+
.setFileUploadComponent(fileUpload);
|
|
139
|
+
const modal = new ModalBuilder()
|
|
140
|
+
.setCustomId(`file_upload_modal:${contextHash}`)
|
|
141
|
+
.setTitle('Upload Files')
|
|
142
|
+
.addLabelComponents(label);
|
|
143
|
+
await interaction.showModal(modal);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Handle the modal submission - download files and resolve the pending promise.
|
|
147
|
+
*/
|
|
148
|
+
export async function handleFileUploadModalSubmit(interaction) {
|
|
149
|
+
const customId = interaction.customId;
|
|
150
|
+
if (!customId.startsWith('file_upload_modal:')) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const contextHash = customId.replace('file_upload_modal:', '');
|
|
154
|
+
const context = pendingFileUploadContexts.get(contextHash);
|
|
155
|
+
if (!context || context.resolved) {
|
|
156
|
+
await interaction.reply({
|
|
157
|
+
content: 'This file upload request has expired.',
|
|
158
|
+
flags: MessageFlags.Ephemeral,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
164
|
+
// File upload data is nested in the LabelModalData -> FileUploadModalData
|
|
165
|
+
const fileField = interaction.fields.getField('uploaded_files', ComponentType.FileUpload);
|
|
166
|
+
const attachments = fileField.attachments;
|
|
167
|
+
if (!attachments || attachments.size === 0) {
|
|
168
|
+
await interaction.editReply({ content: 'No files were uploaded.' });
|
|
169
|
+
updateButtonMessage(context, '_No files uploaded_');
|
|
170
|
+
resolveContext(context, []);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const uploadsDir = path.join(context.directory, 'uploads');
|
|
174
|
+
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
175
|
+
const downloadedPaths = [];
|
|
176
|
+
const errors = [];
|
|
177
|
+
for (const [, attachment] of attachments) {
|
|
178
|
+
// Check if context was cancelled (e.g. user sent new message) while
|
|
179
|
+
// we were downloading previous files - stop downloading more
|
|
180
|
+
if (context.resolved) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(attachment.url);
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
errors.push(`Failed to download ${attachment.name}: HTTP ${response.status}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
190
|
+
const safeName = sanitizeFilename(attachment.name);
|
|
191
|
+
const filePath = path.join(uploadsDir, safeName);
|
|
192
|
+
fs.writeFileSync(filePath, buffer);
|
|
193
|
+
downloadedPaths.push(filePath);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
errors.push(`Failed to download ${attachment.name}: ${msg}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// If context was resolved by cancel/timeout during download, don't try to
|
|
201
|
+
// resolve again - just update the ephemeral reply
|
|
202
|
+
if (context.resolved) {
|
|
203
|
+
await interaction.editReply({ content: 'Upload was cancelled.' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const fileNames = downloadedPaths.map((p) => {
|
|
207
|
+
return path.basename(p);
|
|
208
|
+
});
|
|
209
|
+
updateButtonMessage(context, downloadedPaths.length > 0
|
|
210
|
+
? `Uploaded: ${fileNames.join(', ')}`
|
|
211
|
+
: '_Upload failed_');
|
|
212
|
+
const summary = (() => {
|
|
213
|
+
if (downloadedPaths.length > 0 && errors.length === 0) {
|
|
214
|
+
return `Uploaded ${downloadedPaths.length} file(s) successfully.`;
|
|
215
|
+
}
|
|
216
|
+
if (downloadedPaths.length > 0 && errors.length > 0) {
|
|
217
|
+
return `Uploaded ${downloadedPaths.length} file(s). Errors: ${errors.join('; ')}`;
|
|
218
|
+
}
|
|
219
|
+
return `Upload failed: ${errors.join('; ')}`;
|
|
220
|
+
})();
|
|
221
|
+
await interaction.editReply({ content: summary });
|
|
222
|
+
resolveContext(context, downloadedPaths);
|
|
223
|
+
logger.log(`File upload completed for session ${context.sessionId}: ${downloadedPaths.length} files`);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// Ensure context is always resolved even on unexpected errors
|
|
227
|
+
// so the plugin tool doesn't hang indefinitely
|
|
228
|
+
logger.error('Error in file upload modal submit:', err);
|
|
229
|
+
void notifyError(err, 'File upload modal submit error');
|
|
230
|
+
resolveContext(context, []);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Best-effort update of the original button message (remove button, append status).
|
|
235
|
+
*/
|
|
236
|
+
function updateButtonMessage(context, status) {
|
|
237
|
+
if (!context.messageId) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
context.thread.messages
|
|
241
|
+
.fetch(context.messageId)
|
|
242
|
+
.then((msg) => {
|
|
243
|
+
return msg.edit({
|
|
244
|
+
content: `**File Upload Requested**\n${context.prompt.slice(0, 1900)}\n${status}`,
|
|
245
|
+
components: [],
|
|
246
|
+
});
|
|
247
|
+
})
|
|
248
|
+
.catch(() => { });
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Cancel ALL pending file uploads for a thread (e.g. when user sends a new message).
|
|
252
|
+
*/
|
|
253
|
+
export async function cancelPendingFileUpload(threadId) {
|
|
254
|
+
const toCancel = [];
|
|
255
|
+
for (const [, ctx] of pendingFileUploadContexts) {
|
|
256
|
+
if (ctx.thread.id === threadId) {
|
|
257
|
+
toCancel.push(ctx);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (toCancel.length === 0) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
let cancelled = 0;
|
|
264
|
+
for (const context of toCancel) {
|
|
265
|
+
const didResolve = resolveContext(context, []);
|
|
266
|
+
if (didResolve) {
|
|
267
|
+
updateButtonMessage(context, '_Cancelled - user sent a new message_');
|
|
268
|
+
cancelled++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (cancelled > 0) {
|
|
272
|
+
logger.log(`Cancelled ${cancelled} file upload(s) for thread ${threadId}`);
|
|
273
|
+
}
|
|
274
|
+
return cancelled > 0;
|
|
275
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// /fork command - Fork the session from a past user message.
|
|
2
|
+
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, ThreadAutoArchiveDuration, MessageFlags, } from 'discord.js';
|
|
3
|
+
import { getThreadSession, setThreadSession, setPartMessagesBatch, } from '../database.js';
|
|
4
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
|
+
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
|
|
6
|
+
import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
9
|
+
const sessionLogger = createLogger(LogPrefix.SESSION);
|
|
10
|
+
const forkLogger = createLogger(LogPrefix.FORK);
|
|
11
|
+
export async function handleForkCommand(interaction) {
|
|
12
|
+
const channel = interaction.channel;
|
|
13
|
+
if (!channel) {
|
|
14
|
+
await interaction.reply({
|
|
15
|
+
content: 'This command can only be used in a channel',
|
|
16
|
+
flags: MessageFlags.Ephemeral,
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const isThread = [
|
|
21
|
+
ChannelType.PublicThread,
|
|
22
|
+
ChannelType.PrivateThread,
|
|
23
|
+
ChannelType.AnnouncementThread,
|
|
24
|
+
].includes(channel.type);
|
|
25
|
+
if (!isThread) {
|
|
26
|
+
await interaction.reply({
|
|
27
|
+
content: 'This command can only be used in a thread with an active session',
|
|
28
|
+
flags: MessageFlags.Ephemeral,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const resolved = await resolveWorkingDirectory({
|
|
33
|
+
channel: channel,
|
|
34
|
+
});
|
|
35
|
+
if (!resolved) {
|
|
36
|
+
await interaction.reply({
|
|
37
|
+
content: 'Could not determine project directory for this channel',
|
|
38
|
+
flags: MessageFlags.Ephemeral,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { projectDirectory } = resolved;
|
|
43
|
+
const sessionId = await getThreadSession(channel.id);
|
|
44
|
+
if (!sessionId) {
|
|
45
|
+
await interaction.reply({
|
|
46
|
+
content: 'No active session in this thread',
|
|
47
|
+
flags: MessageFlags.Ephemeral,
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Defer reply before API calls to avoid 3-second timeout
|
|
52
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
53
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
54
|
+
if (getClient instanceof Error) {
|
|
55
|
+
await interaction.editReply({
|
|
56
|
+
content: `Failed to load messages: ${getClient.message}`,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const messagesResponse = await getClient().session.messages({
|
|
62
|
+
sessionID: sessionId,
|
|
63
|
+
});
|
|
64
|
+
if (!messagesResponse.data) {
|
|
65
|
+
await interaction.editReply({
|
|
66
|
+
content: 'Failed to fetch session messages',
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const userMessages = messagesResponse.data.filter((m) => m.info.role === 'user');
|
|
71
|
+
if (userMessages.length === 0) {
|
|
72
|
+
await interaction.editReply({
|
|
73
|
+
content: 'No user messages found in this session',
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const recentMessages = userMessages.slice(-25);
|
|
78
|
+
// Filter out synthetic parts (branch context, memory reminders, etc.)
|
|
79
|
+
// injected by the opencode plugin — they clutter the dropdown preview.
|
|
80
|
+
const options = recentMessages
|
|
81
|
+
.map((m, index) => {
|
|
82
|
+
const textPart = m.parts.find((p) => p.type === 'text' && !p.synthetic);
|
|
83
|
+
if (!textPart?.text) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const preview = textPart.text.slice(0, 80);
|
|
87
|
+
const label = `${index + 1}. ${preview}${preview.length >= 80 ? '...' : ''}`;
|
|
88
|
+
return {
|
|
89
|
+
label: label.slice(0, 100),
|
|
90
|
+
value: m.info.id,
|
|
91
|
+
description: new Date(m.info.time.created)
|
|
92
|
+
.toLocaleString()
|
|
93
|
+
.slice(0, 50),
|
|
94
|
+
};
|
|
95
|
+
})
|
|
96
|
+
.filter((o) => o !== null);
|
|
97
|
+
const selectMenu = new StringSelectMenuBuilder()
|
|
98
|
+
// Discord component custom_id max length is 100 chars.
|
|
99
|
+
// Avoid embedding long directory paths (or base64 of them) in the custom ID.
|
|
100
|
+
// handleForkSelectMenu resolves the directory from the current thread instead.
|
|
101
|
+
.setCustomId(`fork_select:${sessionId}`)
|
|
102
|
+
.setPlaceholder('Select a message to fork from')
|
|
103
|
+
.addOptions(options);
|
|
104
|
+
const actionRow = new ActionRowBuilder().addComponents(selectMenu);
|
|
105
|
+
await interaction.editReply({
|
|
106
|
+
content: '**Fork Session**\nSelect the user message to fork from. The forked session will continue as if you had not sent that message:',
|
|
107
|
+
components: [actionRow],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
forkLogger.error('Error loading messages:', error);
|
|
112
|
+
await interaction.editReply({
|
|
113
|
+
content: `Failed to load messages: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function handleForkSelectMenu(interaction) {
|
|
118
|
+
const customId = interaction.customId;
|
|
119
|
+
if (!customId.startsWith('fork_select:')) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const [, sessionId] = customId.split(':');
|
|
123
|
+
if (!sessionId) {
|
|
124
|
+
await interaction.reply({
|
|
125
|
+
content: 'Invalid selection data',
|
|
126
|
+
flags: MessageFlags.Ephemeral,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const selectedMessageId = interaction.values[0];
|
|
131
|
+
if (!selectedMessageId) {
|
|
132
|
+
await interaction.reply({
|
|
133
|
+
content: 'No message selected',
|
|
134
|
+
flags: MessageFlags.Ephemeral,
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await interaction.deferReply();
|
|
139
|
+
const threadChannel = interaction.channel;
|
|
140
|
+
if (!threadChannel) {
|
|
141
|
+
await interaction.editReply('Could not access thread channel');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const resolved = await resolveWorkingDirectory({
|
|
145
|
+
channel: threadChannel,
|
|
146
|
+
});
|
|
147
|
+
if (!resolved) {
|
|
148
|
+
await interaction.editReply('Could not determine project directory for this channel');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const { projectDirectory } = resolved;
|
|
152
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
153
|
+
if (getClient instanceof Error) {
|
|
154
|
+
await interaction.editReply(`Failed to fork session: ${getClient.message}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const forkResponse = await getClient().session.fork({
|
|
159
|
+
sessionID: sessionId,
|
|
160
|
+
messageID: selectedMessageId,
|
|
161
|
+
});
|
|
162
|
+
if (!forkResponse.data) {
|
|
163
|
+
await interaction.editReply('Failed to fork session');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const forkedSession = forkResponse.data;
|
|
167
|
+
const parentChannel = interaction.channel;
|
|
168
|
+
if (!parentChannel ||
|
|
169
|
+
![
|
|
170
|
+
ChannelType.PublicThread,
|
|
171
|
+
ChannelType.PrivateThread,
|
|
172
|
+
ChannelType.AnnouncementThread,
|
|
173
|
+
].includes(parentChannel.type)) {
|
|
174
|
+
await interaction.editReply('Could not access parent channel');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const textChannel = await resolveTextChannel(parentChannel);
|
|
178
|
+
if (!textChannel) {
|
|
179
|
+
await interaction.editReply('Could not resolve parent text channel');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const thread = await textChannel.threads.create({
|
|
183
|
+
name: `Fork: ${forkedSession.title}`.slice(0, 100),
|
|
184
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
185
|
+
reason: `Forked from session ${sessionId}`,
|
|
186
|
+
});
|
|
187
|
+
// Claim the forked session immediately so external polling does not race
|
|
188
|
+
// and create a duplicate Sync thread before the rest of this setup runs.
|
|
189
|
+
await setThreadSession(thread.id, forkedSession.id);
|
|
190
|
+
// Add user to thread so it appears in their sidebar
|
|
191
|
+
await thread.members.add(interaction.user.id);
|
|
192
|
+
sessionLogger.log(`Created forked session ${forkedSession.id} in thread ${thread.id}`);
|
|
193
|
+
await sendThreadMessage(thread, `**Forked session created!**\nFrom: \`${sessionId}\`\nNew session: \`${forkedSession.id}\``);
|
|
194
|
+
// Fetch and display the last assistant messages from the forked session
|
|
195
|
+
const messagesResponse = await getClient().session.messages({
|
|
196
|
+
sessionID: forkedSession.id,
|
|
197
|
+
});
|
|
198
|
+
if (messagesResponse.data) {
|
|
199
|
+
const { chunks } = collectSessionChunks({
|
|
200
|
+
messages: messagesResponse.data,
|
|
201
|
+
limit: 30,
|
|
202
|
+
});
|
|
203
|
+
const batched = batchChunksForDiscord(chunks);
|
|
204
|
+
for (const batch of batched) {
|
|
205
|
+
const discordMessage = await sendThreadMessage(thread, batch.content);
|
|
206
|
+
await setPartMessagesBatch(batch.partIds.map((partId) => ({
|
|
207
|
+
partId,
|
|
208
|
+
messageId: discordMessage.id,
|
|
209
|
+
threadId: thread.id,
|
|
210
|
+
})));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
await sendThreadMessage(thread, `You can now continue the conversation from this point.`);
|
|
214
|
+
await interaction.editReply(`Session forked! Continue in ${thread.toString()}`);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
forkLogger.error('Error forking session:', error);
|
|
218
|
+
await interaction.editReply(`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Transcription API key button, slash command, and modal handlers.
|
|
2
|
+
// Auto-detects provider from key prefix: sk-* = OpenAI, otherwise Gemini.
|
|
3
|
+
import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, MessageFlags, } from 'discord.js';
|
|
4
|
+
import { setGeminiApiKey, setOpenAIApiKey } from '../database.js';
|
|
5
|
+
function buildTranscriptionApiKeyModal(appId) {
|
|
6
|
+
const modal = new ModalBuilder()
|
|
7
|
+
.setCustomId(`transcription_apikey_modal:${appId}`)
|
|
8
|
+
.setTitle('Transcription API Key');
|
|
9
|
+
const apiKeyInput = new TextInputBuilder()
|
|
10
|
+
.setCustomId('apikey')
|
|
11
|
+
.setLabel('OpenAI or Gemini API Key')
|
|
12
|
+
.setPlaceholder('sk-... or AIza...')
|
|
13
|
+
.setStyle(TextInputStyle.Short)
|
|
14
|
+
.setRequired(true);
|
|
15
|
+
const actionRow = new ActionRowBuilder().addComponents(apiKeyInput);
|
|
16
|
+
modal.addComponents(actionRow);
|
|
17
|
+
return modal;
|
|
18
|
+
}
|
|
19
|
+
export async function handleTranscriptionApiKeyButton(interaction) {
|
|
20
|
+
if (!interaction.customId.startsWith('transcription_apikey:'))
|
|
21
|
+
return;
|
|
22
|
+
const appId = interaction.customId
|
|
23
|
+
.slice('transcription_apikey:'.length)
|
|
24
|
+
.trim();
|
|
25
|
+
if (!appId) {
|
|
26
|
+
await interaction.reply({
|
|
27
|
+
content: 'Missing app id for API key setup.',
|
|
28
|
+
flags: MessageFlags.Ephemeral,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await interaction.showModal(buildTranscriptionApiKeyModal(appId));
|
|
33
|
+
}
|
|
34
|
+
export async function handleTranscriptionApiKeyCommand({ interaction, appId, }) {
|
|
35
|
+
await interaction.showModal(buildTranscriptionApiKeyModal(appId));
|
|
36
|
+
}
|
|
37
|
+
export async function handleTranscriptionApiKeyModalSubmit(interaction) {
|
|
38
|
+
if (!interaction.customId.startsWith('transcription_apikey_modal:'))
|
|
39
|
+
return;
|
|
40
|
+
const appId = interaction.customId
|
|
41
|
+
.slice('transcription_apikey_modal:'.length)
|
|
42
|
+
.trim();
|
|
43
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
44
|
+
if (!appId) {
|
|
45
|
+
await interaction.editReply({
|
|
46
|
+
content: 'Missing app id for API key setup.',
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const apiKey = interaction.fields.getTextInputValue('apikey').trim();
|
|
51
|
+
if (!apiKey) {
|
|
52
|
+
await interaction.editReply({
|
|
53
|
+
content: 'API key is required.',
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Auto-detect provider from key prefix
|
|
58
|
+
if (apiKey.startsWith('sk-')) {
|
|
59
|
+
await setOpenAIApiKey(appId, apiKey);
|
|
60
|
+
await interaction.editReply({
|
|
61
|
+
content: 'OpenAI API key saved. Voice messages will be transcribed with OpenAI.',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await setGeminiApiKey(appId, apiKey);
|
|
66
|
+
await interaction.editReply({
|
|
67
|
+
content: 'Gemini API key saved. Voice messages will be transcribed with Gemini.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|