@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,155 @@
|
|
|
1
|
+
// /remove-project command - Remove Discord channels for a project.
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import * as errore from 'errore'
|
|
5
|
+
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
6
|
+
import {
|
|
7
|
+
findChannelsByDirectory,
|
|
8
|
+
deleteChannelDirectoriesByDirectory,
|
|
9
|
+
getAllTextChannelDirectories,
|
|
10
|
+
} from '../database.js'
|
|
11
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
12
|
+
import { abbreviatePath } from '../utils.js'
|
|
13
|
+
|
|
14
|
+
const logger = createLogger(LogPrefix.REMOVE_PROJECT)
|
|
15
|
+
|
|
16
|
+
export async function handleRemoveProjectCommand({
|
|
17
|
+
command,
|
|
18
|
+
appId,
|
|
19
|
+
}: CommandContext): Promise<void> {
|
|
20
|
+
await command.deferReply()
|
|
21
|
+
|
|
22
|
+
const directory = command.options.getString('project', true)
|
|
23
|
+
const guild = command.guild
|
|
24
|
+
|
|
25
|
+
if (!guild) {
|
|
26
|
+
await command.editReply('This command can only be used in a guild')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Get channel IDs for this directory
|
|
32
|
+
const channels = await findChannelsByDirectory({ directory })
|
|
33
|
+
|
|
34
|
+
if (channels.length === 0) {
|
|
35
|
+
await command.editReply(
|
|
36
|
+
`No channels found for directory: \`${directory}\``,
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const deletedChannels: string[] = []
|
|
42
|
+
const failedChannels: string[] = []
|
|
43
|
+
|
|
44
|
+
for (const { channel_id, channel_type } of channels as Array<{
|
|
45
|
+
channel_id: string
|
|
46
|
+
channel_type: string
|
|
47
|
+
}>) {
|
|
48
|
+
const channel = await errore.tryAsync({
|
|
49
|
+
try: () => guild.channels.fetch(channel_id),
|
|
50
|
+
catch: (e) => e as Error,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (channel instanceof Error) {
|
|
54
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel)
|
|
55
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (channel) {
|
|
60
|
+
try {
|
|
61
|
+
await channel.delete(`Removed by /remove-project command`)
|
|
62
|
+
deletedChannels.push(`${channel_type}: ${channel_id}`)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error)
|
|
65
|
+
failedChannels.push(`${channel_type}: ${channel_id}`)
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Remove from database
|
|
73
|
+
await deleteChannelDirectoriesByDirectory(directory)
|
|
74
|
+
|
|
75
|
+
const projectName = path.basename(directory)
|
|
76
|
+
let message = `Removed project **${projectName}**\n`
|
|
77
|
+
message += `Directory: \`${directory}\`\n\n`
|
|
78
|
+
|
|
79
|
+
if (deletedChannels.length > 0) {
|
|
80
|
+
message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (failedChannels.length > 0) {
|
|
84
|
+
message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await command.editReply(message)
|
|
88
|
+
logger.log(`Removed project ${projectName} at ${directory}`)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error('[REMOVE-PROJECT] Error:', error)
|
|
91
|
+
await command.editReply(
|
|
92
|
+
`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function handleRemoveProjectAutocomplete({
|
|
98
|
+
interaction,
|
|
99
|
+
appId,
|
|
100
|
+
}: AutocompleteContext): Promise<void> {
|
|
101
|
+
const focusedValue = interaction.options.getFocused()
|
|
102
|
+
const guild = interaction.guild
|
|
103
|
+
|
|
104
|
+
if (!guild) {
|
|
105
|
+
await interaction.respond([])
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Get all directories with channels
|
|
111
|
+
const allChannels = (await findChannelsByDirectory({
|
|
112
|
+
channelType: 'text',
|
|
113
|
+
})) as Array<{
|
|
114
|
+
directory: string
|
|
115
|
+
channel_id: string
|
|
116
|
+
}>
|
|
117
|
+
|
|
118
|
+
// Filter to only channels that exist in this guild
|
|
119
|
+
const projectsInGuild: { directory: string; channelId: string }[] = []
|
|
120
|
+
|
|
121
|
+
for (const { directory, channel_id } of allChannels) {
|
|
122
|
+
const channel = await errore.tryAsync({
|
|
123
|
+
try: () => guild.channels.fetch(channel_id),
|
|
124
|
+
catch: (e) => e as Error,
|
|
125
|
+
})
|
|
126
|
+
if (channel instanceof Error) {
|
|
127
|
+
// Channel not in this guild, skip
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (channel) {
|
|
131
|
+
projectsInGuild.push({ directory, channelId: channel_id })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const projects = projectsInGuild
|
|
136
|
+
.filter(({ directory }) => {
|
|
137
|
+
const baseName = path.basename(directory)
|
|
138
|
+
const searchText = `${baseName} ${directory}`.toLowerCase()
|
|
139
|
+
return searchText.includes(focusedValue.toLowerCase())
|
|
140
|
+
})
|
|
141
|
+
.slice(0, 25)
|
|
142
|
+
.map(({ directory }) => {
|
|
143
|
+
const name = `${path.basename(directory)} (${abbreviatePath(directory)})`
|
|
144
|
+
return {
|
|
145
|
+
name: name.length > 100 ? name.slice(0, 99) + '...' : name,
|
|
146
|
+
value: directory,
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await interaction.respond(projects)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error('[AUTOCOMPLETE] Error fetching projects:', error)
|
|
153
|
+
await interaction.respond([])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// /restart-opencode-server command - Restart the single shared opencode server
|
|
2
|
+
// and re-register Discord slash commands.
|
|
3
|
+
// Used for resolving opencode state issues, internal bugs, refreshing auth state,
|
|
4
|
+
// plugins, and picking up new/changed slash commands or agents. Aborts in-progress
|
|
5
|
+
// sessions in this channel before restarting. Note: since there is one shared server,
|
|
6
|
+
// this restart affects all projects. Other runtimes reconnect through their listener
|
|
7
|
+
// backoff loop once the shared server comes back.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ChannelType,
|
|
11
|
+
MessageFlags,
|
|
12
|
+
type ThreadChannel,
|
|
13
|
+
type TextChannel,
|
|
14
|
+
} from 'discord.js'
|
|
15
|
+
import type { Command as OpencodeCommand } from '@opencode-ai/sdk/v2'
|
|
16
|
+
import type { CommandContext } from './types.js'
|
|
17
|
+
import { initializeOpencodeForDirectory, restartOpencodeServer } from '../opencode.js'
|
|
18
|
+
import {
|
|
19
|
+
resolveWorkingDirectory,
|
|
20
|
+
SILENT_MESSAGE_FLAGS,
|
|
21
|
+
} from '../discord-utils.js'
|
|
22
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
23
|
+
import { disposeRuntimesForDirectory } from '../session-handler/thread-session-runtime.js'
|
|
24
|
+
import { registerCommands, type AgentInfo } from '../discord-command-registration.js'
|
|
25
|
+
|
|
26
|
+
const logger = createLogger(LogPrefix.OPENCODE)
|
|
27
|
+
|
|
28
|
+
export async function handleRestartOpencodeServerCommand({
|
|
29
|
+
command,
|
|
30
|
+
appId,
|
|
31
|
+
}: CommandContext): Promise<void> {
|
|
32
|
+
const channel = command.channel
|
|
33
|
+
|
|
34
|
+
if (!channel) {
|
|
35
|
+
await command.reply({
|
|
36
|
+
content: 'This command can only be used in a channel',
|
|
37
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
38
|
+
})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isThread = [
|
|
43
|
+
ChannelType.PublicThread,
|
|
44
|
+
ChannelType.PrivateThread,
|
|
45
|
+
ChannelType.AnnouncementThread,
|
|
46
|
+
].includes(channel.type)
|
|
47
|
+
|
|
48
|
+
const isTextChannel = channel.type === ChannelType.GuildText
|
|
49
|
+
|
|
50
|
+
if (!isThread && !isTextChannel) {
|
|
51
|
+
await command.reply({
|
|
52
|
+
content: 'This command can only be used in text channels or threads',
|
|
53
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
54
|
+
})
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolved = await resolveWorkingDirectory({
|
|
59
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
await command.reply({
|
|
64
|
+
content: 'Could not determine project directory for this channel',
|
|
65
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { projectDirectory } = resolved
|
|
71
|
+
|
|
72
|
+
// Defer reply since restart may take a moment
|
|
73
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
74
|
+
|
|
75
|
+
// Dispose all runtimes for this directory/channel scope.
|
|
76
|
+
// disposeRuntimesForDirectory aborts active runs, kills listeners, and
|
|
77
|
+
// removes runtimes from the registry. Scoped by channelId so runtimes
|
|
78
|
+
// in other channels sharing the same project directory are not affected.
|
|
79
|
+
const parentChannelId = isThread
|
|
80
|
+
? (channel as ThreadChannel).parentId
|
|
81
|
+
: channel.id
|
|
82
|
+
const abortedCount = disposeRuntimesForDirectory({
|
|
83
|
+
directory: projectDirectory,
|
|
84
|
+
channelId: parentChannelId || undefined,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
logger.log(`[RESTART] Restarting shared opencode server`)
|
|
88
|
+
|
|
89
|
+
const result = await restartOpencodeServer()
|
|
90
|
+
|
|
91
|
+
if (result instanceof Error) {
|
|
92
|
+
logger.error('[RESTART] Failed:', result)
|
|
93
|
+
await command.editReply({
|
|
94
|
+
content: `Failed to restart opencode server: ${result.message}`,
|
|
95
|
+
})
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const abortMsg =
|
|
100
|
+
abortedCount > 0
|
|
101
|
+
? ` (aborted ${abortedCount} active session${abortedCount > 1 ? 's' : ''})`
|
|
102
|
+
: ''
|
|
103
|
+
await command.editReply({
|
|
104
|
+
content: `Opencode server **restarted** successfully${abortMsg}. Re-registering slash commands...`,
|
|
105
|
+
})
|
|
106
|
+
logger.log('[RESTART] Shared opencode server restarted')
|
|
107
|
+
|
|
108
|
+
// Re-register Discord slash commands after restart so new/changed
|
|
109
|
+
// commands, agents, and plugins are picked up immediately.
|
|
110
|
+
const token = command.client.token
|
|
111
|
+
if (!token) {
|
|
112
|
+
logger.error('[RESTART] No bot token available, skipping command registration')
|
|
113
|
+
await command.editReply({
|
|
114
|
+
content: `Opencode server **restarted**${abortMsg}, but slash command re-registration skipped (no bot token)`,
|
|
115
|
+
})
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const guildIds = [...command.client.guilds.cache.keys()]
|
|
119
|
+
|
|
120
|
+
const opencodeResult = await initializeOpencodeForDirectory(projectDirectory)
|
|
121
|
+
const [userCommands, agents]: [OpencodeCommand[], AgentInfo[]] =
|
|
122
|
+
await (async (): Promise<[OpencodeCommand[], AgentInfo[]]> => {
|
|
123
|
+
if (opencodeResult instanceof Error) {
|
|
124
|
+
logger.warn('[RESTART] OpenCode init failed, registering without user commands:', opencodeResult.message)
|
|
125
|
+
return [[], []]
|
|
126
|
+
}
|
|
127
|
+
const getClient = opencodeResult
|
|
128
|
+
const [cmds, ags] = await Promise.all([
|
|
129
|
+
getClient()
|
|
130
|
+
.command.list({ directory: projectDirectory })
|
|
131
|
+
.then((r) => r.data || [])
|
|
132
|
+
.catch((e) => {
|
|
133
|
+
logger.warn('[RESTART] Failed to load user commands:', e instanceof Error ? e.stack : String(e))
|
|
134
|
+
return [] as OpencodeCommand[]
|
|
135
|
+
}),
|
|
136
|
+
getClient()
|
|
137
|
+
.app.agents({ directory: projectDirectory })
|
|
138
|
+
.then((r) => r.data || [])
|
|
139
|
+
.catch((e) => {
|
|
140
|
+
logger.warn('[RESTART] Failed to load agents:', e instanceof Error ? e.stack : String(e))
|
|
141
|
+
return [] as AgentInfo[]
|
|
142
|
+
}),
|
|
143
|
+
])
|
|
144
|
+
return [cmds, ags]
|
|
145
|
+
})()
|
|
146
|
+
|
|
147
|
+
const registerResult = await registerCommands({ token, appId, guildIds, userCommands, agents })
|
|
148
|
+
.then(() => null)
|
|
149
|
+
.catch((e: unknown) => (e instanceof Error ? e : new Error(String(e))))
|
|
150
|
+
if (registerResult instanceof Error) {
|
|
151
|
+
logger.error('[RESTART] Failed to re-register commands:', registerResult.message)
|
|
152
|
+
await command.editReply({
|
|
153
|
+
content: `Opencode server **restarted**${abortMsg}, but slash command re-registration failed: ${registerResult.message}`,
|
|
154
|
+
})
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
logger.log('[RESTART] Slash commands re-registered')
|
|
159
|
+
await command.editReply({
|
|
160
|
+
content: `Opencode server **restarted** and slash commands **re-registered**${abortMsg}`,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// /resume command - Resume an existing OpenCode session.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChannelType,
|
|
5
|
+
ThreadAutoArchiveDuration,
|
|
6
|
+
type TextChannel,
|
|
7
|
+
type ThreadChannel,
|
|
8
|
+
} from 'discord.js'
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import type { CommandContext, AutocompleteContext } from './types.js'
|
|
11
|
+
import {
|
|
12
|
+
getChannelDirectory,
|
|
13
|
+
setThreadSession,
|
|
14
|
+
setPartMessagesBatch,
|
|
15
|
+
getAllThreadSessionIds,
|
|
16
|
+
} from '../database.js'
|
|
17
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
18
|
+
import {
|
|
19
|
+
sendThreadMessage,
|
|
20
|
+
resolveProjectDirectoryFromAutocomplete,
|
|
21
|
+
NOTIFY_MESSAGE_FLAGS,
|
|
22
|
+
} from '../discord-utils.js'
|
|
23
|
+
import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js'
|
|
24
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
25
|
+
import * as errore from 'errore'
|
|
26
|
+
|
|
27
|
+
const logger = createLogger(LogPrefix.RESUME)
|
|
28
|
+
|
|
29
|
+
export async function handleResumeCommand({
|
|
30
|
+
command,
|
|
31
|
+
}: CommandContext): Promise<void> {
|
|
32
|
+
await command.deferReply()
|
|
33
|
+
|
|
34
|
+
const sessionId = command.options.getString('session', true)
|
|
35
|
+
const channel = command.channel
|
|
36
|
+
|
|
37
|
+
const isThread =
|
|
38
|
+
channel &&
|
|
39
|
+
[
|
|
40
|
+
ChannelType.PublicThread,
|
|
41
|
+
ChannelType.PrivateThread,
|
|
42
|
+
ChannelType.AnnouncementThread,
|
|
43
|
+
].includes(channel.type)
|
|
44
|
+
|
|
45
|
+
if (isThread) {
|
|
46
|
+
await command.editReply(
|
|
47
|
+
'This command can only be used in project channels, not threads',
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
53
|
+
await command.editReply('This command can only be used in text channels')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const textChannel = channel as TextChannel
|
|
58
|
+
|
|
59
|
+
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
60
|
+
const projectDirectory = channelConfig?.directory
|
|
61
|
+
|
|
62
|
+
if (!projectDirectory) {
|
|
63
|
+
await command.editReply(
|
|
64
|
+
'This channel is not configured with a project directory',
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
70
|
+
await command.editReply(`Directory does not exist: ${projectDirectory}`)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
76
|
+
if (getClient instanceof Error) {
|
|
77
|
+
await command.editReply(getClient.message)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sessionResponse = await getClient().session.get({
|
|
82
|
+
sessionID: sessionId,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!sessionResponse.data) {
|
|
86
|
+
await command.editReply('Session not found')
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sessionTitle = sessionResponse.data.title
|
|
91
|
+
|
|
92
|
+
const thread = await textChannel.threads.create({
|
|
93
|
+
name: `Resume: ${sessionTitle}`.slice(0, 100),
|
|
94
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
95
|
+
reason: `Resuming session ${sessionId}`,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Claim the resumed session immediately so external polling does not race
|
|
99
|
+
// and create a duplicate Sync thread before the rest of this setup runs.
|
|
100
|
+
await setThreadSession(thread.id, sessionId)
|
|
101
|
+
|
|
102
|
+
// Add user to thread so it appears in their sidebar
|
|
103
|
+
await thread.members.add(command.user.id)
|
|
104
|
+
|
|
105
|
+
logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`)
|
|
106
|
+
|
|
107
|
+
const messagesResponse = await getClient().session.messages({
|
|
108
|
+
sessionID: sessionId,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (!messagesResponse.data) {
|
|
112
|
+
throw new Error('Failed to fetch session messages')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const messages = messagesResponse.data
|
|
116
|
+
|
|
117
|
+
await command.editReply(
|
|
118
|
+
`Resumed session "${sessionTitle}" in ${thread.toString()}`,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
await sendThreadMessage(
|
|
122
|
+
thread,
|
|
123
|
+
`**Resumed session:** ${sessionTitle}\n**Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const { chunks, skippedCount } = collectSessionChunks({
|
|
128
|
+
messages,
|
|
129
|
+
limit: 30,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (skippedCount > 0) {
|
|
133
|
+
await sendThreadMessage(
|
|
134
|
+
thread,
|
|
135
|
+
`*Skipped ${skippedCount} older assistant parts...*`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const batched = batchChunksForDiscord(chunks)
|
|
140
|
+
for (const batch of batched) {
|
|
141
|
+
const discordMessage = await sendThreadMessage(thread, batch.content)
|
|
142
|
+
await setPartMessagesBatch(
|
|
143
|
+
batch.partIds.map((partId) => ({
|
|
144
|
+
partId,
|
|
145
|
+
messageId: discordMessage.id,
|
|
146
|
+
threadId: thread.id,
|
|
147
|
+
})),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const messageCount = messages.length
|
|
152
|
+
|
|
153
|
+
await sendThreadMessage(
|
|
154
|
+
thread,
|
|
155
|
+
`**Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`,
|
|
156
|
+
)
|
|
157
|
+
} catch (sendError) {
|
|
158
|
+
logger.error('[RESUME] Error sending messages to thread:', sendError)
|
|
159
|
+
await sendThreadMessage(
|
|
160
|
+
thread,
|
|
161
|
+
`Failed to load message history, but session is connected. You can still send new messages.`,
|
|
162
|
+
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.error('[RESUME] Error:', error)
|
|
167
|
+
await command.editReply(
|
|
168
|
+
`Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function handleResumeAutocomplete({
|
|
174
|
+
interaction,
|
|
175
|
+
}: AutocompleteContext): Promise<void> {
|
|
176
|
+
const focusedValue = interaction.options.getFocused()
|
|
177
|
+
|
|
178
|
+
// interaction.channel can be null when the channel isn't cached
|
|
179
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
180
|
+
// from the raw interaction payload.
|
|
181
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
182
|
+
|
|
183
|
+
if (!projectDirectory) {
|
|
184
|
+
await interaction.respond([])
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
190
|
+
if (getClient instanceof Error) {
|
|
191
|
+
await interaction.respond([])
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const sessionsResponse = await getClient().session.list()
|
|
196
|
+
if (!sessionsResponse.data) {
|
|
197
|
+
await interaction.respond([])
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const existingSessionIds = new Set(await getAllThreadSessionIds())
|
|
202
|
+
|
|
203
|
+
const sessions = sessionsResponse.data
|
|
204
|
+
.filter((session) => !existingSessionIds.has(session.id))
|
|
205
|
+
.filter((session) =>
|
|
206
|
+
session.title.toLowerCase().includes(focusedValue.toLowerCase()),
|
|
207
|
+
)
|
|
208
|
+
.slice(0, 25)
|
|
209
|
+
.map((session) => {
|
|
210
|
+
const dateStr = new Date(session.time.updated).toLocaleString()
|
|
211
|
+
const suffix = ` (${dateStr})`
|
|
212
|
+
const maxTitleLength = 100 - suffix.length
|
|
213
|
+
|
|
214
|
+
let title = session.title
|
|
215
|
+
if (title.length > maxTitleLength) {
|
|
216
|
+
title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
name: `${title}${suffix}`,
|
|
221
|
+
value: session.id,
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await interaction.respond(sessions)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.error('[AUTOCOMPLETE] Error fetching sessions:', error)
|
|
228
|
+
await interaction.respond([])
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// /run-shell-command command - Run an arbitrary shell command in the project directory.
|
|
2
|
+
// Resolves the project directory from the channel and executes the command with it as cwd.
|
|
3
|
+
// Also used by the ! prefix shortcut in discord messages (e.g. "!ls -la").
|
|
4
|
+
// Messages starting with ! are intercepted before session handling and routed here.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ChannelType,
|
|
8
|
+
MessageFlags,
|
|
9
|
+
type TextChannel,
|
|
10
|
+
type ThreadChannel,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import type { CommandContext } from './types.js'
|
|
13
|
+
import {
|
|
14
|
+
resolveWorkingDirectory,
|
|
15
|
+
SILENT_MESSAGE_FLAGS,
|
|
16
|
+
} from '../discord-utils.js'
|
|
17
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
18
|
+
import { execAsync } from '../worktrees.js'
|
|
19
|
+
import { stripAnsi } from '../utils.js'
|
|
20
|
+
|
|
21
|
+
const logger = createLogger(LogPrefix.INTERACTION)
|
|
22
|
+
|
|
23
|
+
const MAX_OUTPUT_CHARS = 1900
|
|
24
|
+
|
|
25
|
+
export async function runShellCommand({
|
|
26
|
+
command,
|
|
27
|
+
directory,
|
|
28
|
+
}: {
|
|
29
|
+
command: string
|
|
30
|
+
directory: string
|
|
31
|
+
}): Promise<string> {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout, stderr } = await execAsync(command, { cwd: directory })
|
|
34
|
+
const output = stripAnsi([stdout, stderr].filter(Boolean).join('\n').trim())
|
|
35
|
+
|
|
36
|
+
const header = `\`${command}\` exited with 0`
|
|
37
|
+
if (!output) {
|
|
38
|
+
return header
|
|
39
|
+
}
|
|
40
|
+
return formatOutput(output, header)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const execError = error as {
|
|
43
|
+
stdout?: string
|
|
44
|
+
stderr?: string
|
|
45
|
+
message?: string
|
|
46
|
+
code?: number | string
|
|
47
|
+
}
|
|
48
|
+
const output = stripAnsi(
|
|
49
|
+
[execError.stdout, execError.stderr].filter(Boolean).join('\n').trim(),
|
|
50
|
+
)
|
|
51
|
+
const exitCode = execError.code ?? 1
|
|
52
|
+
logger.error(
|
|
53
|
+
`[RUN-COMMAND] Command "${command}" exited with ${exitCode}:`,
|
|
54
|
+
error,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const header = `\`${command}\` exited with ${exitCode}`
|
|
58
|
+
return formatOutput(output || execError.message || 'Unknown error', header)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function handleRunCommand({
|
|
63
|
+
command,
|
|
64
|
+
}: CommandContext): Promise<void> {
|
|
65
|
+
const channel = command.channel
|
|
66
|
+
|
|
67
|
+
if (!channel) {
|
|
68
|
+
await command.reply({
|
|
69
|
+
content: 'This command can only be used in a channel.',
|
|
70
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isThread = [
|
|
76
|
+
ChannelType.PublicThread,
|
|
77
|
+
ChannelType.PrivateThread,
|
|
78
|
+
ChannelType.AnnouncementThread,
|
|
79
|
+
].includes(channel.type)
|
|
80
|
+
|
|
81
|
+
const isTextChannel = channel.type === ChannelType.GuildText
|
|
82
|
+
|
|
83
|
+
if (!isThread && !isTextChannel) {
|
|
84
|
+
await command.reply({
|
|
85
|
+
content: 'This command can only be used in a text channel or thread.',
|
|
86
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolved = await resolveWorkingDirectory({
|
|
92
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!resolved) {
|
|
96
|
+
await command.reply({
|
|
97
|
+
content: 'Could not determine project directory for this channel.',
|
|
98
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
99
|
+
})
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const input = command.options.getString('command', true)
|
|
104
|
+
|
|
105
|
+
await command.deferReply()
|
|
106
|
+
|
|
107
|
+
const result = await runShellCommand({
|
|
108
|
+
command: input,
|
|
109
|
+
directory: resolved.workingDirectory,
|
|
110
|
+
})
|
|
111
|
+
await command.editReply({ content: result })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatOutput(output: string, header: string): string {
|
|
115
|
+
// Reserve space for header + newline + code block delimiters (```\n...\n```)
|
|
116
|
+
const overhead = header.length + 1 + 3 + 1 + 1 + 3 // header\n```\n...\n```
|
|
117
|
+
const maxContent = MAX_OUTPUT_CHARS - overhead
|
|
118
|
+
const truncated =
|
|
119
|
+
output.length > maxContent
|
|
120
|
+
? output.slice(0, maxContent - 14) + '\n... truncated'
|
|
121
|
+
: output
|
|
122
|
+
return `${header}\n\`\`\`\n${truncated}\n\`\`\``
|
|
123
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { buildNoVncUrl, createScreenshareTunnelId } from './screenshare.js'
|
|
3
|
+
|
|
4
|
+
describe('screenshare security defaults', () => {
|
|
5
|
+
test('generates a 128-bit tunnel id', () => {
|
|
6
|
+
const ids = new Set(
|
|
7
|
+
Array.from({ length: 32 }, () => {
|
|
8
|
+
return createScreenshareTunnelId()
|
|
9
|
+
}),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
expect(ids.size).toBe(32)
|
|
13
|
+
for (const id of ids) {
|
|
14
|
+
expect(id).toMatch(/^[0-9a-f]{32}$/)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('builds a secure noVNC URL', () => {
|
|
19
|
+
const url = new URL(
|
|
20
|
+
buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
expect(url.origin).toBe('https://novnc.com')
|
|
24
|
+
expect(url.searchParams.get('host')).toBe(
|
|
25
|
+
'0123456789abcdef-tunnel.kimaki.xyz',
|
|
26
|
+
)
|
|
27
|
+
expect(url.searchParams.get('port')).toBe('443')
|
|
28
|
+
expect(url.searchParams.get('encrypt')).toBe('1')
|
|
29
|
+
})
|
|
30
|
+
})
|