@otto-assistant/otto 0.1.1 → 0.7.15
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 +621 -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 +887 -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 +366 -0
- package/dist/commands/model.js +794 -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 +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 +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 +1117 -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 +751 -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 +1175 -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 +485 -0
- package/src/commands/model.ts +1078 -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 +1505 -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 +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 +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 +1453 -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/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
package/src/bin.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Crash-recovery supervisor for `otto gateway start`.
|
|
2
|
+
//
|
|
3
|
+
// Parents run goke-backed `cli.js` directly for every invocation except top-level
|
|
4
|
+
// `otto gateway start` (without `--help`). Those long-running installs get an
|
|
5
|
+
// outer Node process that restarts `cli.js` on non-clean exits (crash, OOM, etc.).
|
|
6
|
+
// Exit code `0` / `EXIT_NO_RESTART=64` / SIGTERM+SIGINT grace paths suppress restarts.
|
|
7
|
+
//
|
|
8
|
+
// When __OTTO_CHILD is set, we're supervised — import `cli.js` directly.
|
|
9
|
+
//
|
|
10
|
+
// V8 heap snapshot flags:
|
|
11
|
+
// Injects --heapsnapshot-near-heap-limit=3 and --diagnostic-dir so V8 writes
|
|
12
|
+
// heap snapshots internally as it approaches the heap limit. This catches OOM
|
|
13
|
+
// situations where SIGKILL (exit 137) would kill the process before our
|
|
14
|
+
// heap-monitor.ts polling can react. The polling monitor is kept as an early
|
|
15
|
+
// warning system at 85% usage; the V8 flag is the last-resort safety net.
|
|
16
|
+
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
18
|
+
import fs from 'node:fs'
|
|
19
|
+
import os from 'node:os'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
|
|
22
|
+
const HEAP_SNAPSHOT_DIR = path.join(os.homedir(), '.otto', 'heap-snapshots')
|
|
23
|
+
const STARTUP_LOG_WAIT_MS = 2_500
|
|
24
|
+
const STARTUP_LOG_POLL_MS = 200
|
|
25
|
+
const STARTUP_LOG_MAX_LINES = 12
|
|
26
|
+
|
|
27
|
+
const argv = process.argv.slice(2)
|
|
28
|
+
const isHelpFlag = process.argv.includes('--help')
|
|
29
|
+
const isGatewayDaemon = process.env.OTTO_GATEWAY_DAEMON === '1'
|
|
30
|
+
|
|
31
|
+
function isGatewayStartInvocation(): boolean {
|
|
32
|
+
return argv[0] === 'gateway' && argv[1] === 'start'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveDataDirFromArgv(): string {
|
|
36
|
+
const inlineArg = argv.find((entry) => {
|
|
37
|
+
return entry.startsWith('--data-dir=')
|
|
38
|
+
})
|
|
39
|
+
if (inlineArg) {
|
|
40
|
+
return path.resolve(inlineArg.slice('--data-dir='.length))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dataDirIndex = argv.findIndex((entry) => {
|
|
44
|
+
return entry === '--data-dir'
|
|
45
|
+
})
|
|
46
|
+
if (dataDirIndex !== -1) {
|
|
47
|
+
const value = argv[dataDirIndex + 1]
|
|
48
|
+
if (value && !value.startsWith('--')) {
|
|
49
|
+
return path.resolve(value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return path.join(os.homedir(), '.otto')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function printStartupLogs({
|
|
57
|
+
dataDir,
|
|
58
|
+
}: {
|
|
59
|
+
dataDir: string
|
|
60
|
+
}): Promise<void> {
|
|
61
|
+
const logPath = path.join(dataDir, 'otto.log')
|
|
62
|
+
const seenLines = new Set<string>()
|
|
63
|
+
const deadline = Date.now() + STARTUP_LOG_WAIT_MS
|
|
64
|
+
|
|
65
|
+
while (Date.now() < deadline && seenLines.size < STARTUP_LOG_MAX_LINES) {
|
|
66
|
+
if (fs.existsSync(logPath)) {
|
|
67
|
+
const content = fs.readFileSync(logPath, 'utf8')
|
|
68
|
+
const lines = content
|
|
69
|
+
.split(/\r?\n/)
|
|
70
|
+
.map((entry) => {
|
|
71
|
+
return entry.trim()
|
|
72
|
+
})
|
|
73
|
+
.filter((entry) => {
|
|
74
|
+
return entry.length > 0
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
if (seenLines.has(line)) {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
seenLines.add(line)
|
|
82
|
+
console.error(`[gateway] ${line}`)
|
|
83
|
+
if (seenLines.size >= STARTUP_LOG_MAX_LINES) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await new Promise<void>((resolve) => {
|
|
90
|
+
setTimeout(resolve, STARTUP_LOG_POLL_MS)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (process.env.__OTTO_CHILD || isHelpFlag || !isGatewayStartInvocation()) {
|
|
96
|
+
await import('./cli.js')
|
|
97
|
+
} else if (!isGatewayDaemon) {
|
|
98
|
+
const dataDir = resolveDataDirFromArgv()
|
|
99
|
+
const daemon = spawn(process.execPath, process.argv.slice(1), {
|
|
100
|
+
stdio: 'ignore',
|
|
101
|
+
detached: true,
|
|
102
|
+
env: { ...process.env, OTTO_GATEWAY_DAEMON: '1' },
|
|
103
|
+
windowsHide: process.platform === 'win32',
|
|
104
|
+
})
|
|
105
|
+
daemon.unref()
|
|
106
|
+
console.error('otto gateway start: launched in background')
|
|
107
|
+
await printStartupLogs({ dataDir })
|
|
108
|
+
process.exit(0)
|
|
109
|
+
} else {
|
|
110
|
+
console.error(
|
|
111
|
+
'otto gateway start: supervised process will restart the child on crash',
|
|
112
|
+
)
|
|
113
|
+
console.error()
|
|
114
|
+
const EXIT_NO_RESTART = 64
|
|
115
|
+
const MAX_RAPID_RESTARTS = 5
|
|
116
|
+
const RAPID_RESTART_WINDOW_MS = 60_000
|
|
117
|
+
const RESTART_DELAY_MS = 2_000
|
|
118
|
+
|
|
119
|
+
const restartTimestamps: number[] = []
|
|
120
|
+
let child: ReturnType<typeof spawn> | null = null
|
|
121
|
+
let shutdownRequested = false
|
|
122
|
+
|
|
123
|
+
function start() {
|
|
124
|
+
if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
|
|
125
|
+
fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true })
|
|
126
|
+
}
|
|
127
|
+
const heapArgs = [
|
|
128
|
+
`--heapsnapshot-near-heap-limit=3`,
|
|
129
|
+
`--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
|
|
130
|
+
]
|
|
131
|
+
const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)]
|
|
132
|
+
child = spawn(process.argv[0]!, args, {
|
|
133
|
+
stdio: 'ignore',
|
|
134
|
+
env: { ...process.env, __OTTO_CHILD: '1' },
|
|
135
|
+
windowsHide: process.platform === 'win32',
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
child.on('exit', (code, signal) => {
|
|
139
|
+
if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
|
|
140
|
+
process.exit(code ?? 0)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const now = Date.now()
|
|
145
|
+
restartTimestamps.push(now)
|
|
146
|
+
while (
|
|
147
|
+
restartTimestamps.length > 0 &&
|
|
148
|
+
restartTimestamps[0]! < now - RAPID_RESTART_WINDOW_MS
|
|
149
|
+
) {
|
|
150
|
+
restartTimestamps.shift()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
|
|
154
|
+
console.error(
|
|
155
|
+
`[otto] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`,
|
|
156
|
+
)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const reason = signal ? `signal ${signal}` : `code ${code}`
|
|
162
|
+
console.error(
|
|
163
|
+
`[otto] Process exited with ${reason}, restarting in ${RESTART_DELAY_MS / 1000}s...`,
|
|
164
|
+
)
|
|
165
|
+
setTimeout(start, RESTART_DELAY_MS)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const sig of ['SIGTERM', 'SIGINT'] as const) {
|
|
170
|
+
process.on(sig, () => {
|
|
171
|
+
shutdownRequested = true
|
|
172
|
+
child?.kill(sig)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
for (const sig of ['SIGUSR1', 'SIGUSR2'] as const) {
|
|
176
|
+
process.on(sig, () => {
|
|
177
|
+
child?.kill(sig)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
start()
|
|
182
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { extractBtwPrefix } from './btw-prefix-detection.js'
|
|
3
|
+
|
|
4
|
+
describe('extractBtwPrefix', () => {
|
|
5
|
+
test('matches lowercase prefix', () => {
|
|
6
|
+
expect(extractBtwPrefix('btw fix this')).toMatchInlineSnapshot(`
|
|
7
|
+
{
|
|
8
|
+
"prompt": "fix this",
|
|
9
|
+
}
|
|
10
|
+
`)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('matches uppercase prefix', () => {
|
|
14
|
+
expect(extractBtwPrefix('BTW check this')).toMatchInlineSnapshot(`
|
|
15
|
+
{
|
|
16
|
+
"prompt": "check this",
|
|
17
|
+
}
|
|
18
|
+
`)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('keeps multiline content', () => {
|
|
22
|
+
expect(extractBtwPrefix(' btw first line\nsecond line ')).toMatchInlineSnapshot(`
|
|
23
|
+
{
|
|
24
|
+
"prompt": "first line
|
|
25
|
+
second line",
|
|
26
|
+
}
|
|
27
|
+
`)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('matches dot separator', () => {
|
|
31
|
+
expect(extractBtwPrefix('btw. fix this')).toMatchInlineSnapshot(`
|
|
32
|
+
{
|
|
33
|
+
"prompt": "fix this",
|
|
34
|
+
}
|
|
35
|
+
`)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('matches comma separator', () => {
|
|
39
|
+
expect(extractBtwPrefix('btw, fix this')).toMatchInlineSnapshot(`
|
|
40
|
+
{
|
|
41
|
+
"prompt": "fix this",
|
|
42
|
+
}
|
|
43
|
+
`)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('matches colon separator', () => {
|
|
47
|
+
expect(extractBtwPrefix('btw: fix this')).toMatchInlineSnapshot(`
|
|
48
|
+
{
|
|
49
|
+
"prompt": "fix this",
|
|
50
|
+
}
|
|
51
|
+
`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('matches punctuation without trailing space', () => {
|
|
55
|
+
expect(extractBtwPrefix('btw.fix this')).toMatchInlineSnapshot(`
|
|
56
|
+
{
|
|
57
|
+
"prompt": "fix this",
|
|
58
|
+
}
|
|
59
|
+
`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('does not match without separating whitespace', () => {
|
|
63
|
+
expect(extractBtwPrefix('btwfix this')).toMatchInlineSnapshot(`null`)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('does not match mid-message', () => {
|
|
67
|
+
expect(extractBtwPrefix('hello btw fix this')).toMatchInlineSnapshot(`null`)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('does not match empty payload', () => {
|
|
71
|
+
expect(extractBtwPrefix('btw ')).toMatchInlineSnapshot(`null`)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Detects the raw `btw ` Discord message shortcut used to fork a side-question
|
|
2
|
+
// thread without invoking the /btw slash command UI.
|
|
3
|
+
|
|
4
|
+
export function extractBtwPrefix(
|
|
5
|
+
content: string,
|
|
6
|
+
): { prompt: string } | null {
|
|
7
|
+
if (!content) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Match "btw" followed by whitespace or punctuation (. , : ; ! ?) then the prompt
|
|
12
|
+
const match = content.match(/^\s*btw[.,;:!?\s]\s*([\s\S]+)$/i)
|
|
13
|
+
if (!match) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const prompt = match[1]?.trim()
|
|
18
|
+
if (!prompt) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { prompt }
|
|
23
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// Discord channel and category management.
|
|
2
|
+
// Creates and manages Otto project channels (text + voice pairs),
|
|
3
|
+
// extracts channel metadata from topic tags, and ensures category structure.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ChannelType,
|
|
7
|
+
type CategoryChannel,
|
|
8
|
+
type Guild,
|
|
9
|
+
type TextChannel,
|
|
10
|
+
} from 'discord.js'
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import {
|
|
14
|
+
getChannelDirectory,
|
|
15
|
+
setChannelDirectory,
|
|
16
|
+
findChannelsByDirectory,
|
|
17
|
+
} from './database.js'
|
|
18
|
+
import { getProjectsDir } from './config.js'
|
|
19
|
+
import { execAsync } from './worktrees.js'
|
|
20
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
21
|
+
|
|
22
|
+
const logger = createLogger(LogPrefix.CHANNEL)
|
|
23
|
+
|
|
24
|
+
// Legacy category names kept for backward-compat lookup on existing servers.
|
|
25
|
+
// New categories are created with the current "Otto" / "Otto Audio" names.
|
|
26
|
+
const CATEGORY_NAME = 'Otto'
|
|
27
|
+
const CATEGORY_NAME_AUDIO = 'Otto Audio'
|
|
28
|
+
const LEGACY_CATEGORY_NAME = 'Kimaki'
|
|
29
|
+
const LEGACY_CATEGORY_NAME_AUDIO = 'Kimaki Audio'
|
|
30
|
+
|
|
31
|
+
export async function ensureOttoCategory(
|
|
32
|
+
guild: Guild,
|
|
33
|
+
botName?: string,
|
|
34
|
+
): Promise<CategoryChannel> {
|
|
35
|
+
// Skip appending bot name if it's already "otto" to avoid "Otto otto"
|
|
36
|
+
const isOttoBot = botName?.toLowerCase() === 'otto'
|
|
37
|
+
const categoryName = botName && !isOttoBot ? `${CATEGORY_NAME} ${botName}` : CATEGORY_NAME
|
|
38
|
+
// Legacy names to check when looking up existing categories on older servers
|
|
39
|
+
const isLegacyKimakiBot = botName?.toLowerCase() === 'otto'
|
|
40
|
+
const legacyCategoryName =
|
|
41
|
+
botName && !isLegacyKimakiBot ? `${LEGACY_CATEGORY_NAME} ${botName}` : LEGACY_CATEGORY_NAME
|
|
42
|
+
|
|
43
|
+
const existingCategory = guild.channels.cache.find(
|
|
44
|
+
(channel): channel is CategoryChannel => {
|
|
45
|
+
if (channel.type !== ChannelType.GuildCategory) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
const name = channel.name.toLowerCase()
|
|
49
|
+
return (
|
|
50
|
+
name === categoryName.toLowerCase() ||
|
|
51
|
+
name === legacyCategoryName.toLowerCase()
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if (existingCategory) {
|
|
57
|
+
return existingCategory
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return guild.channels.create({
|
|
61
|
+
name: categoryName,
|
|
62
|
+
type: ChannelType.GuildCategory,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Keep old export name as an alias for any callers that haven't been updated yet
|
|
67
|
+
export const ensureKimakiCategory = ensureOttoCategory
|
|
68
|
+
|
|
69
|
+
export async function ensureOttoAudioCategory(
|
|
70
|
+
guild: Guild,
|
|
71
|
+
botName?: string,
|
|
72
|
+
): Promise<CategoryChannel> {
|
|
73
|
+
// Skip appending bot name if it's already "otto" to avoid "Otto Audio otto"
|
|
74
|
+
const isOttoBot = botName?.toLowerCase() === 'otto'
|
|
75
|
+
const categoryName =
|
|
76
|
+
botName && !isOttoBot ? `${CATEGORY_NAME_AUDIO} ${botName}` : CATEGORY_NAME_AUDIO
|
|
77
|
+
const isLegacyKimakiBot = botName?.toLowerCase() === 'otto'
|
|
78
|
+
const legacyCategoryName =
|
|
79
|
+
botName && !isLegacyKimakiBot
|
|
80
|
+
? `${LEGACY_CATEGORY_NAME_AUDIO} ${botName}`
|
|
81
|
+
: LEGACY_CATEGORY_NAME_AUDIO
|
|
82
|
+
|
|
83
|
+
const existingCategory = guild.channels.cache.find(
|
|
84
|
+
(channel): channel is CategoryChannel => {
|
|
85
|
+
if (channel.type !== ChannelType.GuildCategory) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
const name = channel.name.toLowerCase()
|
|
89
|
+
return (
|
|
90
|
+
name === categoryName.toLowerCase() ||
|
|
91
|
+
name === legacyCategoryName.toLowerCase()
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if (existingCategory) {
|
|
97
|
+
return existingCategory
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return guild.channels.create({
|
|
101
|
+
name: categoryName,
|
|
102
|
+
type: ChannelType.GuildCategory,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const ensureKimakiAudioCategory = ensureOttoAudioCategory
|
|
107
|
+
|
|
108
|
+
export async function createProjectChannels({
|
|
109
|
+
guild,
|
|
110
|
+
projectDirectory,
|
|
111
|
+
botName,
|
|
112
|
+
enableVoiceChannels = false,
|
|
113
|
+
}: {
|
|
114
|
+
guild: Guild
|
|
115
|
+
projectDirectory: string
|
|
116
|
+
botName?: string
|
|
117
|
+
enableVoiceChannels?: boolean
|
|
118
|
+
}): Promise<{
|
|
119
|
+
textChannelId: string
|
|
120
|
+
voiceChannelId: string | null
|
|
121
|
+
channelName: string
|
|
122
|
+
}> {
|
|
123
|
+
const baseName = path.basename(projectDirectory)
|
|
124
|
+
const channelName = `${baseName}`
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
127
|
+
.slice(0, 100)
|
|
128
|
+
|
|
129
|
+
const ottoCategory = await ensureOttoCategory(guild, botName)
|
|
130
|
+
|
|
131
|
+
const textChannel = await guild.channels.create({
|
|
132
|
+
name: channelName,
|
|
133
|
+
type: ChannelType.GuildText,
|
|
134
|
+
parent: ottoCategory,
|
|
135
|
+
// Channel configuration is stored in SQLite, not in the topic
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await setChannelDirectory({
|
|
139
|
+
channelId: textChannel.id,
|
|
140
|
+
directory: projectDirectory,
|
|
141
|
+
channelType: 'text',
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
let voiceChannelId: string | null = null
|
|
145
|
+
|
|
146
|
+
if (enableVoiceChannels) {
|
|
147
|
+
const ottoAudioCategory = await ensureOttoAudioCategory(guild, botName)
|
|
148
|
+
|
|
149
|
+
const voiceChannel = await guild.channels.create({
|
|
150
|
+
name: channelName,
|
|
151
|
+
type: ChannelType.GuildVoice,
|
|
152
|
+
parent: ottoAudioCategory,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await setChannelDirectory({
|
|
156
|
+
channelId: voiceChannel.id,
|
|
157
|
+
directory: projectDirectory,
|
|
158
|
+
channelType: 'voice',
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
voiceChannelId = voiceChannel.id
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
textChannelId: textChannel.id,
|
|
166
|
+
voiceChannelId,
|
|
167
|
+
channelName,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type ChannelWithTags = {
|
|
172
|
+
id: string
|
|
173
|
+
name: string
|
|
174
|
+
description: string | null
|
|
175
|
+
ottoDirectory?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function getChannelsWithDescriptions(
|
|
179
|
+
guild: Guild,
|
|
180
|
+
): Promise<ChannelWithTags[]> {
|
|
181
|
+
const channels: ChannelWithTags[] = []
|
|
182
|
+
|
|
183
|
+
const textChannels = guild.channels.cache.filter((channel) =>
|
|
184
|
+
channel.isTextBased(),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
for (const channel of textChannels.values()) {
|
|
188
|
+
const textChannel = channel as TextChannel
|
|
189
|
+
const description = textChannel.topic || null
|
|
190
|
+
|
|
191
|
+
// Get channel config from database instead of parsing XML from topic
|
|
192
|
+
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
193
|
+
|
|
194
|
+
channels.push({
|
|
195
|
+
id: textChannel.id,
|
|
196
|
+
name: textChannel.name,
|
|
197
|
+
description,
|
|
198
|
+
ottoDirectory: channelConfig?.directory,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return channels
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const DEFAULT_GITIGNORE = `node_modules/
|
|
206
|
+
dist/
|
|
207
|
+
.env
|
|
208
|
+
.env.*
|
|
209
|
+
!.env.example
|
|
210
|
+
.DS_Store
|
|
211
|
+
tmp/
|
|
212
|
+
*.log
|
|
213
|
+
__pycache__/
|
|
214
|
+
*.pyc
|
|
215
|
+
.venv/
|
|
216
|
+
*.egg-info/
|
|
217
|
+
`
|
|
218
|
+
|
|
219
|
+
const DEFAULT_CHANNEL_TOPIC =
|
|
220
|
+
'General channel for misc tasks with Otto. Not connected to a specific OpenCode project or repository.'
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create (or find) the default "otto" channel for general-purpose tasks.
|
|
224
|
+
* Channel name is "otto-{botName}" for self-hosted bots, "otto" for gateway.
|
|
225
|
+
* Directory is <dataDir>/projects/otto (or the legacy <dataDir>/projects/otto
|
|
226
|
+
* if that already exists on disk), git-initialized with a .gitignore.
|
|
227
|
+
*
|
|
228
|
+
* Idempotency: checks the database for an existing channel mapped to the
|
|
229
|
+
* otto (or legacy otto) projects directory. Also scans guild channels by
|
|
230
|
+
* name+category as a fallback for channels created before DB mapping existed.
|
|
231
|
+
*/
|
|
232
|
+
export async function createDefaultOttoChannel({
|
|
233
|
+
guild,
|
|
234
|
+
botName,
|
|
235
|
+
appId,
|
|
236
|
+
isGatewayMode,
|
|
237
|
+
}: {
|
|
238
|
+
guild: Guild
|
|
239
|
+
botName?: string
|
|
240
|
+
appId: string
|
|
241
|
+
isGatewayMode: boolean
|
|
242
|
+
}): Promise<{
|
|
243
|
+
textChannel: TextChannel
|
|
244
|
+
textChannelId: string
|
|
245
|
+
channelName: string
|
|
246
|
+
projectDirectory: string
|
|
247
|
+
} | null> {
|
|
248
|
+
// Use the legacy "otto" sub-directory if it already exists on disk so
|
|
249
|
+
// existing users keep their project history. New installs use "otto".
|
|
250
|
+
const legacyProjectDirectory = path.join(getProjectsDir(), 'otto')
|
|
251
|
+
const projectDirectory = fs.existsSync(legacyProjectDirectory)
|
|
252
|
+
? legacyProjectDirectory
|
|
253
|
+
: path.join(getProjectsDir(), 'otto')
|
|
254
|
+
|
|
255
|
+
// Ensure the project directory exists before any DB mapping restoration
|
|
256
|
+
// or git setup. Custom data dirs may not have <dataDir>/projects created
|
|
257
|
+
// yet, and later writes assume the full path is present.
|
|
258
|
+
if (!fs.existsSync(projectDirectory)) {
|
|
259
|
+
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
260
|
+
logger.log(`Created default otto directory: ${projectDirectory}`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Hydrate guild channels from API so the cache scan is complete
|
|
264
|
+
try {
|
|
265
|
+
await guild.channels.fetch()
|
|
266
|
+
} catch (error) {
|
|
267
|
+
logger.warn(
|
|
268
|
+
`Could not fetch guild channels for ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 1. Check database for existing channel mapped to this directory.
|
|
273
|
+
// Check ALL mappings (not just the first) since the same directory could
|
|
274
|
+
// have stale rows from deleted channels or other guilds.
|
|
275
|
+
const existingMappings = await findChannelsByDirectory({
|
|
276
|
+
directory: projectDirectory,
|
|
277
|
+
channelType: 'text',
|
|
278
|
+
})
|
|
279
|
+
const mappedChannelInGuild = existingMappings
|
|
280
|
+
.map((row) => guild.channels.cache.get(row.channel_id))
|
|
281
|
+
.find((ch): ch is TextChannel => ch?.type === ChannelType.GuildText)
|
|
282
|
+
if (mappedChannelInGuild) {
|
|
283
|
+
logger.log(`Default otto channel already exists: ${mappedChannelInGuild.id}`)
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 2. Fallback: detect existing channel by name+category (handles both the
|
|
288
|
+
// current "otto"/"otto-*" names and legacy "otto"/"otto-*" names).
|
|
289
|
+
const ottoCategory = await ensureOttoCategory(guild, botName)
|
|
290
|
+
const existingByName = guild.channels.cache.find((ch): ch is TextChannel => {
|
|
291
|
+
if (ch.type !== ChannelType.GuildText) {
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
if (ch.parentId !== ottoCategory.id) {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
return (
|
|
298
|
+
ch.name === 'otto' ||
|
|
299
|
+
ch.name.startsWith('otto-') ||
|
|
300
|
+
ch.name === 'otto' ||
|
|
301
|
+
ch.name.startsWith('otto-')
|
|
302
|
+
)
|
|
303
|
+
})
|
|
304
|
+
if (existingByName) {
|
|
305
|
+
logger.log(
|
|
306
|
+
`Found existing default channel by name: ${existingByName.id}, restoring DB mapping`,
|
|
307
|
+
)
|
|
308
|
+
await setChannelDirectory({
|
|
309
|
+
channelId: existingByName.id,
|
|
310
|
+
directory: projectDirectory,
|
|
311
|
+
channelType: 'text',
|
|
312
|
+
skipIfExists: true,
|
|
313
|
+
})
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Git init — gracefully skip if git is not installed
|
|
318
|
+
const gitDir = path.join(projectDirectory, '.git')
|
|
319
|
+
if (!fs.existsSync(gitDir)) {
|
|
320
|
+
try {
|
|
321
|
+
await execAsync('git init', { cwd: projectDirectory, timeout: 10_000 })
|
|
322
|
+
logger.log(`Initialized git in: ${projectDirectory}`)
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.warn(
|
|
325
|
+
`Could not initialize git in ${projectDirectory}: ${error instanceof Error ? error.stack : String(error)}`,
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Write .gitignore if it doesn't exist
|
|
331
|
+
const gitignorePath = path.join(projectDirectory, '.gitignore')
|
|
332
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
333
|
+
fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Channel name: "otto-{botName}" for self-hosted, "otto" for gateway
|
|
337
|
+
const channelName = (() => {
|
|
338
|
+
if (isGatewayMode || !botName) {
|
|
339
|
+
return 'otto'
|
|
340
|
+
}
|
|
341
|
+
const sanitized = botName
|
|
342
|
+
.toLowerCase()
|
|
343
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
344
|
+
.replace(/-+/g, '-')
|
|
345
|
+
.replace(/^-|-$/g, '')
|
|
346
|
+
if (!sanitized || sanitized === 'otto') {
|
|
347
|
+
return 'otto'
|
|
348
|
+
}
|
|
349
|
+
return `otto-${sanitized}`.slice(0, 100)
|
|
350
|
+
})()
|
|
351
|
+
|
|
352
|
+
const textChannel = await guild.channels.create({
|
|
353
|
+
name: channelName,
|
|
354
|
+
type: ChannelType.GuildText,
|
|
355
|
+
parent: ottoCategory,
|
|
356
|
+
topic: DEFAULT_CHANNEL_TOPIC,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
await setChannelDirectory({
|
|
360
|
+
channelId: textChannel.id,
|
|
361
|
+
directory: projectDirectory,
|
|
362
|
+
channelType: 'text',
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
logger.log(`Created default otto channel: #${channelName} (${textChannel.id})`)
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
textChannel,
|
|
369
|
+
textChannelId: textChannel.id,
|
|
370
|
+
channelName,
|
|
371
|
+
projectDirectory,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Keep legacy export name so any unupdated callers still compile
|
|
376
|
+
export const createDefaultKimakiChannel = createDefaultOttoChannel
|