@otto-assistant/otto 0.1.2 → 0.7.16
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-account-identity.js +62 -0
- package/dist/anthropic-account-identity.test.js +38 -0
- package/dist/anthropic-auth-plugin.js +917 -0
- package/dist/anthropic-auth-state.js +303 -0
- package/dist/anthropic-auth-state.test.js +150 -0
- package/dist/bin.js +152 -0
- package/dist/btw-prefix-detection.js +17 -0
- package/dist/btw-prefix-detection.test.js +63 -0
- package/dist/channel-management.js +259 -0
- package/dist/cli-parsing.test.js +142 -0
- package/dist/cli-send-thread.e2e.test.js +353 -0
- package/dist/cli-telegram-options.test.js +99 -0
- package/dist/cli.js +4210 -568
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-dir.js +124 -0
- package/dist/commands/add-dir.test.js +126 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +355 -0
- package/dist/commands/ask-question.js +320 -0
- package/dist/commands/ask-question.test.js +92 -0
- package/dist/commands/btw.js +121 -0
- package/dist/commands/cli-commands-group-a.test.js +728 -0
- package/dist/commands/cli-commands-group-b.test.js +695 -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/discord-commands-group-a.test.js +655 -0
- package/dist/commands/discord-commands-group-b.test.js +595 -0
- package/dist/commands/discord-commands-group-c.test.js +739 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork-subagent.js +177 -0
- package/dist/commands/fork.js +262 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +893 -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 +162 -0
- package/dist/commands/model-variant.js +369 -0
- package/dist/commands/model.js +798 -0
- package/dist/commands/new-worktree.js +465 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +223 -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/thread-deletion-sync.js +50 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +139 -0
- package/dist/commands/upgrade.js +48 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/vscode.js +269 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +468 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +100 -255
- package/dist/context-awareness-plugin.js +340 -0
- package/dist/context-awareness-plugin.test.js +126 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1355 -0
- package/dist/db.js +260 -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 +1124 -0
- package/dist/discord-command-registration.js +567 -0
- package/dist/discord-urls.js +82 -0
- package/dist/discord-utils.js +616 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +179 -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 +491 -0
- package/dist/format-tables.test.js +478 -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 +485 -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 +58 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +254 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +224 -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 +251 -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 +420 -0
- package/dist/ipc-polling.js +327 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/ipc-utils.js +18 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +171 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +264 -0
- package/dist/memory-overview-plugin.js +128 -0
- package/dist/message-finish-field.e2e.test.js +168 -0
- package/dist/message-formatting.js +415 -0
- package/dist/message-formatting.test.js +115 -0
- package/dist/message-preprocessing.js +359 -0
- package/dist/onboarding-tutorial.js +163 -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 +131 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +388 -0
- package/dist/opencode-interrupt-plugin.test.js +463 -0
- package/dist/opencode.js +1124 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/otto-digital-twin.e2e.test.js +161 -0
- package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
- package/dist/otto-opencode-plugin.js +21 -0
- package/dist/otto-opencode-plugin.test.js +98 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +68 -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 +790 -0
- package/dist/queue-advanced-footer.e2e.test.js +481 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -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 +256 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +514 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +475 -0
- package/dist/session-handler/event-stream-state.test.js +632 -0
- package/dist/session-handler/model-utils.js +147 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +131 -0
- package/dist/session-handler/thread-session-runtime.js +3390 -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 +92 -0
- package/dist/skill-filter.js +31 -0
- package/dist/skill-filter.test.js +65 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +19 -0
- package/dist/subagent-rate-limit-plugin.js +175 -0
- package/dist/system-message.js +702 -0
- package/dist/system-message.test.js +697 -0
- package/dist/task-runner.js +530 -0
- package/dist/task-schedule.js +213 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +313 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +1111 -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 +156 -0
- package/dist/utils.js +172 -0
- package/dist/utils.test.js +130 -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 +456 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +171 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +311 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +991 -0
- package/dist/worktrees.test.js +415 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +90 -38
- package/schema.prisma +303 -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/goke/SKILL.md +38 -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/manual-kimaki-upstream-adapt/SKILL.md +114 -0
- package/skills/new-skill/SKILL.md +237 -0
- package/skills/npm-package/SKILL.md +617 -0
- package/skills/opensrc/SKILL.md +78 -0
- package/skills/otto-publish/SKILL.md +61 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/profano/SKILL.md +16 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/sigillo/SKILL.md +101 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +28 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +98 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +49 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +979 -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-account-identity.test.ts +52 -0
- package/src/anthropic-account-identity.ts +77 -0
- package/src/anthropic-auth-plugin.ts +1139 -0
- package/src/anthropic-auth-state.test.ts +187 -0
- package/src/anthropic-auth-state.ts +386 -0
- package/src/bin.ts +182 -0
- package/src/btw-prefix-detection.test.ts +73 -0
- package/src/btw-prefix-detection.ts +23 -0
- package/src/channel-management.ts +376 -0
- package/src/cli-parsing.test.ts +197 -0
- package/src/cli-send-thread.e2e.test.ts +463 -0
- package/src/cli-telegram-options.test.ts +114 -0
- package/src/cli.ts +5718 -580
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-dir.test.ts +154 -0
- package/src/commands/add-dir.ts +175 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +496 -0
- package/src/commands/ask-question.test.ts +111 -0
- package/src/commands/ask-question.ts +455 -0
- package/src/commands/btw.ts +184 -0
- package/src/commands/cli-commands-group-a.test.ts +837 -0
- package/src/commands/cli-commands-group-b.test.ts +800 -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/discord-commands-group-a.test.ts +789 -0
- package/src/commands/discord-commands-group-b.test.ts +648 -0
- package/src/commands/discord-commands-group-c.test.ts +882 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork-subagent.ts +263 -0
- package/src/commands/fork.ts +386 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1181 -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 +226 -0
- package/src/commands/model-variant.ts +488 -0
- package/src/commands/model.ts +1082 -0
- package/src/commands/new-worktree.ts +645 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +293 -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/thread-deletion-sync.ts +80 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +174 -0
- package/src/commands/upgrade.ts +59 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/vscode.ts +342 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +645 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +103 -339
- package/src/context-awareness-plugin.test.ts +144 -0
- package/src/context-awareness-plugin.ts +469 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1949 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +295 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1507 -0
- package/src/discord-command-registration.ts +752 -0
- package/src/discord-urls.ts +89 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +846 -0
- package/src/errors.ts +232 -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 +515 -0
- package/src/format-tables.ts +718 -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 +644 -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 +770 -0
- package/src/generated/enums.ts +98 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2394 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1700 -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 +299 -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 +610 -0
- package/src/ipc-polling.ts +427 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/ipc-utils.ts +29 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +215 -0
- package/src/markdown.test.ts +315 -0
- package/src/markdown.ts +410 -0
- package/src/memory-overview-plugin.ts +163 -0
- package/src/message-finish-field.e2e.test.ts +195 -0
- package/src/message-formatting.test.ts +126 -0
- package/src/message-formatting.ts +535 -0
- package/src/message-preprocessing.ts +488 -0
- package/src/onboarding-tutorial.ts +167 -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 +191 -0
- package/src/opencode-interrupt-plugin.test.ts +682 -0
- package/src/opencode-interrupt-plugin.ts +507 -0
- package/src/opencode.ts +1462 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/otto-digital-twin.e2e.test.ts +199 -0
- package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
- package/src/otto-opencode-plugin.test.ts +108 -0
- package/src/otto-opencode-plugin.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 +84 -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 +877 -0
- package/src/queue-advanced-footer.e2e.test.ts +591 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -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 +327 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +651 -0
- package/src/schema.sql +174 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +99 -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 +717 -0
- package/src/session-handler/event-stream-state.ts +706 -0
- package/src/session-handler/model-utils.ts +217 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +247 -0
- package/src/session-handler/thread-session-runtime.ts +4440 -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 +130 -0
- package/src/skill-filter.test.ts +83 -0
- package/src/skill-filter.ts +42 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +139 -0
- package/src/subagent-rate-limit-plugin.ts +218 -0
- package/src/system-message.test.ts +710 -0
- package/src/system-message.ts +814 -0
- package/src/task-runner.ts +725 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +317 -0
- package/src/test-utils.ts +451 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1350 -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 +185 -0
- package/src/utils.test.ts +155 -0
- package/src/utils.ts +265 -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 +638 -0
- package/src/wait-session.ts +273 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +396 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +489 -0
- package/src/worktrees.ts +1370 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
- package/README.md +0 -142
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -39
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -202
- package/dist/config.test.js.map +0 -1
- package/dist/detect.d.ts +0 -9
- package/dist/detect.d.ts.map +0 -1
- package/dist/detect.js +0 -40
- package/dist/detect.js.map +0 -1
- package/dist/detect.test.d.ts +0 -2
- package/dist/detect.test.d.ts.map +0 -1
- package/dist/detect.test.js +0 -26
- package/dist/detect.test.js.map +0 -1
- package/dist/docker.d.ts +0 -7
- package/dist/docker.d.ts.map +0 -1
- package/dist/docker.js +0 -17
- package/dist/docker.js.map +0 -1
- package/dist/docker.test.d.ts +0 -2
- package/dist/docker.test.d.ts.map +0 -1
- package/dist/docker.test.js +0 -12
- package/dist/docker.test.js.map +0 -1
- package/dist/health.d.ts +0 -31
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -117
- package/dist/health.js.map +0 -1
- package/dist/health.test.d.ts +0 -2
- package/dist/health.test.d.ts.map +0 -1
- package/dist/health.test.js +0 -52
- package/dist/health.test.js.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/installer.d.ts +0 -10
- package/dist/installer.d.ts.map +0 -1
- package/dist/installer.js +0 -50
- package/dist/installer.js.map +0 -1
- package/dist/installer.test.d.ts +0 -2
- package/dist/installer.test.d.ts.map +0 -1
- package/dist/installer.test.js +0 -43
- package/dist/installer.test.js.map +0 -1
- package/dist/lifecycle.d.ts +0 -10
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js +0 -45
- package/dist/lifecycle.js.map +0 -1
- package/dist/lifecycle.test.d.ts +0 -2
- package/dist/lifecycle.test.d.ts.map +0 -1
- package/dist/lifecycle.test.js +0 -20
- package/dist/lifecycle.test.js.map +0 -1
- package/dist/manifest.d.ts +0 -18
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -30
- package/dist/manifest.js.map +0 -1
- package/dist/skills-baseline.d.ts +0 -7
- package/dist/skills-baseline.d.ts.map +0 -1
- package/dist/skills-baseline.js +0 -9
- package/dist/skills-baseline.js.map +0 -1
- package/dist/skills.d.ts +0 -110
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js +0 -429
- package/dist/skills.js.map +0 -1
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -416
- package/dist/skills.test.js.map +0 -1
- package/dist/sync.d.ts +0 -10
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -39
- package/dist/sync.js.map +0 -1
- package/dist/tenant.d.ts +0 -13
- package/dist/tenant.d.ts.map +0 -1
- package/dist/tenant.js +0 -105
- package/dist/tenant.js.map +0 -1
- package/dist/tenant.test.d.ts +0 -2
- package/dist/tenant.test.d.ts.map +0 -1
- package/dist/tenant.test.js +0 -37
- package/dist/tenant.test.js.map +0 -1
- package/src/config.test.ts +0 -237
- package/src/detect.test.ts +0 -29
- package/src/detect.ts +0 -52
- package/src/docker.test.ts +0 -12
- package/src/docker.ts +0 -23
- package/src/health.test.ts +0 -61
- package/src/health.ts +0 -158
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -62
- package/src/installer.test.ts +0 -52
- package/src/installer.ts +0 -62
- package/src/lifecycle.test.ts +0 -23
- package/src/lifecycle.ts +0 -49
- package/src/manifest.ts +0 -42
- package/src/skills-baseline.ts +0 -14
- package/src/skills.test.ts +0 -503
- package/src/skills.ts +0 -512
- package/src/sync.ts +0 -53
- package/src/tenant.test.ts +0 -49
- package/src/tenant.ts +0 -120
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// /toggle-worktrees command.
|
|
2
|
+
// Allows per-channel opt-in for automatic worktree creation,
|
|
3
|
+
// as an alternative to the global --use-worktrees CLI flag.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ChatInputCommandInteraction,
|
|
7
|
+
MessageFlags,
|
|
8
|
+
ChannelType,
|
|
9
|
+
type TextChannel,
|
|
10
|
+
} from 'discord.js'
|
|
11
|
+
import {
|
|
12
|
+
getChannelWorktreesEnabled,
|
|
13
|
+
setChannelWorktreesEnabled,
|
|
14
|
+
} from '../database.js'
|
|
15
|
+
import { getOttoMetadata } from '../discord-utils.js'
|
|
16
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
17
|
+
|
|
18
|
+
const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handle the /toggle-worktrees slash command.
|
|
22
|
+
* Toggles automatic worktree creation for new sessions in this channel.
|
|
23
|
+
*/
|
|
24
|
+
export async function handleToggleWorktreesCommand({
|
|
25
|
+
command,
|
|
26
|
+
}: {
|
|
27
|
+
command: ChatInputCommandInteraction
|
|
28
|
+
appId: string
|
|
29
|
+
}): Promise<void> {
|
|
30
|
+
worktreeSettingsLogger.log('[TOGGLE_WORKTREES] Command called')
|
|
31
|
+
|
|
32
|
+
const channel = command.channel
|
|
33
|
+
|
|
34
|
+
if (!channel || channel.type !== ChannelType.GuildText) {
|
|
35
|
+
await command.reply({
|
|
36
|
+
content: 'This command can only be used in text channels (not threads).',
|
|
37
|
+
flags: MessageFlags.Ephemeral,
|
|
38
|
+
})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const textChannel = channel as TextChannel
|
|
43
|
+
const metadata = await getOttoMetadata(textChannel)
|
|
44
|
+
|
|
45
|
+
if (!metadata.projectDirectory) {
|
|
46
|
+
await command.reply({
|
|
47
|
+
content:
|
|
48
|
+
'This channel is not configured with a project directory.\nUse `/add-project` to set up this channel.',
|
|
49
|
+
flags: MessageFlags.Ephemeral,
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const wasEnabled = await getChannelWorktreesEnabled(textChannel.id)
|
|
55
|
+
const nextEnabled = !wasEnabled
|
|
56
|
+
await setChannelWorktreesEnabled(textChannel.id, nextEnabled)
|
|
57
|
+
|
|
58
|
+
const nextLabel = nextEnabled ? 'enabled' : 'disabled'
|
|
59
|
+
|
|
60
|
+
worktreeSettingsLogger.log(
|
|
61
|
+
`[TOGGLE_WORKTREES] ${nextLabel.toUpperCase()} for channel ${textChannel.id}`,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await command.reply({
|
|
65
|
+
content: nextEnabled
|
|
66
|
+
? `Worktrees **enabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will now automatically create git worktrees.\n\nNew setting for **#${textChannel.name}**: **enabled**.`
|
|
67
|
+
: `Worktrees **disabled** for this channel.\n\nNew sessions started from messages in **#${textChannel.name}** will use the main project directory.\n\nNew setting for **#${textChannel.name}**: **disabled**.`,
|
|
68
|
+
flags: MessageFlags.Ephemeral,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
// /worktrees command — list all git worktrees for the current channel's project.
|
|
2
|
+
// Uses `git worktree list --porcelain` as source of truth, enriched with
|
|
3
|
+
// DB metadata (thread link, created_at) when available. Shows otto-created,
|
|
4
|
+
// opencode-created, and manually created worktrees in a single table.
|
|
5
|
+
// Renders a markdown table that the CV2 pipeline auto-formats for Discord,
|
|
6
|
+
// including HTML-backed action buttons for deletable worktrees.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ButtonInteraction,
|
|
10
|
+
ChatInputCommandInteraction,
|
|
11
|
+
ChannelType,
|
|
12
|
+
ComponentType,
|
|
13
|
+
MessageFlags,
|
|
14
|
+
type TextChannel,
|
|
15
|
+
type ThreadChannel,
|
|
16
|
+
type APIMessageTopLevelComponent,
|
|
17
|
+
type APITextDisplayComponent,
|
|
18
|
+
type InteractionEditReplyOptions,
|
|
19
|
+
} from 'discord.js'
|
|
20
|
+
import {
|
|
21
|
+
deleteThreadWorktree,
|
|
22
|
+
type ThreadWorktree,
|
|
23
|
+
} from '../database.js'
|
|
24
|
+
import { getPrisma } from '../db.js'
|
|
25
|
+
import { splitTablesFromMarkdown } from '../format-tables.js'
|
|
26
|
+
import {
|
|
27
|
+
buildHtmlActionCustomId,
|
|
28
|
+
cancelHtmlActionsForOwner,
|
|
29
|
+
registerHtmlAction,
|
|
30
|
+
} from '../html-actions.js'
|
|
31
|
+
import * as errore from 'errore'
|
|
32
|
+
import crypto from 'node:crypto'
|
|
33
|
+
import { GitCommandError } from '../errors.js'
|
|
34
|
+
import { resolveWorkingDirectory } from '../discord-utils.js'
|
|
35
|
+
import {
|
|
36
|
+
deleteWorktree,
|
|
37
|
+
git,
|
|
38
|
+
getDefaultBranch,
|
|
39
|
+
listGitWorktrees,
|
|
40
|
+
type GitWorktree,
|
|
41
|
+
} from '../worktrees.js'
|
|
42
|
+
import path from 'node:path'
|
|
43
|
+
|
|
44
|
+
// Extracts the git stderr from a deleteWorktree error via errore.findCause.
|
|
45
|
+
// Chain: Error { cause: GitCommandError { cause: CommandError { stderr } } }.
|
|
46
|
+
export function extractGitStderr(error: Error): string | undefined {
|
|
47
|
+
const gitErr = errore.findCause(error, GitCommandError)
|
|
48
|
+
const stderr = (gitErr?.cause as { stderr?: string } | undefined)?.stderr?.trim()
|
|
49
|
+
if (stderr && stderr.length > 0) {
|
|
50
|
+
return stderr
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatTimeAgo(date: Date): string {
|
|
56
|
+
const diffMs = Date.now() - date.getTime()
|
|
57
|
+
if (diffMs < 0) {
|
|
58
|
+
return 'just now'
|
|
59
|
+
}
|
|
60
|
+
const totalSeconds = Math.floor(diffMs / 1000)
|
|
61
|
+
if (totalSeconds < 60) {
|
|
62
|
+
return `${totalSeconds}s ago`
|
|
63
|
+
}
|
|
64
|
+
const totalMinutes = Math.floor(totalSeconds / 60)
|
|
65
|
+
if (totalMinutes < 60) {
|
|
66
|
+
return `${totalMinutes}m ago`
|
|
67
|
+
}
|
|
68
|
+
const hours = Math.floor(totalMinutes / 60)
|
|
69
|
+
const minutes = totalMinutes % 60
|
|
70
|
+
if (hours < 24) {
|
|
71
|
+
return minutes > 0 ? `${hours}h ${minutes}m ago` : `${hours}h ago`
|
|
72
|
+
}
|
|
73
|
+
const days = Math.floor(hours / 24)
|
|
74
|
+
const remainingHours = hours % 24
|
|
75
|
+
return remainingHours > 0 ? `${days}d ${remainingHours}h ago` : `${days}d ago`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Stable button ID derived from directory path via sha1 hash.
|
|
79
|
+
// Avoids collisions that truncated path suffixes can cause.
|
|
80
|
+
function worktreeButtonKey(directory: string): string {
|
|
81
|
+
return crypto.createHash('sha1').update(directory).digest('hex').slice(0, 12)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Unified worktree row that merges git data with optional DB metadata.
|
|
85
|
+
type WorktreeRow = {
|
|
86
|
+
directory: string
|
|
87
|
+
branch: string | null
|
|
88
|
+
name: string
|
|
89
|
+
threadId: string | null
|
|
90
|
+
guildId: string | null
|
|
91
|
+
createdAt: Date | null
|
|
92
|
+
source: 'otto' | 'opencode' | 'manual'
|
|
93
|
+
// DB-only worktrees (pending/error) won't appear in git list
|
|
94
|
+
dbStatus: 'ready' | 'pending' | 'error'
|
|
95
|
+
// Git-level flags that block deletion
|
|
96
|
+
locked: boolean
|
|
97
|
+
prunable: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type WorktreeGitStatus = {
|
|
101
|
+
dirty: boolean
|
|
102
|
+
aheadCount: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type WorktreesReplyTarget = {
|
|
106
|
+
guildId: string
|
|
107
|
+
userId: string
|
|
108
|
+
channelId: string
|
|
109
|
+
projectDirectory: string
|
|
110
|
+
notice?: string
|
|
111
|
+
editReply: (
|
|
112
|
+
options: string | InteractionEditReplyOptions,
|
|
113
|
+
) => Promise<unknown>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 5s timeout per git call — prevents hangs from deleted dirs, git locks, slow disks.
|
|
117
|
+
// Returns null on timeout/error so the table shows "unknown" for that worktree.
|
|
118
|
+
const GIT_CMD_TIMEOUT = 5_000
|
|
119
|
+
const GLOBAL_TIMEOUT = 10_000
|
|
120
|
+
|
|
121
|
+
// Detect worktree source from branch name and directory path.
|
|
122
|
+
// opencode/otto-* and legacy opencode/otto-* branches → otto,
|
|
123
|
+
// opencode worktree paths → opencode, else manual.
|
|
124
|
+
function detectWorktreeSource({
|
|
125
|
+
branch,
|
|
126
|
+
directory,
|
|
127
|
+
}: {
|
|
128
|
+
branch: string | null
|
|
129
|
+
directory: string
|
|
130
|
+
}): 'otto' | 'opencode' | 'manual' {
|
|
131
|
+
if (branch?.startsWith('opencode/otto-') || branch?.startsWith('opencode/otto-')) {
|
|
132
|
+
return 'otto'
|
|
133
|
+
}
|
|
134
|
+
// opencode stores worktrees under ~/.local/share/opencode/worktree/
|
|
135
|
+
if (directory.includes('/opencode/worktree/')) {
|
|
136
|
+
return 'opencode'
|
|
137
|
+
}
|
|
138
|
+
return 'manual'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Checks dirty state and commits ahead of default branch in parallel.
|
|
142
|
+
// Returns null when the directory is missing / git commands fail / timeout.
|
|
143
|
+
async function getWorktreeGitStatus({
|
|
144
|
+
directory,
|
|
145
|
+
defaultBranch,
|
|
146
|
+
}: {
|
|
147
|
+
directory: string
|
|
148
|
+
defaultBranch: string
|
|
149
|
+
}): Promise<WorktreeGitStatus | null> {
|
|
150
|
+
try {
|
|
151
|
+
// Use raw git calls so errors/timeouts are visible — isDirty() swallows
|
|
152
|
+
// errors and returns false, which would render "merged" instead of "unknown".
|
|
153
|
+
const [statusResult, aheadResult] = await Promise.all([
|
|
154
|
+
git(directory, 'status --porcelain', { timeout: GIT_CMD_TIMEOUT }),
|
|
155
|
+
git(directory, `rev-list --count "${defaultBranch}..HEAD"`, {
|
|
156
|
+
timeout: GIT_CMD_TIMEOUT,
|
|
157
|
+
}),
|
|
158
|
+
])
|
|
159
|
+
if (statusResult instanceof Error || aheadResult instanceof Error) {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
const aheadCount = parseInt(aheadResult, 10)
|
|
163
|
+
if (!Number.isFinite(aheadCount)) {
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
return { dirty: statusResult.length > 0, aheadCount }
|
|
167
|
+
} catch {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildWorktreeTable({
|
|
173
|
+
rows,
|
|
174
|
+
gitStatuses,
|
|
175
|
+
guildId,
|
|
176
|
+
}: {
|
|
177
|
+
rows: WorktreeRow[]
|
|
178
|
+
gitStatuses: (WorktreeGitStatus | null)[]
|
|
179
|
+
guildId: string
|
|
180
|
+
}): string {
|
|
181
|
+
const header = '| Source | Name | Status | Created | Folder | Action |'
|
|
182
|
+
const separator = '|---|---|---|---|---|---|'
|
|
183
|
+
const tableRows = rows.map((row, i) => {
|
|
184
|
+
const sourceCell = (() => {
|
|
185
|
+
if (row.threadId && row.guildId) {
|
|
186
|
+
const threadLink = `[${row.source}](https://discord.com/channels/${row.guildId}/${row.threadId})`
|
|
187
|
+
return threadLink
|
|
188
|
+
}
|
|
189
|
+
return row.source
|
|
190
|
+
})()
|
|
191
|
+
const name = row.name
|
|
192
|
+
const gs = gitStatuses[i] ?? null
|
|
193
|
+
const status = (() => {
|
|
194
|
+
if (row.dbStatus !== 'ready') {
|
|
195
|
+
return row.dbStatus
|
|
196
|
+
}
|
|
197
|
+
if (row.locked) {
|
|
198
|
+
return 'locked'
|
|
199
|
+
}
|
|
200
|
+
if (row.prunable) {
|
|
201
|
+
return 'prunable'
|
|
202
|
+
}
|
|
203
|
+
if (!gs) {
|
|
204
|
+
return 'unknown'
|
|
205
|
+
}
|
|
206
|
+
const parts: string[] = []
|
|
207
|
+
if (gs.dirty) {
|
|
208
|
+
parts.push('dirty')
|
|
209
|
+
}
|
|
210
|
+
if (gs.aheadCount > 0) {
|
|
211
|
+
parts.push(`${gs.aheadCount} ahead`)
|
|
212
|
+
} else {
|
|
213
|
+
parts.push('merged')
|
|
214
|
+
}
|
|
215
|
+
return parts.join(', ')
|
|
216
|
+
})()
|
|
217
|
+
const created = row.createdAt ? formatTimeAgo(row.createdAt) : '-'
|
|
218
|
+
const folder = row.directory
|
|
219
|
+
const action = buildActionCell({ row, gitStatus: gs })
|
|
220
|
+
return `| ${sourceCell} | ${name} | ${status} | ${created} | ${folder} | ${action} |`
|
|
221
|
+
})
|
|
222
|
+
return [header, separator, ...tableRows].join('\n')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildActionCell({
|
|
226
|
+
row,
|
|
227
|
+
gitStatus,
|
|
228
|
+
}: {
|
|
229
|
+
row: WorktreeRow
|
|
230
|
+
gitStatus: WorktreeGitStatus | null
|
|
231
|
+
}): string {
|
|
232
|
+
if (!canDeleteWorktree({ row, gitStatus })) {
|
|
233
|
+
return '-'
|
|
234
|
+
}
|
|
235
|
+
return buildDeleteButtonHtml({
|
|
236
|
+
buttonId: `del-wt-${worktreeButtonKey(row.directory)}`,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildDeleteButtonHtml({
|
|
241
|
+
buttonId,
|
|
242
|
+
}: {
|
|
243
|
+
buttonId: string
|
|
244
|
+
}): string {
|
|
245
|
+
return `<button id="${buttonId}" variant="secondary">Delete</button>`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function canDeleteWorktree({
|
|
249
|
+
row,
|
|
250
|
+
gitStatus,
|
|
251
|
+
}: {
|
|
252
|
+
row: WorktreeRow
|
|
253
|
+
gitStatus: WorktreeGitStatus | null
|
|
254
|
+
}): boolean {
|
|
255
|
+
if (row.dbStatus !== 'ready') {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
if (row.locked) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
if (!gitStatus) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
if (gitStatus.dirty) {
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
return gitStatus.aheadCount === 0
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Resolves git statuses for all worktrees within a single global deadline.
|
|
271
|
+
async function resolveGitStatuses({
|
|
272
|
+
rows,
|
|
273
|
+
projectDirectory,
|
|
274
|
+
timeout,
|
|
275
|
+
}: {
|
|
276
|
+
rows: WorktreeRow[]
|
|
277
|
+
projectDirectory: string
|
|
278
|
+
timeout: number
|
|
279
|
+
}): Promise<(WorktreeGitStatus | null)[]> {
|
|
280
|
+
const nullFallback = rows.map(() => null)
|
|
281
|
+
|
|
282
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
283
|
+
const deadline = new Promise<(WorktreeGitStatus | null)[]>((resolve) => {
|
|
284
|
+
timer = setTimeout(() => {
|
|
285
|
+
resolve(nullFallback)
|
|
286
|
+
}, timeout)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
const work = (async () => {
|
|
290
|
+
const defaultBranch = await getDefaultBranch(projectDirectory, {
|
|
291
|
+
timeout: GIT_CMD_TIMEOUT,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return Promise.all(
|
|
295
|
+
rows.map((row) => {
|
|
296
|
+
if (row.dbStatus !== 'ready' || row.locked || row.prunable) {
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
return getWorktreeGitStatus({ directory: row.directory, defaultBranch })
|
|
300
|
+
}),
|
|
301
|
+
)
|
|
302
|
+
})()
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
return await Promise.race([work, deadline])
|
|
306
|
+
} finally {
|
|
307
|
+
clearTimeout(timer)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Merge git worktrees with DB metadata into unified WorktreeRows.
|
|
312
|
+
// Git is the source of truth for what exists on disk. DB rows that aren't
|
|
313
|
+
// in the git list (pending/error) are appended at the end.
|
|
314
|
+
async function buildWorktreeRows({
|
|
315
|
+
projectDirectory,
|
|
316
|
+
gitWorktrees,
|
|
317
|
+
}: {
|
|
318
|
+
projectDirectory: string
|
|
319
|
+
gitWorktrees: GitWorktree[]
|
|
320
|
+
}): Promise<WorktreeRow[]> {
|
|
321
|
+
const prisma = await getPrisma()
|
|
322
|
+
const dbWorktrees = await prisma.thread_worktrees.findMany({
|
|
323
|
+
where: { project_directory: projectDirectory },
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Index DB worktrees by directory for fast lookup
|
|
327
|
+
const dbByDirectory = new Map<string, ThreadWorktree>()
|
|
328
|
+
for (const dbWt of dbWorktrees) {
|
|
329
|
+
if (dbWt.worktree_directory) {
|
|
330
|
+
dbByDirectory.set(dbWt.worktree_directory, dbWt)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Track which DB rows got matched so we can append unmatched ones
|
|
335
|
+
const matchedDbThreadIds = new Set<string>()
|
|
336
|
+
|
|
337
|
+
// Build rows from git worktrees (the source of truth for on-disk state).
|
|
338
|
+
// Use real DB status when available — a git-visible worktree whose DB row
|
|
339
|
+
// is still 'pending' means setup hasn't finished (race window).
|
|
340
|
+
const gitRows: WorktreeRow[] = gitWorktrees.map((gw) => {
|
|
341
|
+
const dbMatch = dbByDirectory.get(gw.directory)
|
|
342
|
+
if (dbMatch) {
|
|
343
|
+
matchedDbThreadIds.add(dbMatch.thread_id)
|
|
344
|
+
}
|
|
345
|
+
const source = detectWorktreeSource({
|
|
346
|
+
branch: gw.branch,
|
|
347
|
+
directory: gw.directory,
|
|
348
|
+
})
|
|
349
|
+
const name = gw.branch ?? path.basename(gw.directory)
|
|
350
|
+
const dbStatus: 'ready' | 'pending' | 'error' = (() => {
|
|
351
|
+
if (!dbMatch) {
|
|
352
|
+
return 'ready'
|
|
353
|
+
}
|
|
354
|
+
if (dbMatch.status === 'error') {
|
|
355
|
+
return 'error'
|
|
356
|
+
}
|
|
357
|
+
if (dbMatch.status === 'pending') {
|
|
358
|
+
return 'pending'
|
|
359
|
+
}
|
|
360
|
+
return 'ready'
|
|
361
|
+
})()
|
|
362
|
+
return {
|
|
363
|
+
directory: gw.directory,
|
|
364
|
+
branch: gw.branch,
|
|
365
|
+
name,
|
|
366
|
+
threadId: dbMatch?.thread_id ?? null,
|
|
367
|
+
guildId: null, // filled in by caller
|
|
368
|
+
createdAt: dbMatch?.created_at ?? null,
|
|
369
|
+
source,
|
|
370
|
+
dbStatus,
|
|
371
|
+
locked: gw.locked,
|
|
372
|
+
prunable: gw.prunable,
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// Append DB-only worktrees (pending/error/stale — not visible to git).
|
|
377
|
+
// Preserve actual DB status so stale 'ready' rows show as 'ready' (missing).
|
|
378
|
+
const dbOnlyRows: WorktreeRow[] = dbWorktrees
|
|
379
|
+
.filter((dbWt) => {
|
|
380
|
+
return !matchedDbThreadIds.has(dbWt.thread_id)
|
|
381
|
+
})
|
|
382
|
+
.map((dbWt) => {
|
|
383
|
+
const dbStatus: 'ready' | 'pending' | 'error' = (() => {
|
|
384
|
+
if (dbWt.status === 'error') {
|
|
385
|
+
return 'error'
|
|
386
|
+
}
|
|
387
|
+
if (dbWt.status === 'pending') {
|
|
388
|
+
return 'pending'
|
|
389
|
+
}
|
|
390
|
+
return 'ready'
|
|
391
|
+
})()
|
|
392
|
+
return {
|
|
393
|
+
directory: dbWt.worktree_directory ?? dbWt.project_directory,
|
|
394
|
+
branch: null,
|
|
395
|
+
name: dbWt.worktree_name,
|
|
396
|
+
threadId: dbWt.thread_id,
|
|
397
|
+
guildId: null,
|
|
398
|
+
createdAt: dbWt.created_at,
|
|
399
|
+
source: 'otto' as const,
|
|
400
|
+
dbStatus,
|
|
401
|
+
locked: false,
|
|
402
|
+
prunable: false,
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
return [...gitRows, ...dbOnlyRows]
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function getWorktreesActionOwnerKey({
|
|
410
|
+
userId,
|
|
411
|
+
channelId,
|
|
412
|
+
}: {
|
|
413
|
+
userId: string
|
|
414
|
+
channelId: string
|
|
415
|
+
}): string {
|
|
416
|
+
return `worktrees:${userId}:${channelId}`
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isProjectChannel(
|
|
420
|
+
channel: ChatInputCommandInteraction['channel'] | ButtonInteraction['channel'],
|
|
421
|
+
): boolean {
|
|
422
|
+
if (!channel) {
|
|
423
|
+
return false
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return [
|
|
427
|
+
ChannelType.GuildText,
|
|
428
|
+
ChannelType.PublicThread,
|
|
429
|
+
ChannelType.PrivateThread,
|
|
430
|
+
ChannelType.AnnouncementThread,
|
|
431
|
+
].includes(channel.type)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function renderWorktreesReply({
|
|
435
|
+
guildId,
|
|
436
|
+
userId,
|
|
437
|
+
channelId,
|
|
438
|
+
projectDirectory,
|
|
439
|
+
notice,
|
|
440
|
+
editReply,
|
|
441
|
+
}: WorktreesReplyTarget): Promise<void> {
|
|
442
|
+
const ownerKey = getWorktreesActionOwnerKey({ userId, channelId })
|
|
443
|
+
cancelHtmlActionsForOwner(ownerKey)
|
|
444
|
+
|
|
445
|
+
const gitWorktrees = await listGitWorktrees({
|
|
446
|
+
projectDirectory,
|
|
447
|
+
timeout: GIT_CMD_TIMEOUT,
|
|
448
|
+
})
|
|
449
|
+
// On git failure, fall back to empty list (DB-only rows still shown)
|
|
450
|
+
const gitList = gitWorktrees instanceof Error ? [] : gitWorktrees
|
|
451
|
+
|
|
452
|
+
const rows = await buildWorktreeRows({ projectDirectory, gitWorktrees: gitList })
|
|
453
|
+
// Inject guildId into all rows for thread link rendering
|
|
454
|
+
for (const row of rows) {
|
|
455
|
+
row.guildId = guildId
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (rows.length === 0) {
|
|
459
|
+
const message = notice
|
|
460
|
+
? `${notice}\n\nNo worktrees found.`
|
|
461
|
+
: 'No worktrees found.'
|
|
462
|
+
const textDisplay: APITextDisplayComponent = {
|
|
463
|
+
type: ComponentType.TextDisplay,
|
|
464
|
+
content: message,
|
|
465
|
+
}
|
|
466
|
+
await editReply({
|
|
467
|
+
components: [textDisplay],
|
|
468
|
+
flags: MessageFlags.IsComponentsV2,
|
|
469
|
+
})
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const gitStatuses = await resolveGitStatuses({
|
|
474
|
+
rows,
|
|
475
|
+
projectDirectory,
|
|
476
|
+
timeout: GLOBAL_TIMEOUT,
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
// Map deletable worktrees by button ID for the HTML action resolver.
|
|
480
|
+
// Uses the same worktreeButtonKey() as buildActionCell.
|
|
481
|
+
const deletableRowsByButtonId = new Map<string, WorktreeRow>()
|
|
482
|
+
rows.forEach((row, index) => {
|
|
483
|
+
const gitStatus = gitStatuses[index] ?? null
|
|
484
|
+
if (!canDeleteWorktree({ row, gitStatus })) {
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
deletableRowsByButtonId.set(`del-wt-${worktreeButtonKey(row.directory)}`, row)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const tableMarkdown = buildWorktreeTable({
|
|
491
|
+
rows,
|
|
492
|
+
gitStatuses,
|
|
493
|
+
guildId,
|
|
494
|
+
})
|
|
495
|
+
const markdown = notice ? `${notice}\n\n${tableMarkdown}` : tableMarkdown
|
|
496
|
+
const segments = splitTablesFromMarkdown(markdown, {
|
|
497
|
+
resolveButtonCustomId: ({ button }) => {
|
|
498
|
+
const row = deletableRowsByButtonId.get(button.id)
|
|
499
|
+
if (!row) {
|
|
500
|
+
return new Error(`No worktree registered for button ${button.id}`)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const actionId = registerHtmlAction({
|
|
504
|
+
ownerKey,
|
|
505
|
+
threadId: row.threadId ?? row.directory,
|
|
506
|
+
run: async ({ interaction }) => {
|
|
507
|
+
await handleDeleteWorktreeAction({
|
|
508
|
+
interaction,
|
|
509
|
+
row,
|
|
510
|
+
projectDirectory,
|
|
511
|
+
})
|
|
512
|
+
},
|
|
513
|
+
})
|
|
514
|
+
return buildHtmlActionCustomId(actionId)
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
const components: APIMessageTopLevelComponent[] = segments.flatMap((segment) => {
|
|
519
|
+
if (segment.type === 'components') {
|
|
520
|
+
return segment.components
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const textDisplay: APITextDisplayComponent = {
|
|
524
|
+
type: ComponentType.TextDisplay,
|
|
525
|
+
content: segment.text,
|
|
526
|
+
}
|
|
527
|
+
return [textDisplay]
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
await editReply({
|
|
531
|
+
components,
|
|
532
|
+
flags: MessageFlags.IsComponentsV2,
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function handleDeleteWorktreeAction({
|
|
537
|
+
interaction,
|
|
538
|
+
row,
|
|
539
|
+
projectDirectory,
|
|
540
|
+
}: {
|
|
541
|
+
interaction: ButtonInteraction
|
|
542
|
+
row: WorktreeRow
|
|
543
|
+
projectDirectory: string
|
|
544
|
+
}): Promise<void> {
|
|
545
|
+
const guildId = interaction.guildId
|
|
546
|
+
if (!guildId) {
|
|
547
|
+
await interaction.editReply({
|
|
548
|
+
components: [
|
|
549
|
+
{
|
|
550
|
+
type: ComponentType.TextDisplay,
|
|
551
|
+
content: 'This action can only be used in a server.',
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
flags: MessageFlags.IsComponentsV2,
|
|
555
|
+
})
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Pass branch name for branch cleanup. Empty string for detached HEAD
|
|
560
|
+
// worktrees so deleteWorktree skips the `git branch -d` step.
|
|
561
|
+
const displayName = row.branch ?? row.name
|
|
562
|
+
const deleteResult = await deleteWorktree({
|
|
563
|
+
projectDirectory,
|
|
564
|
+
worktreeDirectory: row.directory,
|
|
565
|
+
worktreeName: row.branch ?? '',
|
|
566
|
+
})
|
|
567
|
+
if (deleteResult instanceof Error) {
|
|
568
|
+
const gitStderr = extractGitStderr(deleteResult)
|
|
569
|
+
const detail = gitStderr
|
|
570
|
+
? `\`\`\`\n${gitStderr}\n\`\`\``
|
|
571
|
+
: deleteResult.message
|
|
572
|
+
await interaction
|
|
573
|
+
.followUp({
|
|
574
|
+
content: `Failed to delete \`${displayName}\`\n${detail}`,
|
|
575
|
+
flags: MessageFlags.Ephemeral,
|
|
576
|
+
})
|
|
577
|
+
.catch(() => {
|
|
578
|
+
return undefined
|
|
579
|
+
})
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Clean up DB row if this was an otto-tracked worktree
|
|
584
|
+
if (row.threadId) {
|
|
585
|
+
await deleteThreadWorktree(row.threadId)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
await renderWorktreesReply({
|
|
589
|
+
guildId,
|
|
590
|
+
userId: interaction.user.id,
|
|
591
|
+
channelId: interaction.channelId,
|
|
592
|
+
projectDirectory,
|
|
593
|
+
notice: `Deleted \`${displayName}\`.`,
|
|
594
|
+
editReply: (options) => {
|
|
595
|
+
return interaction.editReply(options)
|
|
596
|
+
},
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export async function handleWorktreesCommand({
|
|
601
|
+
command,
|
|
602
|
+
}: {
|
|
603
|
+
command: ChatInputCommandInteraction
|
|
604
|
+
appId: string
|
|
605
|
+
}): Promise<void> {
|
|
606
|
+
const channel = command.channel
|
|
607
|
+
const guildId = command.guildId
|
|
608
|
+
if (!guildId || !channel) {
|
|
609
|
+
await command.reply({
|
|
610
|
+
content: 'This command can only be used in a server channel.',
|
|
611
|
+
flags: MessageFlags.Ephemeral,
|
|
612
|
+
})
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!isProjectChannel(channel)) {
|
|
617
|
+
await command.reply({
|
|
618
|
+
content: 'This command can only be used in a project channel or thread.',
|
|
619
|
+
flags: MessageFlags.Ephemeral,
|
|
620
|
+
})
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const resolved = await resolveWorkingDirectory({
|
|
625
|
+
channel: channel as TextChannel | ThreadChannel,
|
|
626
|
+
})
|
|
627
|
+
if (!resolved) {
|
|
628
|
+
await command.reply({
|
|
629
|
+
content: 'Could not determine the project folder for this channel.',
|
|
630
|
+
flags: MessageFlags.Ephemeral,
|
|
631
|
+
})
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
await command.deferReply({ flags: MessageFlags.Ephemeral })
|
|
636
|
+
await renderWorktreesReply({
|
|
637
|
+
guildId,
|
|
638
|
+
userId: command.user.id,
|
|
639
|
+
channelId: command.channelId,
|
|
640
|
+
projectDirectory: resolved.projectDirectory,
|
|
641
|
+
editReply: (options) => {
|
|
642
|
+
return command.editReply(options)
|
|
643
|
+
},
|
|
644
|
+
})
|
|
645
|
+
}
|