@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,205 @@
|
|
|
1
|
+
// /tasks command — list all scheduled tasks sorted by next run time.
|
|
2
|
+
// Renders a markdown table that the CV2 pipeline auto-formats for Discord,
|
|
3
|
+
// including HTML-backed action buttons for cancellable tasks.
|
|
4
|
+
import { ButtonInteraction, ChatInputCommandInteraction, ComponentType, MessageFlags, } from 'discord.js';
|
|
5
|
+
import { cancelScheduledTask, listScheduledTasks, } from '../database.js';
|
|
6
|
+
import { splitTablesFromMarkdown } from '../format-tables.js';
|
|
7
|
+
import { buildHtmlActionCustomId, cancelHtmlActionsForOwner, registerHtmlAction, } from '../html-actions.js';
|
|
8
|
+
import { formatTimeAgo } from './worktrees.js';
|
|
9
|
+
function formatTimeUntil(date) {
|
|
10
|
+
const diffMs = date.getTime() - Date.now();
|
|
11
|
+
if (diffMs <= 0) {
|
|
12
|
+
return 'due now';
|
|
13
|
+
}
|
|
14
|
+
const totalSeconds = Math.floor(diffMs / 1000);
|
|
15
|
+
if (totalSeconds < 60) {
|
|
16
|
+
return `in ${totalSeconds}s`;
|
|
17
|
+
}
|
|
18
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
19
|
+
if (totalMinutes < 60) {
|
|
20
|
+
return `in ${totalMinutes}m`;
|
|
21
|
+
}
|
|
22
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
23
|
+
const minutes = totalMinutes % 60;
|
|
24
|
+
if (hours < 24) {
|
|
25
|
+
return minutes > 0 ? `in ${hours}h ${minutes}m` : `in ${hours}h`;
|
|
26
|
+
}
|
|
27
|
+
const days = Math.floor(hours / 24);
|
|
28
|
+
const remainingHours = hours % 24;
|
|
29
|
+
return remainingHours > 0 ? `in ${days}d ${remainingHours}h` : `in ${days}d`;
|
|
30
|
+
}
|
|
31
|
+
function scheduleLabel(task) {
|
|
32
|
+
if (task.schedule_kind === 'cron') {
|
|
33
|
+
return task.cron_expr || 'cron';
|
|
34
|
+
}
|
|
35
|
+
return 'one-time';
|
|
36
|
+
}
|
|
37
|
+
function canCancelTask(task) {
|
|
38
|
+
return task.status === 'planned' || task.status === 'running';
|
|
39
|
+
}
|
|
40
|
+
// Escape pipe chars and collapse whitespace so free-text fields don't break
|
|
41
|
+
// GFM table column alignment.
|
|
42
|
+
function sanitizeTableCell(value) {
|
|
43
|
+
return value.replaceAll('|', '\\|').replace(/\s+/g, ' ').trim();
|
|
44
|
+
}
|
|
45
|
+
function buildCancelButtonHtml({ buttonId }) {
|
|
46
|
+
return `<button id="${buttonId}" variant="secondary">Delete</button>`;
|
|
47
|
+
}
|
|
48
|
+
function buildActionCell(task) {
|
|
49
|
+
if (!canCancelTask(task)) {
|
|
50
|
+
return '-';
|
|
51
|
+
}
|
|
52
|
+
return buildCancelButtonHtml({ buttonId: `cancel-task-${task.id}` });
|
|
53
|
+
}
|
|
54
|
+
// Cap rows to avoid exceeding Discord's 40-component CV2 limit.
|
|
55
|
+
// Each cancellable row renders as text + action row + button (~4 components),
|
|
56
|
+
// so 10 rows is a safe ceiling.
|
|
57
|
+
const MAX_TASK_ROWS = 10;
|
|
58
|
+
function buildTaskTable({ tasks, }) {
|
|
59
|
+
const header = '| ID | Status | Prompt | Schedule | Next Run | Action |';
|
|
60
|
+
const separator = '|---|---|---|---|---|---|';
|
|
61
|
+
const rows = tasks.map((task) => {
|
|
62
|
+
const id = String(task.id);
|
|
63
|
+
const status = task.status;
|
|
64
|
+
const prompt = sanitizeTableCell(task.prompt_preview.length > 240
|
|
65
|
+
? task.prompt_preview.slice(0, 237) + '...'
|
|
66
|
+
: task.prompt_preview);
|
|
67
|
+
const schedule = sanitizeTableCell(scheduleLabel(task));
|
|
68
|
+
const nextRun = (() => {
|
|
69
|
+
if (task.status === 'completed' ||
|
|
70
|
+
task.status === 'cancelled' ||
|
|
71
|
+
task.status === 'failed') {
|
|
72
|
+
return task.last_run_at ? formatTimeAgo(task.last_run_at) : '-';
|
|
73
|
+
}
|
|
74
|
+
return formatTimeUntil(task.next_run_at);
|
|
75
|
+
})();
|
|
76
|
+
const action = buildActionCell(task);
|
|
77
|
+
return `| ${id} | ${status} | ${prompt} | ${schedule} | ${nextRun} | ${action} |`;
|
|
78
|
+
});
|
|
79
|
+
return [header, separator, ...rows].join('\n');
|
|
80
|
+
}
|
|
81
|
+
function getTasksActionOwnerKey({ userId, channelId, }) {
|
|
82
|
+
return `tasks:${userId}:${channelId}`;
|
|
83
|
+
}
|
|
84
|
+
async function renderTasksReply({ guildId, userId, channelId, showAll, notice, editReply, }) {
|
|
85
|
+
const ownerKey = getTasksActionOwnerKey({ userId, channelId });
|
|
86
|
+
cancelHtmlActionsForOwner(ownerKey);
|
|
87
|
+
const statuses = showAll
|
|
88
|
+
? undefined
|
|
89
|
+
: ['planned', 'running'];
|
|
90
|
+
const allTasks = await listScheduledTasks({ statuses });
|
|
91
|
+
if (allTasks.length === 0) {
|
|
92
|
+
const message = notice
|
|
93
|
+
? `${notice}\n\nNo scheduled tasks found.`
|
|
94
|
+
: 'No scheduled tasks found.';
|
|
95
|
+
const textDisplay = {
|
|
96
|
+
type: ComponentType.TextDisplay,
|
|
97
|
+
content: message,
|
|
98
|
+
};
|
|
99
|
+
await editReply({
|
|
100
|
+
components: [textDisplay],
|
|
101
|
+
flags: MessageFlags.IsComponentsV2,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const tasks = allTasks.slice(0, MAX_TASK_ROWS);
|
|
106
|
+
const truncatedNotice = allTasks.length > MAX_TASK_ROWS
|
|
107
|
+
? `Showing ${MAX_TASK_ROWS}/${allTasks.length} tasks. Use \`kimaki task list\` for full list.`
|
|
108
|
+
: undefined;
|
|
109
|
+
const combinedNotice = [notice, truncatedNotice].filter(Boolean).join('\n');
|
|
110
|
+
const cancellableTasksByButtonId = new Map();
|
|
111
|
+
tasks.forEach((task) => {
|
|
112
|
+
if (!canCancelTask(task)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
cancellableTasksByButtonId.set(`cancel-task-${task.id}`, task);
|
|
116
|
+
});
|
|
117
|
+
const tableMarkdown = buildTaskTable({ tasks });
|
|
118
|
+
const markdown = combinedNotice
|
|
119
|
+
? `${combinedNotice}\n\n${tableMarkdown}`
|
|
120
|
+
: tableMarkdown;
|
|
121
|
+
const segments = splitTablesFromMarkdown(markdown, {
|
|
122
|
+
resolveButtonCustomId: ({ button }) => {
|
|
123
|
+
const task = cancellableTasksByButtonId.get(button.id);
|
|
124
|
+
if (!task) {
|
|
125
|
+
return new Error(`No task registered for button ${button.id}`);
|
|
126
|
+
}
|
|
127
|
+
const actionId = registerHtmlAction({
|
|
128
|
+
ownerKey,
|
|
129
|
+
threadId: String(task.id),
|
|
130
|
+
run: async ({ interaction }) => {
|
|
131
|
+
await handleCancelTaskAction({
|
|
132
|
+
interaction,
|
|
133
|
+
taskId: task.id,
|
|
134
|
+
showAll,
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
return buildHtmlActionCustomId(actionId);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const components = segments.flatMap((segment) => {
|
|
142
|
+
if (segment.type === 'components') {
|
|
143
|
+
return segment.components;
|
|
144
|
+
}
|
|
145
|
+
const textDisplay = {
|
|
146
|
+
type: ComponentType.TextDisplay,
|
|
147
|
+
content: segment.text,
|
|
148
|
+
};
|
|
149
|
+
return [textDisplay];
|
|
150
|
+
});
|
|
151
|
+
await editReply({
|
|
152
|
+
components,
|
|
153
|
+
flags: MessageFlags.IsComponentsV2,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function handleCancelTaskAction({ interaction, taskId, showAll, }) {
|
|
157
|
+
const guildId = interaction.guildId;
|
|
158
|
+
if (!guildId) {
|
|
159
|
+
await interaction.editReply({
|
|
160
|
+
components: [
|
|
161
|
+
{
|
|
162
|
+
type: ComponentType.TextDisplay,
|
|
163
|
+
content: 'This action can only be used in a server.',
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
flags: MessageFlags.IsComponentsV2,
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const cancelled = await cancelScheduledTask(taskId);
|
|
171
|
+
const notice = cancelled
|
|
172
|
+
? `Cancelled task #${taskId}.`
|
|
173
|
+
: `Task #${taskId} not found or already finalized.`;
|
|
174
|
+
await renderTasksReply({
|
|
175
|
+
guildId,
|
|
176
|
+
userId: interaction.user.id,
|
|
177
|
+
channelId: interaction.channelId,
|
|
178
|
+
showAll,
|
|
179
|
+
notice,
|
|
180
|
+
editReply: (options) => {
|
|
181
|
+
return interaction.editReply(options);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
export async function handleTasksCommand({ command, }) {
|
|
186
|
+
const guildId = command.guildId;
|
|
187
|
+
if (!guildId) {
|
|
188
|
+
await command.reply({
|
|
189
|
+
content: 'This command can only be used in a server.',
|
|
190
|
+
flags: MessageFlags.Ephemeral,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const showAll = command.options.getBoolean('all') ?? false;
|
|
195
|
+
await command.deferReply({ flags: MessageFlags.Ephemeral });
|
|
196
|
+
await renderTasksReply({
|
|
197
|
+
guildId,
|
|
198
|
+
userId: command.user.id,
|
|
199
|
+
channelId: command.channelId,
|
|
200
|
+
showAll,
|
|
201
|
+
editReply: (options) => {
|
|
202
|
+
return command.editReply(options);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Undo/Redo commands - /undo, /redo
|
|
2
|
+
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
3
|
+
import { getThreadSession } from '../database.js';
|
|
4
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
|
+
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
6
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
|
+
const logger = createLogger(LogPrefix.UNDO_REDO);
|
|
8
|
+
async function waitForSessionIdle({ client, sessionId, directory, timeoutMs = 2_000, }) {
|
|
9
|
+
const deadline = Date.now() + timeoutMs;
|
|
10
|
+
while (Date.now() < deadline) {
|
|
11
|
+
const statusResponse = await client.session.status({ directory });
|
|
12
|
+
const sessionStatus = statusResponse.data?.[sessionId];
|
|
13
|
+
if (!sessionStatus || sessionStatus.type === 'idle') {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await new Promise((resolve) => {
|
|
17
|
+
setTimeout(resolve, 50);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function handleUndoCommand({ command, }) {
|
|
22
|
+
const channel = command.channel;
|
|
23
|
+
if (!channel) {
|
|
24
|
+
await command.reply({
|
|
25
|
+
content: 'This command can only be used in a channel',
|
|
26
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const isThread = [
|
|
31
|
+
ChannelType.PublicThread,
|
|
32
|
+
ChannelType.PrivateThread,
|
|
33
|
+
ChannelType.AnnouncementThread,
|
|
34
|
+
].includes(channel.type);
|
|
35
|
+
if (!isThread) {
|
|
36
|
+
await command.reply({
|
|
37
|
+
content: 'This command can only be used in a thread with an active session',
|
|
38
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const resolved = await resolveWorkingDirectory({
|
|
43
|
+
channel: channel,
|
|
44
|
+
});
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
await command.reply({
|
|
47
|
+
content: 'Could not determine project directory for this channel',
|
|
48
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const { projectDirectory, workingDirectory } = resolved;
|
|
53
|
+
const sessionId = await getThreadSession(channel.id);
|
|
54
|
+
if (!sessionId) {
|
|
55
|
+
await command.reply({
|
|
56
|
+
content: 'No active session in this thread',
|
|
57
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
62
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
63
|
+
if (getClient instanceof Error) {
|
|
64
|
+
await command.editReply(`Failed to undo: ${getClient.message}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const client = getClient();
|
|
69
|
+
// Fetch session to check existing revert state
|
|
70
|
+
const sessionResponse = await client.session.get({
|
|
71
|
+
sessionID: sessionId,
|
|
72
|
+
directory: workingDirectory,
|
|
73
|
+
});
|
|
74
|
+
if (sessionResponse.error) {
|
|
75
|
+
await command.editReply(`Failed to undo: ${JSON.stringify(sessionResponse.error)}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Abort if session is busy before reverting, matching TUI behavior
|
|
79
|
+
// (use-session-commands.tsx always aborts non-idle sessions before revert).
|
|
80
|
+
// session.status() returns a sparse map — only non-idle sessions have entries,
|
|
81
|
+
// so a missing key means idle.
|
|
82
|
+
const statusResponse = await client.session.status({
|
|
83
|
+
directory: workingDirectory,
|
|
84
|
+
});
|
|
85
|
+
const sessionStatus = statusResponse.data?.[sessionId];
|
|
86
|
+
if (sessionStatus && sessionStatus.type !== 'idle') {
|
|
87
|
+
await client.session.abort({
|
|
88
|
+
sessionID: sessionId,
|
|
89
|
+
directory: workingDirectory,
|
|
90
|
+
}).catch((error) => {
|
|
91
|
+
logger.warn(`[UNDO] abort failed for ${sessionId}`, error);
|
|
92
|
+
});
|
|
93
|
+
await waitForSessionIdle({
|
|
94
|
+
client,
|
|
95
|
+
sessionId,
|
|
96
|
+
directory: workingDirectory,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const messagesResponse = await client.session.messages({
|
|
100
|
+
sessionID: sessionId,
|
|
101
|
+
directory: workingDirectory,
|
|
102
|
+
});
|
|
103
|
+
if (messagesResponse.error) {
|
|
104
|
+
await command.editReply(`Failed to undo: ${JSON.stringify(messagesResponse.error)}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!messagesResponse.data || messagesResponse.data.length === 0) {
|
|
108
|
+
await command.editReply('No messages to undo');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Follow the same approach as the OpenCode TUI (use-session-commands.tsx):
|
|
112
|
+
// find the last user message that is before the current revert point
|
|
113
|
+
// (or the last user message if no revert is active). This matches the
|
|
114
|
+
// TUI's `findLast(userMessages(), (x) => !revert || x.id < revert)`.
|
|
115
|
+
const currentRevert = sessionResponse.data?.revert?.messageID;
|
|
116
|
+
const userMessages = messagesResponse.data.filter((m) => {
|
|
117
|
+
return m.info.role === 'user';
|
|
118
|
+
});
|
|
119
|
+
const targetUserMessage = [...userMessages].reverse().find((m) => {
|
|
120
|
+
return !currentRevert || m.info.id < currentRevert;
|
|
121
|
+
});
|
|
122
|
+
if (!targetUserMessage) {
|
|
123
|
+
await command.editReply('No messages to undo');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const targetAssistantMessage = [...messagesResponse.data].reverse().find((m) => {
|
|
127
|
+
return m.info.role === 'assistant' && m.info.parentID === targetUserMessage.info.id;
|
|
128
|
+
});
|
|
129
|
+
const revertMessageId = targetAssistantMessage?.info.id || targetUserMessage.info.id;
|
|
130
|
+
// session.revert() reverts filesystem patches (file edits, writes) and
|
|
131
|
+
// marks the session with revert.messageID. Messages are NOT deleted — they
|
|
132
|
+
// get cleaned up automatically on the next promptAsync() call via
|
|
133
|
+
// SessionRevert.cleanup(). The model only sees messages before the revert
|
|
134
|
+
// point when processing the next prompt.
|
|
135
|
+
logger.log(`[UNDO] session.revert start messageId=${revertMessageId}`);
|
|
136
|
+
let response = await client.session.revert({
|
|
137
|
+
sessionID: sessionId,
|
|
138
|
+
directory: workingDirectory,
|
|
139
|
+
messageID: revertMessageId,
|
|
140
|
+
});
|
|
141
|
+
logger.log(`[UNDO] session.revert done error=${Boolean(response.error)}`);
|
|
142
|
+
if (response.error) {
|
|
143
|
+
logger.log('[UNDO] retry wait idle before revert retry');
|
|
144
|
+
await waitForSessionIdle({
|
|
145
|
+
client,
|
|
146
|
+
sessionId,
|
|
147
|
+
directory: workingDirectory,
|
|
148
|
+
});
|
|
149
|
+
logger.log('[UNDO] retry revert start');
|
|
150
|
+
response = await client.session.revert({
|
|
151
|
+
sessionID: sessionId,
|
|
152
|
+
directory: workingDirectory,
|
|
153
|
+
messageID: revertMessageId,
|
|
154
|
+
});
|
|
155
|
+
logger.log(`[UNDO] retry revert done error=${Boolean(response.error)}`);
|
|
156
|
+
if (response.error) {
|
|
157
|
+
await command.editReply(`Failed to undo: ${JSON.stringify(response.error)}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const diffInfo = response.data?.revert?.diff
|
|
162
|
+
? `\n\`\`\`diff\n${response.data.revert.diff.slice(0, 1500)}\n\`\`\``
|
|
163
|
+
: '';
|
|
164
|
+
await command.editReply(`Undone - reverted last assistant message${diffInfo}`);
|
|
165
|
+
logger.log(`Session ${sessionId} reverted at message ${revertMessageId}`);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
logger.error('[UNDO] Error:', error);
|
|
169
|
+
await command.editReply(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function handleRedoCommand({ command, }) {
|
|
173
|
+
const channel = command.channel;
|
|
174
|
+
if (!channel) {
|
|
175
|
+
await command.reply({
|
|
176
|
+
content: 'This command can only be used in a channel',
|
|
177
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const isThread = [
|
|
182
|
+
ChannelType.PublicThread,
|
|
183
|
+
ChannelType.PrivateThread,
|
|
184
|
+
ChannelType.AnnouncementThread,
|
|
185
|
+
].includes(channel.type);
|
|
186
|
+
if (!isThread) {
|
|
187
|
+
await command.reply({
|
|
188
|
+
content: 'This command can only be used in a thread with an active session',
|
|
189
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const resolved = await resolveWorkingDirectory({
|
|
194
|
+
channel: channel,
|
|
195
|
+
});
|
|
196
|
+
if (!resolved) {
|
|
197
|
+
await command.reply({
|
|
198
|
+
content: 'Could not determine project directory for this channel',
|
|
199
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { projectDirectory, workingDirectory } = resolved;
|
|
204
|
+
const sessionId = await getThreadSession(channel.id);
|
|
205
|
+
if (!sessionId) {
|
|
206
|
+
await command.reply({
|
|
207
|
+
content: 'No active session in this thread',
|
|
208
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
213
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
214
|
+
if (getClient instanceof Error) {
|
|
215
|
+
await command.editReply(`Failed to redo: ${getClient.message}`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const client = getClient();
|
|
220
|
+
// Fetch session to check existing revert state
|
|
221
|
+
const sessionResponse = await client.session.get({
|
|
222
|
+
sessionID: sessionId,
|
|
223
|
+
directory: workingDirectory,
|
|
224
|
+
});
|
|
225
|
+
if (sessionResponse.error) {
|
|
226
|
+
await command.editReply(`Failed to redo: ${JSON.stringify(sessionResponse.error)}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const revertMessageID = sessionResponse.data?.revert?.messageID;
|
|
230
|
+
if (!revertMessageID) {
|
|
231
|
+
await command.editReply('Nothing to redo - no previous undo found');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Abort if session is busy before reverting/unreverting — both enforce
|
|
235
|
+
// assertNotBusy in OpenCode and would fail with "Session is busy"
|
|
236
|
+
const redoStatusResponse = await client.session.status({
|
|
237
|
+
directory: workingDirectory,
|
|
238
|
+
});
|
|
239
|
+
const redoSessionStatus = redoStatusResponse.data?.[sessionId];
|
|
240
|
+
if (redoSessionStatus && redoSessionStatus.type !== 'idle') {
|
|
241
|
+
await client.session.abort({
|
|
242
|
+
sessionID: sessionId,
|
|
243
|
+
directory: workingDirectory,
|
|
244
|
+
}).catch((error) => {
|
|
245
|
+
logger.warn(`[REDO] abort failed for ${sessionId}`, error);
|
|
246
|
+
});
|
|
247
|
+
await waitForSessionIdle({
|
|
248
|
+
client,
|
|
249
|
+
sessionId,
|
|
250
|
+
directory: workingDirectory,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
await new Promise((resolve) => {
|
|
254
|
+
setTimeout(resolve, 500);
|
|
255
|
+
});
|
|
256
|
+
// Follow the same approach as the OpenCode TUI (use-session-commands.tsx):
|
|
257
|
+
// find the next user message after the current revert point. If one exists,
|
|
258
|
+
// move the revert cursor forward to it (one step redo). If none exists,
|
|
259
|
+
// fully unrevert — we're at the end of the message history.
|
|
260
|
+
const messagesResponse = await client.session.messages({
|
|
261
|
+
sessionID: sessionId,
|
|
262
|
+
directory: workingDirectory,
|
|
263
|
+
});
|
|
264
|
+
if (messagesResponse.error) {
|
|
265
|
+
await command.editReply(`Failed to redo: ${JSON.stringify(messagesResponse.error)}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const userMessages = (messagesResponse.data ?? []).filter((m) => {
|
|
269
|
+
return m.info.role === 'user';
|
|
270
|
+
});
|
|
271
|
+
const nextMessage = userMessages.find((m) => {
|
|
272
|
+
return m.info.id > revertMessageID;
|
|
273
|
+
});
|
|
274
|
+
if (!nextMessage) {
|
|
275
|
+
// No more messages after revert point — fully unrevert
|
|
276
|
+
const response = await client.session.unrevert({
|
|
277
|
+
sessionID: sessionId,
|
|
278
|
+
directory: workingDirectory,
|
|
279
|
+
});
|
|
280
|
+
if (response.error) {
|
|
281
|
+
await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await command.editReply('Restored - session fully back to previous state');
|
|
285
|
+
logger.log(`Session ${sessionId} unrevert completed`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Move revert cursor forward one step to the next user message
|
|
289
|
+
const response = await client.session.revert({
|
|
290
|
+
sessionID: sessionId,
|
|
291
|
+
directory: workingDirectory,
|
|
292
|
+
messageID: nextMessage.info.id,
|
|
293
|
+
});
|
|
294
|
+
if (response.error) {
|
|
295
|
+
await command.editReply(`Failed to redo: ${JSON.stringify(response.error)}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await command.editReply('Restored one step forward');
|
|
299
|
+
logger.log(`Session ${sessionId} redo: moved revert to ${nextMessage.info.id}`);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
logger.error('[REDO] Error:', error);
|
|
303
|
+
await command.editReply(`Failed to redo: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// /unset-model-override command - Remove model overrides and use default instead.
|
|
2
|
+
import { ChatInputCommandInteraction, ChannelType, MessageFlags, } from 'discord.js';
|
|
3
|
+
import { getChannelModel, getSessionModel, getThreadSession, clearSessionModel, } from '../database.js';
|
|
4
|
+
import { getPrisma } from '../db.js';
|
|
5
|
+
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
+
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
|
+
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
8
|
+
import { getCurrentModelInfo } from './model.js';
|
|
9
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
|
+
const unsetModelLogger = createLogger(LogPrefix.MODEL);
|
|
11
|
+
function formatModelSource(type, agentName) {
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'session':
|
|
14
|
+
return 'session override';
|
|
15
|
+
case 'agent':
|
|
16
|
+
return `agent "${agentName}"`;
|
|
17
|
+
case 'channel':
|
|
18
|
+
return 'channel override';
|
|
19
|
+
case 'global':
|
|
20
|
+
return 'global default';
|
|
21
|
+
case 'opencode-config':
|
|
22
|
+
case 'opencode-recent':
|
|
23
|
+
case 'opencode-provider-default':
|
|
24
|
+
return 'opencode default';
|
|
25
|
+
default:
|
|
26
|
+
return 'none';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Handle the /unset-model-override slash command.
|
|
31
|
+
* In thread: clears session override if exists, otherwise channel override.
|
|
32
|
+
* In channel: clears channel override.
|
|
33
|
+
*/
|
|
34
|
+
export async function handleUnsetModelCommand({ interaction, appId, }) {
|
|
35
|
+
unsetModelLogger.log('[UNSET-MODEL] handleUnsetModelCommand called');
|
|
36
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
37
|
+
const channel = interaction.channel;
|
|
38
|
+
if (!channel) {
|
|
39
|
+
await interaction.editReply({
|
|
40
|
+
content: 'This command can only be used in a channel',
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const isThread = [
|
|
45
|
+
ChannelType.PublicThread,
|
|
46
|
+
ChannelType.PrivateThread,
|
|
47
|
+
ChannelType.AnnouncementThread,
|
|
48
|
+
].includes(channel.type);
|
|
49
|
+
let projectDirectory;
|
|
50
|
+
let targetChannelId;
|
|
51
|
+
let sessionId;
|
|
52
|
+
if (isThread) {
|
|
53
|
+
const thread = channel;
|
|
54
|
+
const textChannel = await resolveTextChannel(thread);
|
|
55
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
56
|
+
projectDirectory = metadata.projectDirectory;
|
|
57
|
+
targetChannelId = textChannel?.id || channel.id;
|
|
58
|
+
sessionId = await getThreadSession(thread.id);
|
|
59
|
+
}
|
|
60
|
+
else if (channel.type === ChannelType.GuildText) {
|
|
61
|
+
const textChannel = channel;
|
|
62
|
+
const metadata = await getKimakiMetadata(textChannel);
|
|
63
|
+
projectDirectory = metadata.projectDirectory;
|
|
64
|
+
targetChannelId = channel.id;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await interaction.editReply({
|
|
68
|
+
content: 'This command can only be used in text channels or threads',
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!projectDirectory) {
|
|
73
|
+
await interaction.editReply({
|
|
74
|
+
content: 'This channel is not configured with a project directory',
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Check what overrides exist
|
|
79
|
+
const [sessionPref, channelPref] = await Promise.all([
|
|
80
|
+
sessionId ? getSessionModel(sessionId) : Promise.resolve(undefined),
|
|
81
|
+
getChannelModel(targetChannelId),
|
|
82
|
+
]);
|
|
83
|
+
let clearedType = null;
|
|
84
|
+
let clearedModel;
|
|
85
|
+
if (isThread && sessionId && sessionPref) {
|
|
86
|
+
// In thread with session override: clear session
|
|
87
|
+
await clearSessionModel(sessionId);
|
|
88
|
+
clearedType = 'session';
|
|
89
|
+
clearedModel = sessionPref.modelId;
|
|
90
|
+
unsetModelLogger.log(`[UNSET-MODEL] Cleared session model for ${sessionId}`);
|
|
91
|
+
}
|
|
92
|
+
else if (channelPref) {
|
|
93
|
+
// Clear channel override
|
|
94
|
+
const prisma = await getPrisma();
|
|
95
|
+
await prisma.channel_models.deleteMany({
|
|
96
|
+
where: { channel_id: targetChannelId },
|
|
97
|
+
});
|
|
98
|
+
clearedType = 'channel';
|
|
99
|
+
clearedModel = channelPref.modelId;
|
|
100
|
+
unsetModelLogger.log(`[UNSET-MODEL] Cleared channel model for ${targetChannelId}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await interaction.editReply({
|
|
104
|
+
content: 'No model override to clear.',
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Get the new model that will be used
|
|
109
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
110
|
+
let newModelText = 'unknown';
|
|
111
|
+
if (!(getClient instanceof Error)) {
|
|
112
|
+
const newModelInfo = await getCurrentModelInfo({
|
|
113
|
+
sessionId,
|
|
114
|
+
channelId: targetChannelId,
|
|
115
|
+
appId,
|
|
116
|
+
getClient,
|
|
117
|
+
});
|
|
118
|
+
newModelText =
|
|
119
|
+
newModelInfo.type === 'none'
|
|
120
|
+
? 'none'
|
|
121
|
+
: `\`${newModelInfo.model}\` (${formatModelSource(newModelInfo.type, 'agentName' in newModelInfo ? newModelInfo.agentName : undefined)})`;
|
|
122
|
+
}
|
|
123
|
+
// Check if there's a running request and abort+retry with new model (only for session changes in threads)
|
|
124
|
+
let retried = false;
|
|
125
|
+
if (isThread && clearedType === 'session' && sessionId) {
|
|
126
|
+
const runtime = getRuntime(channel.id);
|
|
127
|
+
if (runtime) {
|
|
128
|
+
retried = await runtime.retryLastUserPrompt();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const clearedTypeText = clearedType === 'session' ? 'Session' : 'Channel';
|
|
132
|
+
const retriedText = retried
|
|
133
|
+
? '\n_Restarting current request with new model..._'
|
|
134
|
+
: '';
|
|
135
|
+
await interaction.editReply({
|
|
136
|
+
content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}${retriedText}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// /upgrade-and-restart command - Upgrade kimaki to the latest version and restart the bot.
|
|
2
|
+
// Checks npm for a newer version, installs it globally, then spawns a new kimaki process.
|
|
3
|
+
// The new process kills the old one on startup (kimaki's single-instance lock).
|
|
4
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
5
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
6
|
+
import { getCurrentVersion, upgrade } from '../upgrade.js';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
const logger = createLogger(LogPrefix.CLI);
|
|
9
|
+
export async function handleUpgradeAndRestartCommand({ command, }) {
|
|
10
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
11
|
+
logger.log('[UPGRADE] /upgrade-and-restart triggered');
|
|
12
|
+
try {
|
|
13
|
+
const currentVersion = getCurrentVersion();
|
|
14
|
+
const newVersion = await upgrade();
|
|
15
|
+
if (!newVersion) {
|
|
16
|
+
await command.editReply({
|
|
17
|
+
content: `Already on latest version: **v${currentVersion}**`,
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await command.editReply({
|
|
22
|
+
content: `Upgraded kimaki **v${currentVersion}** -> **v${newVersion}**. Restarting bot...`,
|
|
23
|
+
});
|
|
24
|
+
// Spawning bare `kimaki` works even if the user originally ran via npx/bunx:
|
|
25
|
+
// `npm i -g kimaki@latest` creates a global bin link, and npx resolves
|
|
26
|
+
// local -> global -> cache -> registry, so it prefers the global install.
|
|
27
|
+
// bunx shares the same global cache, so it also picks up the new version.
|
|
28
|
+
const child = spawn('kimaki', process.argv.slice(2), {
|
|
29
|
+
shell: true,
|
|
30
|
+
stdio: 'ignore',
|
|
31
|
+
detached: true,
|
|
32
|
+
});
|
|
33
|
+
child.unref();
|
|
34
|
+
logger.debug('Started new background kimaki');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.error('[UPGRADE] Failed:', error);
|
|
38
|
+
await command.editReply({
|
|
39
|
+
content: `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|