@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,303 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { normalizeAnthropicAccountIdentity, } from './anthropic-account-identity.js';
|
|
5
|
+
const AUTH_LOCK_STALE_MS = 30_000;
|
|
6
|
+
const AUTH_LOCK_RETRY_MS = 100;
|
|
7
|
+
async function readJson(filePath, fallback) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function writeJson(filePath, value) {
|
|
16
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
17
|
+
await fs.writeFile(filePath, JSON.stringify(value, null, 2), 'utf8');
|
|
18
|
+
await fs.chmod(filePath, 0o600);
|
|
19
|
+
}
|
|
20
|
+
function getErrorCode(error) {
|
|
21
|
+
if (!(error instanceof Error))
|
|
22
|
+
return undefined;
|
|
23
|
+
return error.code;
|
|
24
|
+
}
|
|
25
|
+
async function sleep(ms) {
|
|
26
|
+
await new Promise((resolve) => {
|
|
27
|
+
setTimeout(resolve, ms);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function authFilePath() {
|
|
31
|
+
if (process.env.XDG_DATA_HOME) {
|
|
32
|
+
return path.join(process.env.XDG_DATA_HOME, 'opencode', 'auth.json');
|
|
33
|
+
}
|
|
34
|
+
return path.join(homedir(), '.local', 'share', 'opencode', 'auth.json');
|
|
35
|
+
}
|
|
36
|
+
export function accountsFilePath() {
|
|
37
|
+
if (process.env.XDG_DATA_HOME) {
|
|
38
|
+
return path.join(process.env.XDG_DATA_HOME, 'opencode', 'anthropic-oauth-accounts.json');
|
|
39
|
+
}
|
|
40
|
+
return path.join(homedir(), '.local', 'share', 'opencode', 'anthropic-oauth-accounts.json');
|
|
41
|
+
}
|
|
42
|
+
export async function withAuthStateLock(fn) {
|
|
43
|
+
const file = authFilePath();
|
|
44
|
+
const lockDir = `${file}.lock`;
|
|
45
|
+
const deadline = Date.now() + AUTH_LOCK_STALE_MS;
|
|
46
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
47
|
+
while (true) {
|
|
48
|
+
try {
|
|
49
|
+
await fs.mkdir(lockDir);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const code = getErrorCode(error);
|
|
54
|
+
if (code !== 'EEXIST') {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const stats = await fs.stat(lockDir).catch(() => {
|
|
58
|
+
return null;
|
|
59
|
+
});
|
|
60
|
+
if (stats && Date.now() - stats.mtimeMs > AUTH_LOCK_STALE_MS) {
|
|
61
|
+
await fs.rm(lockDir, { force: true, recursive: true }).catch(() => { });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Date.now() >= deadline) {
|
|
65
|
+
throw new Error(`Timed out waiting for auth lock: ${lockDir}`);
|
|
66
|
+
}
|
|
67
|
+
await sleep(AUTH_LOCK_RETRY_MS);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return await fn();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await fs.rm(lockDir, { force: true, recursive: true }).catch(() => { });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function normalizeAccountStore(input) {
|
|
78
|
+
const accounts = Array.isArray(input?.accounts)
|
|
79
|
+
? input.accounts.filter((account) => !!account &&
|
|
80
|
+
account.type === 'oauth' &&
|
|
81
|
+
typeof account.refresh === 'string' &&
|
|
82
|
+
typeof account.access === 'string' &&
|
|
83
|
+
typeof account.expires === 'number' &&
|
|
84
|
+
(typeof account.email === 'undefined' || typeof account.email === 'string') &&
|
|
85
|
+
(typeof account.accountId === 'undefined' || typeof account.accountId === 'string') &&
|
|
86
|
+
typeof account.addedAt === 'number' &&
|
|
87
|
+
typeof account.lastUsed === 'number')
|
|
88
|
+
: [];
|
|
89
|
+
const rawIndex = typeof input?.activeIndex === 'number' ? Math.floor(input.activeIndex) : 0;
|
|
90
|
+
const activeIndex = accounts.length === 0 ? 0 : ((rawIndex % accounts.length) + accounts.length) % accounts.length;
|
|
91
|
+
return { version: 1, activeIndex, accounts };
|
|
92
|
+
}
|
|
93
|
+
export async function loadAccountStore() {
|
|
94
|
+
const raw = await readJson(accountsFilePath(), null);
|
|
95
|
+
return normalizeAccountStore(raw);
|
|
96
|
+
}
|
|
97
|
+
export async function saveAccountStore(store) {
|
|
98
|
+
await writeJson(accountsFilePath(), normalizeAccountStore(store));
|
|
99
|
+
}
|
|
100
|
+
/** Short label for an account: first 8 + last 4 chars of refresh token. */
|
|
101
|
+
export function accountLabel(account, index) {
|
|
102
|
+
const accountWithIdentity = account;
|
|
103
|
+
const identity = accountWithIdentity.email || accountWithIdentity.accountId;
|
|
104
|
+
const r = account.refresh;
|
|
105
|
+
const short = r.length > 12 ? `${r.slice(0, 8)}...${r.slice(-4)}` : r;
|
|
106
|
+
if (identity) {
|
|
107
|
+
return index !== undefined ? `#${index + 1} (${identity})` : identity;
|
|
108
|
+
}
|
|
109
|
+
return index !== undefined ? `#${index + 1} (${short})` : short;
|
|
110
|
+
}
|
|
111
|
+
function findCurrentAccountIndex(store, auth) {
|
|
112
|
+
if (!store.accounts.length)
|
|
113
|
+
return 0;
|
|
114
|
+
const byRefresh = store.accounts.findIndex((account) => {
|
|
115
|
+
return account.refresh === auth.refresh;
|
|
116
|
+
});
|
|
117
|
+
if (byRefresh >= 0)
|
|
118
|
+
return byRefresh;
|
|
119
|
+
const byAccess = store.accounts.findIndex((account) => {
|
|
120
|
+
return account.access === auth.access;
|
|
121
|
+
});
|
|
122
|
+
if (byAccess >= 0)
|
|
123
|
+
return byAccess;
|
|
124
|
+
return store.activeIndex;
|
|
125
|
+
}
|
|
126
|
+
export function upsertAccount(store, auth, now = Date.now()) {
|
|
127
|
+
const authWithIdentity = auth;
|
|
128
|
+
const identity = normalizeAnthropicAccountIdentity({
|
|
129
|
+
email: authWithIdentity.email,
|
|
130
|
+
accountId: authWithIdentity.accountId,
|
|
131
|
+
});
|
|
132
|
+
const index = store.accounts.findIndex((account) => {
|
|
133
|
+
if (account.refresh === auth.refresh || account.access === auth.access) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (identity?.accountId && account.accountId === identity.accountId) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (identity?.email && account.email === identity.email) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
});
|
|
144
|
+
const nextAccount = {
|
|
145
|
+
type: 'oauth',
|
|
146
|
+
refresh: auth.refresh,
|
|
147
|
+
access: auth.access,
|
|
148
|
+
expires: auth.expires,
|
|
149
|
+
...identity,
|
|
150
|
+
addedAt: now,
|
|
151
|
+
lastUsed: now,
|
|
152
|
+
};
|
|
153
|
+
if (index < 0) {
|
|
154
|
+
store.accounts.push(nextAccount);
|
|
155
|
+
store.activeIndex = store.accounts.length - 1;
|
|
156
|
+
return store.activeIndex;
|
|
157
|
+
}
|
|
158
|
+
const existing = store.accounts[index];
|
|
159
|
+
if (!existing)
|
|
160
|
+
return index;
|
|
161
|
+
store.accounts[index] = {
|
|
162
|
+
...existing,
|
|
163
|
+
...nextAccount,
|
|
164
|
+
addedAt: existing.addedAt,
|
|
165
|
+
email: nextAccount.email || existing.email,
|
|
166
|
+
accountId: nextAccount.accountId || existing.accountId,
|
|
167
|
+
};
|
|
168
|
+
store.activeIndex = index;
|
|
169
|
+
return index;
|
|
170
|
+
}
|
|
171
|
+
export async function rememberAnthropicOAuth(auth, identity) {
|
|
172
|
+
await withAuthStateLock(async () => {
|
|
173
|
+
const store = await loadAccountStore();
|
|
174
|
+
upsertAccount(store, { ...auth, ...normalizeAnthropicAccountIdentity(identity) });
|
|
175
|
+
await saveAccountStore(store);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function writeAnthropicAuthFile(auth) {
|
|
179
|
+
const file = authFilePath();
|
|
180
|
+
const data = await readJson(file, {});
|
|
181
|
+
if (auth) {
|
|
182
|
+
data.anthropic = auth;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
delete data.anthropic;
|
|
186
|
+
}
|
|
187
|
+
await writeJson(file, data);
|
|
188
|
+
}
|
|
189
|
+
function isOAuthStored(value) {
|
|
190
|
+
if (!value || typeof value !== 'object') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const record = value;
|
|
194
|
+
return (record.type === 'oauth' &&
|
|
195
|
+
typeof record.refresh === 'string' &&
|
|
196
|
+
typeof record.access === 'string' &&
|
|
197
|
+
typeof record.expires === 'number');
|
|
198
|
+
}
|
|
199
|
+
export async function getCurrentAnthropicAccount() {
|
|
200
|
+
const authJson = await readJson(authFilePath(), {});
|
|
201
|
+
const auth = authJson.anthropic;
|
|
202
|
+
if (!isOAuthStored(auth)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const store = await loadAccountStore();
|
|
206
|
+
const index = findCurrentAccountIndex(store, auth);
|
|
207
|
+
const account = store.accounts[index];
|
|
208
|
+
if (!account) {
|
|
209
|
+
return { auth };
|
|
210
|
+
}
|
|
211
|
+
if (account.refresh !== auth.refresh && account.access !== auth.access) {
|
|
212
|
+
return { auth };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
auth,
|
|
216
|
+
account,
|
|
217
|
+
index,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export async function setAnthropicAuth(auth, client) {
|
|
221
|
+
await writeAnthropicAuthFile(auth);
|
|
222
|
+
await client.auth.set({ path: { id: 'anthropic' }, body: auth });
|
|
223
|
+
}
|
|
224
|
+
export async function rotateAnthropicAccount(auth, client) {
|
|
225
|
+
return withAuthStateLock(async () => {
|
|
226
|
+
const store = await loadAccountStore();
|
|
227
|
+
if (store.accounts.length < 2)
|
|
228
|
+
return undefined;
|
|
229
|
+
const currentIndex = findCurrentAccountIndex(store, auth);
|
|
230
|
+
const currentAccount = store.accounts[currentIndex];
|
|
231
|
+
const nextIndex = (currentIndex + 1) % store.accounts.length;
|
|
232
|
+
const nextAccount = store.accounts[nextIndex];
|
|
233
|
+
if (!nextAccount)
|
|
234
|
+
return undefined;
|
|
235
|
+
const fromLabel = currentAccount
|
|
236
|
+
? accountLabel(currentAccount, currentIndex)
|
|
237
|
+
: accountLabel(auth, currentIndex);
|
|
238
|
+
nextAccount.lastUsed = Date.now();
|
|
239
|
+
store.activeIndex = nextIndex;
|
|
240
|
+
await saveAccountStore(store);
|
|
241
|
+
const nextAuth = {
|
|
242
|
+
type: 'oauth',
|
|
243
|
+
refresh: nextAccount.refresh,
|
|
244
|
+
access: nextAccount.access,
|
|
245
|
+
expires: nextAccount.expires,
|
|
246
|
+
};
|
|
247
|
+
await setAnthropicAuth(nextAuth, client);
|
|
248
|
+
return {
|
|
249
|
+
auth: nextAuth,
|
|
250
|
+
fromLabel,
|
|
251
|
+
toLabel: accountLabel(nextAccount, nextIndex),
|
|
252
|
+
fromIndex: currentIndex,
|
|
253
|
+
toIndex: nextIndex,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
export async function removeAccount(index) {
|
|
258
|
+
return withAuthStateLock(async () => {
|
|
259
|
+
const store = await loadAccountStore();
|
|
260
|
+
if (!Number.isInteger(index) || index < 0 || index >= store.accounts.length) {
|
|
261
|
+
throw new Error(`Account ${index + 1} does not exist`);
|
|
262
|
+
}
|
|
263
|
+
store.accounts.splice(index, 1);
|
|
264
|
+
if (store.accounts.length === 0) {
|
|
265
|
+
store.activeIndex = 0;
|
|
266
|
+
await saveAccountStore(store);
|
|
267
|
+
await writeAnthropicAuthFile(undefined);
|
|
268
|
+
return { store, active: undefined };
|
|
269
|
+
}
|
|
270
|
+
if (store.activeIndex > index) {
|
|
271
|
+
store.activeIndex -= 1;
|
|
272
|
+
}
|
|
273
|
+
else if (store.activeIndex >= store.accounts.length) {
|
|
274
|
+
store.activeIndex = 0;
|
|
275
|
+
}
|
|
276
|
+
const active = store.accounts[store.activeIndex];
|
|
277
|
+
if (!active)
|
|
278
|
+
throw new Error('Active Anthropic account disappeared during removal');
|
|
279
|
+
active.lastUsed = Date.now();
|
|
280
|
+
await saveAccountStore(store);
|
|
281
|
+
const nextAuth = {
|
|
282
|
+
type: 'oauth',
|
|
283
|
+
refresh: active.refresh,
|
|
284
|
+
access: active.access,
|
|
285
|
+
expires: active.expires,
|
|
286
|
+
};
|
|
287
|
+
await writeAnthropicAuthFile(nextAuth);
|
|
288
|
+
return { store, active: nextAuth };
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export function shouldRotateAuth(status, bodyText) {
|
|
292
|
+
const haystack = bodyText.toLowerCase();
|
|
293
|
+
if (status === 429)
|
|
294
|
+
return true;
|
|
295
|
+
if (status === 401 || status === 403)
|
|
296
|
+
return true;
|
|
297
|
+
return (haystack.includes('rate_limit') ||
|
|
298
|
+
haystack.includes('rate limit') ||
|
|
299
|
+
haystack.includes('invalid api key') ||
|
|
300
|
+
haystack.includes('authentication_error') ||
|
|
301
|
+
haystack.includes('permission_error') ||
|
|
302
|
+
haystack.includes('oauth'));
|
|
303
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Tests Anthropic OAuth account persistence, deduplication, and rotation.
|
|
2
|
+
import { mkdtemp, readFile, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
+
import { accountLabel, authFilePath, loadAccountStore, rememberAnthropicOAuth, removeAccount, rotateAnthropicAccount, saveAccountStore, shouldRotateAuth, } from './anthropic-auth-state.js';
|
|
7
|
+
const firstAccount = {
|
|
8
|
+
type: 'oauth',
|
|
9
|
+
refresh: 'refresh-first',
|
|
10
|
+
access: 'access-first',
|
|
11
|
+
expires: 1,
|
|
12
|
+
};
|
|
13
|
+
const secondAccount = {
|
|
14
|
+
type: 'oauth',
|
|
15
|
+
refresh: 'refresh-second',
|
|
16
|
+
access: 'access-second',
|
|
17
|
+
expires: 2,
|
|
18
|
+
};
|
|
19
|
+
let originalXdgDataHome;
|
|
20
|
+
let tempDir = '';
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
originalXdgDataHome = process.env.XDG_DATA_HOME;
|
|
23
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'anthropic-auth-plugin-'));
|
|
24
|
+
process.env.XDG_DATA_HOME = tempDir;
|
|
25
|
+
});
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
if (originalXdgDataHome === undefined) {
|
|
28
|
+
delete process.env.XDG_DATA_HOME;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
process.env.XDG_DATA_HOME = originalXdgDataHome;
|
|
32
|
+
}
|
|
33
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
34
|
+
});
|
|
35
|
+
describe('rememberAnthropicOAuth', () => {
|
|
36
|
+
test('stores accounts and updates existing entries by refresh token', async () => {
|
|
37
|
+
await rememberAnthropicOAuth(firstAccount);
|
|
38
|
+
await rememberAnthropicOAuth({ ...firstAccount, access: 'access-first-new', expires: 3 });
|
|
39
|
+
const store = await loadAccountStore();
|
|
40
|
+
expect(store.activeIndex).toBe(0);
|
|
41
|
+
expect(store.accounts).toHaveLength(1);
|
|
42
|
+
expect(store.accounts[0]).toMatchObject({
|
|
43
|
+
refresh: 'refresh-first',
|
|
44
|
+
access: 'access-first-new',
|
|
45
|
+
expires: 3,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
test('deduplicates new tokens by email or account ID', async () => {
|
|
49
|
+
await rememberAnthropicOAuth(firstAccount, {
|
|
50
|
+
email: 'user@example.com',
|
|
51
|
+
accountId: 'usr_123',
|
|
52
|
+
});
|
|
53
|
+
await rememberAnthropicOAuth(secondAccount, {
|
|
54
|
+
email: 'User@example.com',
|
|
55
|
+
accountId: 'usr_123',
|
|
56
|
+
});
|
|
57
|
+
const store = await loadAccountStore();
|
|
58
|
+
expect(store.accounts).toHaveLength(1);
|
|
59
|
+
expect(store.accounts[0]).toMatchObject({
|
|
60
|
+
refresh: 'refresh-second',
|
|
61
|
+
access: 'access-second',
|
|
62
|
+
email: 'user@example.com',
|
|
63
|
+
accountId: 'usr_123',
|
|
64
|
+
});
|
|
65
|
+
expect(accountLabel(store.accounts[0])).toBe('user@example.com');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('rotateAnthropicAccount', () => {
|
|
69
|
+
test('rotates to the next stored account and syncs auth state', async () => {
|
|
70
|
+
await saveAccountStore({
|
|
71
|
+
version: 1,
|
|
72
|
+
activeIndex: 0,
|
|
73
|
+
accounts: [
|
|
74
|
+
{ ...firstAccount, addedAt: 1, lastUsed: 1 },
|
|
75
|
+
{ ...secondAccount, addedAt: 2, lastUsed: 2 },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
const authSetCalls = [];
|
|
79
|
+
const client = {
|
|
80
|
+
auth: {
|
|
81
|
+
set: async (input) => {
|
|
82
|
+
authSetCalls.push(input);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const rotated = await rotateAnthropicAccount(firstAccount, client);
|
|
87
|
+
const store = await loadAccountStore();
|
|
88
|
+
const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
|
|
89
|
+
expect(rotated).toMatchObject({
|
|
90
|
+
auth: { refresh: 'refresh-second' },
|
|
91
|
+
fromLabel: '#1 (refresh-...irst)',
|
|
92
|
+
toLabel: '#2 (refresh-...cond)',
|
|
93
|
+
fromIndex: 0,
|
|
94
|
+
toIndex: 1,
|
|
95
|
+
});
|
|
96
|
+
expect(store.activeIndex).toBe(1);
|
|
97
|
+
expect(authJson.anthropic?.refresh).toBe('refresh-second');
|
|
98
|
+
expect(authSetCalls).toEqual([
|
|
99
|
+
{
|
|
100
|
+
path: { id: 'anthropic' },
|
|
101
|
+
body: {
|
|
102
|
+
type: 'oauth',
|
|
103
|
+
refresh: 'refresh-second',
|
|
104
|
+
access: 'access-second',
|
|
105
|
+
expires: 2,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('removeAccount', () => {
|
|
112
|
+
test('removing the active account promotes the next stored account', async () => {
|
|
113
|
+
await saveAccountStore({
|
|
114
|
+
version: 1,
|
|
115
|
+
activeIndex: 1,
|
|
116
|
+
accounts: [
|
|
117
|
+
{ ...firstAccount, addedAt: 1, lastUsed: 1 },
|
|
118
|
+
{ ...secondAccount, addedAt: 2, lastUsed: 2 },
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
await removeAccount(1);
|
|
122
|
+
const store = await loadAccountStore();
|
|
123
|
+
const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
|
|
124
|
+
expect(store.activeIndex).toBe(0);
|
|
125
|
+
expect(store.accounts).toHaveLength(1);
|
|
126
|
+
expect(store.accounts[0]?.refresh).toBe('refresh-first');
|
|
127
|
+
expect(authJson.anthropic?.refresh).toBe('refresh-first');
|
|
128
|
+
});
|
|
129
|
+
test('removing the last account clears active Anthropic auth', async () => {
|
|
130
|
+
await saveAccountStore({
|
|
131
|
+
version: 1,
|
|
132
|
+
activeIndex: 0,
|
|
133
|
+
accounts: [{ ...firstAccount, addedAt: 1, lastUsed: 1 }],
|
|
134
|
+
});
|
|
135
|
+
await mkdir(path.dirname(authFilePath()), { recursive: true });
|
|
136
|
+
await writeFile(authFilePath(), JSON.stringify({ anthropic: firstAccount }, null, 2));
|
|
137
|
+
await removeAccount(0);
|
|
138
|
+
const store = await loadAccountStore();
|
|
139
|
+
const authJson = JSON.parse(await readFile(authFilePath(), 'utf8'));
|
|
140
|
+
expect(store.accounts).toHaveLength(0);
|
|
141
|
+
expect(authJson.anthropic).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('shouldRotateAuth', () => {
|
|
145
|
+
test('only rotates on rate limit or auth failures', () => {
|
|
146
|
+
expect(shouldRotateAuth(429, '')).toBe(true);
|
|
147
|
+
expect(shouldRotateAuth(401, 'permission_error')).toBe(true);
|
|
148
|
+
expect(shouldRotateAuth(400, 'bad request')).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
const HEAP_SNAPSHOT_DIR = path.join(os.homedir(), '.otto', 'heap-snapshots');
|
|
21
|
+
const STARTUP_LOG_WAIT_MS = 2_500;
|
|
22
|
+
const STARTUP_LOG_POLL_MS = 200;
|
|
23
|
+
const STARTUP_LOG_MAX_LINES = 12;
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
25
|
+
const isHelpFlag = process.argv.includes('--help');
|
|
26
|
+
const isGatewayDaemon = process.env.OTTO_GATEWAY_DAEMON === '1';
|
|
27
|
+
function isGatewayStartInvocation() {
|
|
28
|
+
return argv[0] === 'gateway' && argv[1] === 'start';
|
|
29
|
+
}
|
|
30
|
+
function resolveDataDirFromArgv() {
|
|
31
|
+
const inlineArg = argv.find((entry) => {
|
|
32
|
+
return entry.startsWith('--data-dir=');
|
|
33
|
+
});
|
|
34
|
+
if (inlineArg) {
|
|
35
|
+
return path.resolve(inlineArg.slice('--data-dir='.length));
|
|
36
|
+
}
|
|
37
|
+
const dataDirIndex = argv.findIndex((entry) => {
|
|
38
|
+
return entry === '--data-dir';
|
|
39
|
+
});
|
|
40
|
+
if (dataDirIndex !== -1) {
|
|
41
|
+
const value = argv[dataDirIndex + 1];
|
|
42
|
+
if (value && !value.startsWith('--')) {
|
|
43
|
+
return path.resolve(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return path.join(os.homedir(), '.otto');
|
|
47
|
+
}
|
|
48
|
+
async function printStartupLogs({ dataDir, }) {
|
|
49
|
+
const logPath = path.join(dataDir, 'otto.log');
|
|
50
|
+
const seenLines = new Set();
|
|
51
|
+
const deadline = Date.now() + STARTUP_LOG_WAIT_MS;
|
|
52
|
+
while (Date.now() < deadline && seenLines.size < STARTUP_LOG_MAX_LINES) {
|
|
53
|
+
if (fs.existsSync(logPath)) {
|
|
54
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
55
|
+
const lines = content
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.map((entry) => {
|
|
58
|
+
return entry.trim();
|
|
59
|
+
})
|
|
60
|
+
.filter((entry) => {
|
|
61
|
+
return entry.length > 0;
|
|
62
|
+
});
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
if (seenLines.has(line)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
seenLines.add(line);
|
|
68
|
+
console.error(`[gateway] ${line}`);
|
|
69
|
+
if (seenLines.size >= STARTUP_LOG_MAX_LINES) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await new Promise((resolve) => {
|
|
75
|
+
setTimeout(resolve, STARTUP_LOG_POLL_MS);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (process.env.__OTTO_CHILD || isHelpFlag || !isGatewayStartInvocation()) {
|
|
80
|
+
await import('./cli.js');
|
|
81
|
+
}
|
|
82
|
+
else if (!isGatewayDaemon) {
|
|
83
|
+
const dataDir = resolveDataDirFromArgv();
|
|
84
|
+
const daemon = spawn(process.execPath, process.argv.slice(1), {
|
|
85
|
+
stdio: 'ignore',
|
|
86
|
+
detached: true,
|
|
87
|
+
env: { ...process.env, OTTO_GATEWAY_DAEMON: '1' },
|
|
88
|
+
windowsHide: process.platform === 'win32',
|
|
89
|
+
});
|
|
90
|
+
daemon.unref();
|
|
91
|
+
console.error('otto gateway start: launched in background');
|
|
92
|
+
await printStartupLogs({ dataDir });
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.error('otto gateway start: supervised process will restart the child on crash');
|
|
97
|
+
console.error();
|
|
98
|
+
const EXIT_NO_RESTART = 64;
|
|
99
|
+
const MAX_RAPID_RESTARTS = 5;
|
|
100
|
+
const RAPID_RESTART_WINDOW_MS = 60_000;
|
|
101
|
+
const RESTART_DELAY_MS = 2_000;
|
|
102
|
+
const restartTimestamps = [];
|
|
103
|
+
let child = null;
|
|
104
|
+
let shutdownRequested = false;
|
|
105
|
+
function start() {
|
|
106
|
+
if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
|
|
107
|
+
fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
const heapArgs = [
|
|
110
|
+
`--heapsnapshot-near-heap-limit=3`,
|
|
111
|
+
`--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
|
|
112
|
+
];
|
|
113
|
+
const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)];
|
|
114
|
+
child = spawn(process.argv[0], args, {
|
|
115
|
+
stdio: 'ignore',
|
|
116
|
+
env: { ...process.env, __OTTO_CHILD: '1' },
|
|
117
|
+
windowsHide: process.platform === 'win32',
|
|
118
|
+
});
|
|
119
|
+
child.on('exit', (code, signal) => {
|
|
120
|
+
if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
|
|
121
|
+
process.exit(code ?? 0);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
restartTimestamps.push(now);
|
|
126
|
+
while (restartTimestamps.length > 0 &&
|
|
127
|
+
restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
|
|
128
|
+
restartTimestamps.shift();
|
|
129
|
+
}
|
|
130
|
+
if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
|
|
131
|
+
console.error(`[otto] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
|
136
|
+
console.error(`[otto] Process exited with ${reason}, restarting in ${RESTART_DELAY_MS / 1000}s...`);
|
|
137
|
+
setTimeout(start, RESTART_DELAY_MS);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
141
|
+
process.on(sig, () => {
|
|
142
|
+
shutdownRequested = true;
|
|
143
|
+
child?.kill(sig);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
for (const sig of ['SIGUSR1', 'SIGUSR2']) {
|
|
147
|
+
process.on(sig, () => {
|
|
148
|
+
child?.kill(sig);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
start();
|
|
152
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Detects the raw `btw ` Discord message shortcut used to fork a side-question
|
|
2
|
+
// thread without invoking the /btw slash command UI.
|
|
3
|
+
export function extractBtwPrefix(content) {
|
|
4
|
+
if (!content) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
// Match "btw" followed by whitespace or punctuation (. , : ; ! ?) then the prompt
|
|
8
|
+
const match = content.match(/^\s*btw[.,;:!?\s]\s*([\s\S]+)$/i);
|
|
9
|
+
if (!match) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const prompt = match[1]?.trim();
|
|
13
|
+
if (!prompt) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return { prompt };
|
|
17
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { extractBtwPrefix } from './btw-prefix-detection.js';
|
|
3
|
+
describe('extractBtwPrefix', () => {
|
|
4
|
+
test('matches lowercase prefix', () => {
|
|
5
|
+
expect(extractBtwPrefix('btw fix this')).toMatchInlineSnapshot(`
|
|
6
|
+
{
|
|
7
|
+
"prompt": "fix this",
|
|
8
|
+
}
|
|
9
|
+
`);
|
|
10
|
+
});
|
|
11
|
+
test('matches uppercase prefix', () => {
|
|
12
|
+
expect(extractBtwPrefix('BTW check this')).toMatchInlineSnapshot(`
|
|
13
|
+
{
|
|
14
|
+
"prompt": "check this",
|
|
15
|
+
}
|
|
16
|
+
`);
|
|
17
|
+
});
|
|
18
|
+
test('keeps multiline content', () => {
|
|
19
|
+
expect(extractBtwPrefix(' btw first line\nsecond line ')).toMatchInlineSnapshot(`
|
|
20
|
+
{
|
|
21
|
+
"prompt": "first line
|
|
22
|
+
second line",
|
|
23
|
+
}
|
|
24
|
+
`);
|
|
25
|
+
});
|
|
26
|
+
test('matches dot separator', () => {
|
|
27
|
+
expect(extractBtwPrefix('btw. fix this')).toMatchInlineSnapshot(`
|
|
28
|
+
{
|
|
29
|
+
"prompt": "fix this",
|
|
30
|
+
}
|
|
31
|
+
`);
|
|
32
|
+
});
|
|
33
|
+
test('matches comma separator', () => {
|
|
34
|
+
expect(extractBtwPrefix('btw, fix this')).toMatchInlineSnapshot(`
|
|
35
|
+
{
|
|
36
|
+
"prompt": "fix this",
|
|
37
|
+
}
|
|
38
|
+
`);
|
|
39
|
+
});
|
|
40
|
+
test('matches colon separator', () => {
|
|
41
|
+
expect(extractBtwPrefix('btw: fix this')).toMatchInlineSnapshot(`
|
|
42
|
+
{
|
|
43
|
+
"prompt": "fix this",
|
|
44
|
+
}
|
|
45
|
+
`);
|
|
46
|
+
});
|
|
47
|
+
test('matches punctuation without trailing space', () => {
|
|
48
|
+
expect(extractBtwPrefix('btw.fix this')).toMatchInlineSnapshot(`
|
|
49
|
+
{
|
|
50
|
+
"prompt": "fix this",
|
|
51
|
+
}
|
|
52
|
+
`);
|
|
53
|
+
});
|
|
54
|
+
test('does not match without separating whitespace', () => {
|
|
55
|
+
expect(extractBtwPrefix('btwfix this')).toMatchInlineSnapshot(`null`);
|
|
56
|
+
});
|
|
57
|
+
test('does not match mid-message', () => {
|
|
58
|
+
expect(extractBtwPrefix('hello btw fix this')).toMatchInlineSnapshot(`null`);
|
|
59
|
+
});
|
|
60
|
+
test('does not match empty payload', () => {
|
|
61
|
+
expect(extractBtwPrefix('btw ')).toMatchInlineSnapshot(`null`);
|
|
62
|
+
});
|
|
63
|
+
});
|