@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,591 @@
|
|
|
1
|
+
// Discord-specific utility functions.
|
|
2
|
+
// Handles markdown splitting for Discord's 2000-char limit, code block escaping,
|
|
3
|
+
// thread message sending, and channel metadata extraction from topic tags.
|
|
4
|
+
import { ChannelType, GuildMember, MessageFlags, PermissionsBitField, } from 'discord.js';
|
|
5
|
+
import { REST, Routes } from 'discord.js';
|
|
6
|
+
import { discordApiUrl } from './discord-urls.js';
|
|
7
|
+
import { Lexer } from 'marked';
|
|
8
|
+
import { splitTablesFromMarkdown } from './format-tables.js';
|
|
9
|
+
import { getChannelDirectory, getThreadWorktree } from './database.js';
|
|
10
|
+
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
11
|
+
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
12
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
13
|
+
import * as errore from 'errore';
|
|
14
|
+
import mime from 'mime';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
18
|
+
/**
|
|
19
|
+
* Centralized permission check for Kimaki bot access.
|
|
20
|
+
* Returns true if the member has permission to use the bot:
|
|
21
|
+
* - Server owner, Administrator, Manage Server, or "Kimaki" role (case-insensitive).
|
|
22
|
+
* Returns false if member is null or has the "no-kimaki" role (overrides all).
|
|
23
|
+
*/
|
|
24
|
+
export function hasKimakiBotPermission(member, guild) {
|
|
25
|
+
if (!member) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const hasNoKimakiRole = hasRoleByName(member, 'no-kimaki', guild);
|
|
29
|
+
if (hasNoKimakiRole) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const memberPermissions = member instanceof GuildMember
|
|
33
|
+
? member.permissions
|
|
34
|
+
: new PermissionsBitField(BigInt(member.permissions));
|
|
35
|
+
const ownerId = member instanceof GuildMember ? member.guild.ownerId : guild?.ownerId;
|
|
36
|
+
const memberId = member instanceof GuildMember ? member.id : member.user.id;
|
|
37
|
+
const isOwner = ownerId ? memberId === ownerId : false;
|
|
38
|
+
const isAdmin = memberPermissions.has(PermissionsBitField.Flags.Administrator);
|
|
39
|
+
const canManageServer = memberPermissions.has(PermissionsBitField.Flags.ManageGuild);
|
|
40
|
+
const hasKimakiRole = hasRoleByName(member, 'kimaki', guild);
|
|
41
|
+
return isOwner || isAdmin || canManageServer || hasKimakiRole;
|
|
42
|
+
}
|
|
43
|
+
function hasRoleByName(member, roleName, guild) {
|
|
44
|
+
const target = roleName.toLowerCase();
|
|
45
|
+
if (member instanceof GuildMember) {
|
|
46
|
+
return member.roles.cache.some((role) => role.name.toLowerCase() === target);
|
|
47
|
+
}
|
|
48
|
+
if (!guild) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const roleIds = Array.isArray(member.roles) ? member.roles : [];
|
|
52
|
+
for (const roleId of roleIds) {
|
|
53
|
+
const role = guild.roles.cache.get(roleId);
|
|
54
|
+
if (role?.name.toLowerCase() === target) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if the member has the "no-kimaki" role that blocks bot access.
|
|
62
|
+
* Separate from hasKimakiBotPermission so callers can show a specific error message.
|
|
63
|
+
*/
|
|
64
|
+
export function hasNoKimakiRole(member) {
|
|
65
|
+
if (!member?.roles?.cache) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return member.roles.cache.some((role) => role.name.toLowerCase() === 'no-kimaki');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* React to a thread's starter message with an emoji.
|
|
72
|
+
* Thread ID equals the starter message ID in Discord.
|
|
73
|
+
*/
|
|
74
|
+
export async function reactToThread({ rest, threadId, channelId, emoji, }) {
|
|
75
|
+
const parentChannelId = await (async () => {
|
|
76
|
+
if (channelId) {
|
|
77
|
+
return channelId;
|
|
78
|
+
}
|
|
79
|
+
// Fetch the thread to get its parent channel ID
|
|
80
|
+
const threadResult = await errore.tryAsync(() => {
|
|
81
|
+
return rest.get(Routes.channel(threadId));
|
|
82
|
+
});
|
|
83
|
+
if (threadResult instanceof Error) {
|
|
84
|
+
discordLogger.warn(`Failed to fetch thread ${threadId}:`, threadResult.message);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return threadResult.parent_id || null;
|
|
88
|
+
})();
|
|
89
|
+
if (!parentChannelId) {
|
|
90
|
+
discordLogger.warn(`Could not resolve parent channel for thread ${threadId}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// React to the thread starter message in the parent channel.
|
|
94
|
+
// Thread ID equals the starter message ID for threads created from messages.
|
|
95
|
+
const result = await errore.tryAsync(() => {
|
|
96
|
+
return rest.put(Routes.channelMessageOwnReaction(parentChannelId, threadId, encodeURIComponent(emoji)));
|
|
97
|
+
});
|
|
98
|
+
if (result instanceof Error) {
|
|
99
|
+
discordLogger.warn(`Failed to react to thread ${threadId} with ${emoji}:`, result.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function archiveThread({ rest, threadId, parentChannelId, sessionId, client, archiveDelay = 0, }) {
|
|
103
|
+
await reactToThread({
|
|
104
|
+
rest,
|
|
105
|
+
threadId,
|
|
106
|
+
channelId: parentChannelId,
|
|
107
|
+
emoji: '📁',
|
|
108
|
+
});
|
|
109
|
+
if (client && sessionId) {
|
|
110
|
+
const updateResult = await errore.tryAsync({
|
|
111
|
+
try: async () => {
|
|
112
|
+
const sessionResponse = await client.session.get({
|
|
113
|
+
sessionID: sessionId,
|
|
114
|
+
});
|
|
115
|
+
if (!sessionResponse.data) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const currentTitle = sessionResponse.data.title || '';
|
|
119
|
+
const newTitle = currentTitle.startsWith('📁')
|
|
120
|
+
? currentTitle
|
|
121
|
+
: `📁 ${currentTitle}`.trim();
|
|
122
|
+
await client.session.update({
|
|
123
|
+
sessionID: sessionId,
|
|
124
|
+
title: newTitle,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
catch: (e) => new Error('Failed to update session title', { cause: e }),
|
|
128
|
+
});
|
|
129
|
+
if (updateResult instanceof Error) {
|
|
130
|
+
discordLogger.warn(`[archive-thread] ${updateResult.message}`);
|
|
131
|
+
}
|
|
132
|
+
const abortResult = await errore.tryAsync({
|
|
133
|
+
try: async () => {
|
|
134
|
+
await client.session.abort({ sessionID: sessionId });
|
|
135
|
+
},
|
|
136
|
+
catch: (e) => new Error('Failed to abort session', { cause: e }),
|
|
137
|
+
});
|
|
138
|
+
if (abortResult instanceof Error) {
|
|
139
|
+
discordLogger.warn(`[archive-thread] ${abortResult.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (archiveDelay > 0) {
|
|
143
|
+
await new Promise((resolve) => {
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
resolve();
|
|
146
|
+
}, archiveDelay);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
await rest.patch(Routes.channel(threadId), {
|
|
150
|
+
body: { archived: true },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** Remove Discord mentions from text so they don't appear in thread titles */
|
|
154
|
+
export function stripMentions(text) {
|
|
155
|
+
return text
|
|
156
|
+
.replace(/<@!?\d+>/g, '') // user mentions
|
|
157
|
+
.replace(/<@&\d+>/g, '') // role mentions
|
|
158
|
+
.replace(/<#\d+>/g, '') // channel mentions
|
|
159
|
+
.replace(/\s+/g, ' ')
|
|
160
|
+
.trim();
|
|
161
|
+
}
|
|
162
|
+
export const SILENT_MESSAGE_FLAGS = 4 | 4096;
|
|
163
|
+
// Same as SILENT but without SuppressNotifications - triggers badge/notification
|
|
164
|
+
export const NOTIFY_MESSAGE_FLAGS = 4;
|
|
165
|
+
export function escapeBackticksInCodeBlocks(markdown) {
|
|
166
|
+
const lexer = new Lexer();
|
|
167
|
+
const tokens = lexer.lex(markdown);
|
|
168
|
+
let result = '';
|
|
169
|
+
for (const token of tokens) {
|
|
170
|
+
if (token.type === 'code') {
|
|
171
|
+
const escapedCode = token.text.replace(/`/g, '\\`');
|
|
172
|
+
result += '```' + (token.lang || '') + '\n' + escapedCode + '\n```\n';
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
result += token.raw;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
181
|
+
if (content.length <= maxLength) {
|
|
182
|
+
return [content];
|
|
183
|
+
}
|
|
184
|
+
const lexer = new Lexer();
|
|
185
|
+
const tokens = lexer.lex(content);
|
|
186
|
+
const lines = [];
|
|
187
|
+
const ensureNewlineBeforeCode = () => {
|
|
188
|
+
const last = lines[lines.length - 1];
|
|
189
|
+
if (!last) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (last.text.endsWith('\n')) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
lines.push({
|
|
196
|
+
text: '\n',
|
|
197
|
+
inCodeBlock: false,
|
|
198
|
+
lang: '',
|
|
199
|
+
isOpeningFence: false,
|
|
200
|
+
isClosingFence: false,
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
for (const token of tokens) {
|
|
204
|
+
if (token.type === 'code') {
|
|
205
|
+
ensureNewlineBeforeCode();
|
|
206
|
+
const lang = token.lang || '';
|
|
207
|
+
lines.push({
|
|
208
|
+
text: '```' + lang + '\n',
|
|
209
|
+
inCodeBlock: false,
|
|
210
|
+
lang,
|
|
211
|
+
isOpeningFence: true,
|
|
212
|
+
isClosingFence: false,
|
|
213
|
+
});
|
|
214
|
+
const codeLines = token.text.split('\n');
|
|
215
|
+
for (const codeLine of codeLines) {
|
|
216
|
+
lines.push({
|
|
217
|
+
text: codeLine + '\n',
|
|
218
|
+
inCodeBlock: true,
|
|
219
|
+
lang,
|
|
220
|
+
isOpeningFence: false,
|
|
221
|
+
isClosingFence: false,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
lines.push({
|
|
225
|
+
text: '```\n',
|
|
226
|
+
inCodeBlock: false,
|
|
227
|
+
lang: '',
|
|
228
|
+
isOpeningFence: false,
|
|
229
|
+
isClosingFence: true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const rawLines = token.raw.split('\n');
|
|
234
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
235
|
+
const isLast = i === rawLines.length - 1;
|
|
236
|
+
const text = isLast ? rawLines[i] : rawLines[i] + '\n';
|
|
237
|
+
if (text) {
|
|
238
|
+
lines.push({
|
|
239
|
+
text,
|
|
240
|
+
inCodeBlock: false,
|
|
241
|
+
lang: '',
|
|
242
|
+
isOpeningFence: false,
|
|
243
|
+
isClosingFence: false,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const chunks = [];
|
|
250
|
+
let currentChunk = '';
|
|
251
|
+
let currentLang = null;
|
|
252
|
+
// helper to split a long line into smaller pieces at word boundaries or hard breaks
|
|
253
|
+
const splitLongLine = (text, available, inCode) => {
|
|
254
|
+
const pieces = [];
|
|
255
|
+
let remaining = text;
|
|
256
|
+
while (remaining.length > available) {
|
|
257
|
+
let splitAt = available;
|
|
258
|
+
// for non-code, try to split at word boundary
|
|
259
|
+
if (!inCode) {
|
|
260
|
+
const lastSpace = remaining.lastIndexOf(' ', available);
|
|
261
|
+
if (lastSpace > available * 0.5) {
|
|
262
|
+
splitAt = lastSpace + 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
pieces.push(remaining.slice(0, splitAt));
|
|
266
|
+
remaining = remaining.slice(splitAt);
|
|
267
|
+
}
|
|
268
|
+
if (remaining) {
|
|
269
|
+
pieces.push(remaining);
|
|
270
|
+
}
|
|
271
|
+
return pieces;
|
|
272
|
+
};
|
|
273
|
+
const closingFence = '```\n';
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
// openingFenceSize accounts for the fence text when starting a fresh chunk
|
|
276
|
+
const openingFenceSize = currentChunk.length === 0 && (line.inCodeBlock || line.isOpeningFence)
|
|
277
|
+
? ('```' + line.lang + '\n').length
|
|
278
|
+
: 0;
|
|
279
|
+
// When opening fence starts a fresh chunk, its size is in openingFenceSize.
|
|
280
|
+
// Otherwise count it normally so the overflow check doesn't miss the fence text.
|
|
281
|
+
const lineLength = line.isOpeningFence && currentChunk.length === 0 ? 0 : line.text.length;
|
|
282
|
+
const activeFenceOverhead = currentLang !== null || openingFenceSize > 0 ? closingFence.length : 0;
|
|
283
|
+
const wouldExceed = currentChunk.length +
|
|
284
|
+
openingFenceSize +
|
|
285
|
+
lineLength +
|
|
286
|
+
activeFenceOverhead >
|
|
287
|
+
maxLength;
|
|
288
|
+
if (wouldExceed) {
|
|
289
|
+
// handle case where single line is longer than maxLength
|
|
290
|
+
if (line.text.length > maxLength) {
|
|
291
|
+
// first, flush current chunk if any
|
|
292
|
+
if (currentChunk) {
|
|
293
|
+
if (currentLang !== null) {
|
|
294
|
+
currentChunk += '```\n';
|
|
295
|
+
}
|
|
296
|
+
chunks.push(currentChunk);
|
|
297
|
+
currentChunk = '';
|
|
298
|
+
}
|
|
299
|
+
// calculate overhead for code block markers
|
|
300
|
+
const codeBlockOverhead = line.inCodeBlock
|
|
301
|
+
? ('```' + line.lang + '\n').length + '```\n'.length
|
|
302
|
+
: 0;
|
|
303
|
+
// ensure at least 10 chars available, even if maxLength is very small
|
|
304
|
+
const availablePerChunk = Math.max(10, maxLength - codeBlockOverhead - 50);
|
|
305
|
+
const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
|
|
306
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
307
|
+
const piece = pieces[i];
|
|
308
|
+
if (line.inCodeBlock) {
|
|
309
|
+
chunks.push('```' + line.lang + '\n' + piece + '```\n');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
chunks.push(piece);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
currentLang = null;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// normal case: line fits in a chunk but current chunk would overflow
|
|
319
|
+
if (currentChunk) {
|
|
320
|
+
if (currentLang !== null) {
|
|
321
|
+
currentChunk += '```\n';
|
|
322
|
+
}
|
|
323
|
+
chunks.push(currentChunk);
|
|
324
|
+
if (line.isClosingFence && currentLang !== null) {
|
|
325
|
+
currentChunk = '';
|
|
326
|
+
currentLang = null;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
330
|
+
const lang = line.lang;
|
|
331
|
+
currentChunk = '```' + lang + '\n';
|
|
332
|
+
if (!line.isOpeningFence) {
|
|
333
|
+
currentChunk += line.text;
|
|
334
|
+
}
|
|
335
|
+
currentLang = lang;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
currentChunk = line.text;
|
|
339
|
+
currentLang = null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// currentChunk is empty but line still exceeds - shouldn't happen after above check
|
|
344
|
+
const openingFence = line.inCodeBlock || line.isOpeningFence;
|
|
345
|
+
const openingFenceSize = openingFence
|
|
346
|
+
? ('```' + line.lang + '\n').length
|
|
347
|
+
: 0;
|
|
348
|
+
if (line.text.length + openingFenceSize + activeFenceOverhead >
|
|
349
|
+
maxLength) {
|
|
350
|
+
const fencedOverhead = openingFence
|
|
351
|
+
? ('```' + line.lang + '\n').length + closingFence.length
|
|
352
|
+
: 0;
|
|
353
|
+
const availablePerChunk = Math.max(10, maxLength - fencedOverhead - 50);
|
|
354
|
+
const pieces = splitLongLine(line.text, availablePerChunk, line.inCodeBlock);
|
|
355
|
+
for (const piece of pieces) {
|
|
356
|
+
if (openingFence) {
|
|
357
|
+
chunks.push('```' + line.lang + '\n' + piece + closingFence);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
chunks.push(piece);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
currentChunk = '';
|
|
364
|
+
currentLang = null;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
if (openingFence) {
|
|
368
|
+
currentChunk = '```' + line.lang + '\n';
|
|
369
|
+
if (!line.isOpeningFence) {
|
|
370
|
+
currentChunk += line.text;
|
|
371
|
+
}
|
|
372
|
+
currentLang = line.lang;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
currentChunk = line.text;
|
|
376
|
+
currentLang = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
currentChunk += line.text;
|
|
383
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
384
|
+
currentLang = line.lang;
|
|
385
|
+
}
|
|
386
|
+
else if (line.isClosingFence) {
|
|
387
|
+
currentLang = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (currentChunk) {
|
|
392
|
+
if (currentLang !== null) {
|
|
393
|
+
currentChunk += closingFence;
|
|
394
|
+
}
|
|
395
|
+
chunks.push(currentChunk);
|
|
396
|
+
}
|
|
397
|
+
return chunks;
|
|
398
|
+
}
|
|
399
|
+
export async function sendThreadMessage(thread, content, options) {
|
|
400
|
+
const MAX_LENGTH = 2000;
|
|
401
|
+
// Split content into text and CV2 component segments (tables → Container components)
|
|
402
|
+
const segments = splitTablesFromMarkdown(content);
|
|
403
|
+
const baseFlags = options?.flags ?? SILENT_MESSAGE_FLAGS;
|
|
404
|
+
let firstMessage;
|
|
405
|
+
for (const segment of segments) {
|
|
406
|
+
if (segment.type === 'components') {
|
|
407
|
+
const message = await thread.send({
|
|
408
|
+
components: segment.components,
|
|
409
|
+
flags: MessageFlags.IsComponentsV2 | baseFlags,
|
|
410
|
+
});
|
|
411
|
+
if (!firstMessage) {
|
|
412
|
+
firstMessage = message;
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
// Apply text transformations to text segments
|
|
417
|
+
let text = segment.text;
|
|
418
|
+
text = unnestCodeBlocksFromLists(text);
|
|
419
|
+
text = limitHeadingDepth(text);
|
|
420
|
+
text = escapeBackticksInCodeBlocks(text);
|
|
421
|
+
if (!text.trim()) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const sendFlags = options?.flags ?? SILENT_MESSAGE_FLAGS;
|
|
425
|
+
const chunks = splitMarkdownForDiscord({
|
|
426
|
+
content: text,
|
|
427
|
+
maxLength: MAX_LENGTH,
|
|
428
|
+
});
|
|
429
|
+
if (chunks.length > 1) {
|
|
430
|
+
discordLogger.log(`MESSAGE: Splitting ${text.length} chars into ${chunks.length} messages`);
|
|
431
|
+
}
|
|
432
|
+
for (let chunk of chunks) {
|
|
433
|
+
if (!chunk) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
// Safety net: hard-truncate if splitting still produced an oversized chunk
|
|
437
|
+
if (chunk.length > MAX_LENGTH) {
|
|
438
|
+
chunk = chunk.slice(0, MAX_LENGTH - 4) + '...';
|
|
439
|
+
}
|
|
440
|
+
const message = await thread.send({ content: chunk, flags: sendFlags });
|
|
441
|
+
if (!firstMessage) {
|
|
442
|
+
firstMessage = message;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return firstMessage;
|
|
447
|
+
}
|
|
448
|
+
export async function resolveTextChannel(channel) {
|
|
449
|
+
if (!channel) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
if (channel.type === ChannelType.GuildText) {
|
|
453
|
+
return channel;
|
|
454
|
+
}
|
|
455
|
+
if (channel.type === ChannelType.PublicThread ||
|
|
456
|
+
channel.type === ChannelType.PrivateThread ||
|
|
457
|
+
channel.type === ChannelType.AnnouncementThread) {
|
|
458
|
+
const parentId = channel.parentId;
|
|
459
|
+
if (parentId) {
|
|
460
|
+
const parent = await channel.guild.channels.fetch(parentId);
|
|
461
|
+
if (parent?.type === ChannelType.GuildText) {
|
|
462
|
+
return parent;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
export function escapeDiscordFormatting(text) {
|
|
469
|
+
return text.replace(/```/g, '\\`\\`\\`').replace(/````/g, '\\`\\`\\`\\`');
|
|
470
|
+
}
|
|
471
|
+
export async function getKimakiMetadata(textChannel) {
|
|
472
|
+
if (!textChannel) {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
const channelConfig = await getChannelDirectory(textChannel.id);
|
|
476
|
+
if (!channelConfig) {
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
projectDirectory: channelConfig.directory,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Resolve project directory from an autocomplete interaction.
|
|
485
|
+
* Uses interaction.channelId (always available from raw payload) instead of
|
|
486
|
+
* interaction.channel (cache-based getter, often null with gateway-proxy).
|
|
487
|
+
* Checks the channel ID directly in DB, then tries thread worktree lookup,
|
|
488
|
+
* then falls back to fetching the channel to resolve thread parent.
|
|
489
|
+
*/
|
|
490
|
+
export async function resolveProjectDirectoryFromAutocomplete(interaction) {
|
|
491
|
+
const channelId = interaction.channelId;
|
|
492
|
+
// Direct channel lookup — works when the command is run from a project text channel
|
|
493
|
+
const channelConfig = await getChannelDirectory(channelId);
|
|
494
|
+
if (channelConfig) {
|
|
495
|
+
return channelConfig.directory;
|
|
496
|
+
}
|
|
497
|
+
// If we're in a thread, try worktree info first (has project_directory)
|
|
498
|
+
const worktreeInfo = await getThreadWorktree(channelId);
|
|
499
|
+
if (worktreeInfo?.project_directory) {
|
|
500
|
+
return worktreeInfo.project_directory;
|
|
501
|
+
}
|
|
502
|
+
// Thread fallback: resolve parent channel ID and look up its directory.
|
|
503
|
+
// Try cached channel first, then fetch if cache misses (gateway-proxy scenario).
|
|
504
|
+
const cachedParentId = interaction.channel?.isThread() ? interaction.channel.parentId : null;
|
|
505
|
+
if (cachedParentId) {
|
|
506
|
+
const parentConfig = await getChannelDirectory(cachedParentId);
|
|
507
|
+
if (parentConfig) {
|
|
508
|
+
return parentConfig.directory;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Last resort: fetch the channel from Discord API to get parentId for threads
|
|
512
|
+
// when the channel isn't cached at all (common with gateway-proxy).
|
|
513
|
+
if (!cachedParentId) {
|
|
514
|
+
const fetched = await errore.tryAsync({
|
|
515
|
+
try: () => { return interaction.client.channels.fetch(channelId); },
|
|
516
|
+
catch: (e) => { return e; },
|
|
517
|
+
});
|
|
518
|
+
if (!(fetched instanceof Error) && fetched?.isThread() && fetched.parentId) {
|
|
519
|
+
const parentConfig = await getChannelDirectory(fetched.parentId);
|
|
520
|
+
if (parentConfig) {
|
|
521
|
+
return parentConfig.directory;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Resolve the working directory for a channel or thread.
|
|
529
|
+
* Returns both the base project directory (for server init) and the working directory
|
|
530
|
+
* (worktree directory if in a worktree thread, otherwise same as projectDirectory).
|
|
531
|
+
* This prevents commands from accidentally running in the base project dir when a
|
|
532
|
+
* worktree is active — the bug that caused /diff, /compact, etc. to use wrong cwd.
|
|
533
|
+
*/
|
|
534
|
+
export async function resolveWorkingDirectory({ channel, }) {
|
|
535
|
+
const isThread = [
|
|
536
|
+
ChannelType.PublicThread,
|
|
537
|
+
ChannelType.PrivateThread,
|
|
538
|
+
ChannelType.AnnouncementThread,
|
|
539
|
+
].includes(channel.type);
|
|
540
|
+
const textChannel = isThread
|
|
541
|
+
? await resolveTextChannel(channel)
|
|
542
|
+
: channel;
|
|
543
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
544
|
+
if (!metadata.projectDirectory) {
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
let workingDirectory = metadata.projectDirectory;
|
|
548
|
+
if (isThread) {
|
|
549
|
+
const worktreeInfo = await getThreadWorktree(channel.id);
|
|
550
|
+
if (worktreeInfo?.status === 'ready' && worktreeInfo.worktree_directory) {
|
|
551
|
+
workingDirectory = worktreeInfo.worktree_directory;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
projectDirectory: metadata.projectDirectory,
|
|
556
|
+
workingDirectory,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Upload files to a Discord thread/channel in a single message.
|
|
561
|
+
* Sending all files in one message causes Discord to display images in a grid layout.
|
|
562
|
+
*/
|
|
563
|
+
export async function uploadFilesToDiscord({ threadId, botToken, files, }) {
|
|
564
|
+
if (files.length === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
// Build attachments array for all files
|
|
568
|
+
const attachments = files.map((file, index) => ({
|
|
569
|
+
id: index,
|
|
570
|
+
filename: path.basename(file),
|
|
571
|
+
}));
|
|
572
|
+
const formData = new FormData();
|
|
573
|
+
formData.append('payload_json', JSON.stringify({ attachments }));
|
|
574
|
+
// Append each file with its array index, with correct MIME type for grid display
|
|
575
|
+
files.forEach((file, index) => {
|
|
576
|
+
const buffer = fs.readFileSync(file);
|
|
577
|
+
const mimeType = mime.getType(file) || 'application/octet-stream';
|
|
578
|
+
formData.append(`files[${index}]`, new Blob([buffer], { type: mimeType }), path.basename(file));
|
|
579
|
+
});
|
|
580
|
+
const response = await fetch(discordApiUrl(`/channels/${threadId}/messages`), {
|
|
581
|
+
method: 'POST',
|
|
582
|
+
headers: {
|
|
583
|
+
Authorization: `Bot ${botToken}`,
|
|
584
|
+
},
|
|
585
|
+
body: formData,
|
|
586
|
+
});
|
|
587
|
+
if (!response.ok) {
|
|
588
|
+
const error = await response.text();
|
|
589
|
+
throw new Error(`Discord API error: ${response.status} - ${error}`);
|
|
590
|
+
}
|
|
591
|
+
}
|