@otto-assistant/otto 0.1.2 → 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/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,469 @@
|
|
|
1
|
+
// OpenCode plugin that injects synthetic message parts for context awareness:
|
|
2
|
+
// - Git branch / detached HEAD changes
|
|
3
|
+
// - Working directory (pwd) changes (e.g. after /new-worktree mid-session)
|
|
4
|
+
// - MEMORY.md reminder after a large assistant reply
|
|
5
|
+
// - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
|
|
6
|
+
//
|
|
7
|
+
// Synthetic parts are hidden from the TUI but sent to the model, keeping it
|
|
8
|
+
// aware of context changes without cluttering the UI.
|
|
9
|
+
//
|
|
10
|
+
// State design: all per-session mutable state is encapsulated in a single
|
|
11
|
+
// SessionState object per session ID. One Map, one delete() on cleanup.
|
|
12
|
+
// Decision logic is extracted into pure functions that take state + input
|
|
13
|
+
// and return whether to inject — making them testable without mocking.
|
|
14
|
+
//
|
|
15
|
+
// Exported from otto-opencode-plugin.ts — each export is treated as a separate
|
|
16
|
+
// plugin by OpenCode's plugin loader.
|
|
17
|
+
|
|
18
|
+
import type { Plugin } from '@opencode-ai/plugin'
|
|
19
|
+
import crypto from 'node:crypto'
|
|
20
|
+
import * as errore from 'errore'
|
|
21
|
+
import {
|
|
22
|
+
createPluginLogger,
|
|
23
|
+
formatPluginErrorWithStack,
|
|
24
|
+
setPluginLogFilePath,
|
|
25
|
+
} from './plugin-logger.js'
|
|
26
|
+
import { setDataDir } from './config.js'
|
|
27
|
+
import { initSentry, notifyError } from './sentry.js'
|
|
28
|
+
import { execAsync } from './exec-async.js'
|
|
29
|
+
import {
|
|
30
|
+
ONBOARDING_TUTORIAL_INSTRUCTIONS,
|
|
31
|
+
TUTORIAL_WELCOME_TEXT,
|
|
32
|
+
} from './onboarding-tutorial.js'
|
|
33
|
+
|
|
34
|
+
const logger = createPluginLogger('OPENCODE')
|
|
35
|
+
|
|
36
|
+
// ── Types ────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
type GitState = {
|
|
39
|
+
key: string
|
|
40
|
+
kind: 'branch' | 'detached-head' | 'detached-submodule'
|
|
41
|
+
label: string
|
|
42
|
+
warning: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// All per-session mutable state in one place. One Map entry, one delete.
|
|
46
|
+
type SessionState = {
|
|
47
|
+
gitState: GitState | undefined
|
|
48
|
+
gitStateDirectory: string | undefined
|
|
49
|
+
gitStateCheckedAtMs: number
|
|
50
|
+
lastMemoryReminderAssistantMessageId: string | undefined
|
|
51
|
+
latestAssistantMessage: AssistantMessageInfo | undefined
|
|
52
|
+
tutorialInjected: boolean
|
|
53
|
+
|
|
54
|
+
// Last directory observed from plugin input.directory.
|
|
55
|
+
// Updated on each real user message to detect cwd switches.
|
|
56
|
+
resolvedDirectory: string | undefined
|
|
57
|
+
// Last directory we announced via pwd injection.
|
|
58
|
+
announcedDirectory: string | undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Pure derivation functions ────────────────────────────────────
|
|
62
|
+
// These take state + fresh input and return whether to inject.
|
|
63
|
+
// No side effects, no mutations — easy to test with fixtures.
|
|
64
|
+
|
|
65
|
+
export function shouldInjectBranch({
|
|
66
|
+
previousGitState,
|
|
67
|
+
currentGitState,
|
|
68
|
+
}: {
|
|
69
|
+
previousGitState: GitState | undefined
|
|
70
|
+
currentGitState: GitState | null
|
|
71
|
+
}): { inject: false } | { inject: true; text: string } {
|
|
72
|
+
if (!currentGitState) {
|
|
73
|
+
return { inject: false }
|
|
74
|
+
}
|
|
75
|
+
if (previousGitState && previousGitState.key === currentGitState.key) {
|
|
76
|
+
return { inject: false }
|
|
77
|
+
}
|
|
78
|
+
// Trailing newline so this synthetic part does not fuse with the next text
|
|
79
|
+
// part when the model concatenates message parts.
|
|
80
|
+
const base = currentGitState.warning || `\n[current git branch is ${currentGitState.label}]`
|
|
81
|
+
return { inject: true, text: `${base}\n` }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function shouldInjectPwd({
|
|
85
|
+
currentDir,
|
|
86
|
+
previousDir,
|
|
87
|
+
announcedDir,
|
|
88
|
+
}: {
|
|
89
|
+
currentDir: string
|
|
90
|
+
previousDir: string | undefined
|
|
91
|
+
announcedDir: string | undefined
|
|
92
|
+
}): { inject: false } | { inject: true; text: string } {
|
|
93
|
+
if (announcedDir === currentDir) {
|
|
94
|
+
return { inject: false }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const priorDirectory = announcedDir || previousDir
|
|
98
|
+
if (!priorDirectory || priorDirectory === currentDir) {
|
|
99
|
+
return { inject: false }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
inject: true,
|
|
104
|
+
// Trailing newline so this synthetic part does not fuse with the next text
|
|
105
|
+
// part when the model concatenates message parts.
|
|
106
|
+
text:
|
|
107
|
+
`\n[working directory changed (cwd / pwd has changed). ` +
|
|
108
|
+
`The user expects you to edit files in the new cwd. ` +
|
|
109
|
+
`Previous folder (DO NOT TOUCH): ${priorDirectory}. ` +
|
|
110
|
+
`New folder (new cwd / pwd, edit files here): ${currentDir}. ` +
|
|
111
|
+
`You MUST read, write, and edit files only under the new folder ${currentDir}. ` +
|
|
112
|
+
`You MUST NOT read, write, or edit any files under the previous folder ${priorDirectory} — ` +
|
|
113
|
+
`that folder is a separate checkout and the user or another agent may be actively working there, ` +
|
|
114
|
+
`so writing to it would override their unrelated changes.]\n`,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000
|
|
119
|
+
const GIT_STATE_CACHE_TTL_MS = 2_000
|
|
120
|
+
|
|
121
|
+
type AssistantTokenUsage = {
|
|
122
|
+
input: number
|
|
123
|
+
output: number
|
|
124
|
+
reasoning: number
|
|
125
|
+
cache: { read: number; write: number }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type AssistantMessageInfo = {
|
|
129
|
+
id: string
|
|
130
|
+
role: string
|
|
131
|
+
time?: { completed?: number; created?: number }
|
|
132
|
+
tokens?: AssistantTokenUsage
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function shouldInjectMemoryReminderFromLatestAssistant({
|
|
136
|
+
lastMemoryReminderAssistantMessageId,
|
|
137
|
+
latestAssistantMessage,
|
|
138
|
+
threshold = MEMORY_REMINDER_OUTPUT_TOKENS,
|
|
139
|
+
}: {
|
|
140
|
+
lastMemoryReminderAssistantMessageId?: string
|
|
141
|
+
latestAssistantMessage: AssistantMessageInfo | undefined
|
|
142
|
+
threshold?: number
|
|
143
|
+
}): { inject: false } | { inject: true; assistantMessageId: string } {
|
|
144
|
+
if (!latestAssistantMessage) {
|
|
145
|
+
return { inject: false }
|
|
146
|
+
}
|
|
147
|
+
if (latestAssistantMessage.role !== 'assistant') {
|
|
148
|
+
return { inject: false }
|
|
149
|
+
}
|
|
150
|
+
if (typeof latestAssistantMessage.time?.completed !== 'number') {
|
|
151
|
+
return { inject: false }
|
|
152
|
+
}
|
|
153
|
+
if (!latestAssistantMessage.tokens) {
|
|
154
|
+
return { inject: false }
|
|
155
|
+
}
|
|
156
|
+
if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
|
|
157
|
+
return { inject: false }
|
|
158
|
+
}
|
|
159
|
+
const outputTokens = Math.max(
|
|
160
|
+
0,
|
|
161
|
+
latestAssistantMessage.tokens.output + latestAssistantMessage.tokens.reasoning,
|
|
162
|
+
)
|
|
163
|
+
if (outputTokens < threshold) {
|
|
164
|
+
return { inject: false }
|
|
165
|
+
}
|
|
166
|
+
return { inject: true, assistantMessageId: latestAssistantMessage.id }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function shouldInjectTutorial({
|
|
170
|
+
alreadyInjected,
|
|
171
|
+
parts,
|
|
172
|
+
}: {
|
|
173
|
+
alreadyInjected: boolean
|
|
174
|
+
parts: Array<{ type: string; text?: string }>
|
|
175
|
+
}): boolean {
|
|
176
|
+
if (alreadyInjected) {
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
return parts.some((part) => {
|
|
180
|
+
return part.type === 'text' && part.text?.includes(TUTORIAL_WELCOME_TEXT)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Impure helpers (I/O) ─────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
async function resolveGitState({
|
|
187
|
+
directory,
|
|
188
|
+
}: {
|
|
189
|
+
directory: string
|
|
190
|
+
}): Promise<GitState | null> {
|
|
191
|
+
const branchResult = await errore.tryAsync(() => {
|
|
192
|
+
return execAsync('git symbolic-ref --short HEAD', { cwd: directory })
|
|
193
|
+
})
|
|
194
|
+
if (!(branchResult instanceof Error)) {
|
|
195
|
+
const branch = branchResult.stdout.trim()
|
|
196
|
+
if (branch) {
|
|
197
|
+
return {
|
|
198
|
+
key: `branch:${branch}`,
|
|
199
|
+
kind: 'branch',
|
|
200
|
+
label: branch,
|
|
201
|
+
warning: null,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const shaResult = await errore.tryAsync(() => {
|
|
207
|
+
return execAsync('git rev-parse --short HEAD', { cwd: directory })
|
|
208
|
+
})
|
|
209
|
+
if (shaResult instanceof Error) {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const shortSha = shaResult.stdout.trim()
|
|
214
|
+
if (!shortSha) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const superprojectResult = await errore.tryAsync(() => {
|
|
219
|
+
return execAsync('git rev-parse --show-superproject-working-tree', {
|
|
220
|
+
cwd: directory,
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
const superproject =
|
|
224
|
+
superprojectResult instanceof Error ? '' : superprojectResult.stdout.trim()
|
|
225
|
+
if (superproject) {
|
|
226
|
+
return {
|
|
227
|
+
key: `detached-submodule:${shortSha}`,
|
|
228
|
+
kind: 'detached-submodule',
|
|
229
|
+
label: `detached submodule @ ${shortSha}`,
|
|
230
|
+
warning:
|
|
231
|
+
`\n[warning: submodule is in detached HEAD at ${shortSha}. ` +
|
|
232
|
+
'create or switch to a branch before committing.]',
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
key: `detached-head:${shortSha}`,
|
|
238
|
+
kind: 'detached-head',
|
|
239
|
+
label: `detached HEAD @ ${shortSha}`,
|
|
240
|
+
warning:
|
|
241
|
+
`\n[warning: repository is in detached HEAD at ${shortSha}. ` +
|
|
242
|
+
'create or switch to a branch before committing.]',
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Plugin ───────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
const contextAwarenessPlugin: Plugin = async ({ directory }) => {
|
|
249
|
+
initSentry()
|
|
250
|
+
|
|
251
|
+
const dataDir = process.env.OTTO_DATA_DIR
|
|
252
|
+
if (dataDir) {
|
|
253
|
+
setDataDir(dataDir)
|
|
254
|
+
setPluginLogFilePath(dataDir)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Single Map for all per-session state. One entry per session, one
|
|
258
|
+
// delete on cleanup — no parallel Maps that can drift out of sync.
|
|
259
|
+
const sessions = new Map<string, SessionState>()
|
|
260
|
+
|
|
261
|
+
function getOrCreateSession(sessionID: string): SessionState {
|
|
262
|
+
const existing = sessions.get(sessionID)
|
|
263
|
+
if (existing) {
|
|
264
|
+
return existing
|
|
265
|
+
}
|
|
266
|
+
const state: SessionState = {
|
|
267
|
+
gitState: undefined,
|
|
268
|
+
gitStateDirectory: undefined,
|
|
269
|
+
gitStateCheckedAtMs: 0,
|
|
270
|
+
lastMemoryReminderAssistantMessageId: undefined,
|
|
271
|
+
latestAssistantMessage: undefined,
|
|
272
|
+
tutorialInjected: false,
|
|
273
|
+
|
|
274
|
+
resolvedDirectory: undefined,
|
|
275
|
+
announcedDirectory: undefined,
|
|
276
|
+
}
|
|
277
|
+
sessions.set(sessionID, state)
|
|
278
|
+
return state
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
'chat.message': async (input, output) => {
|
|
283
|
+
const hookResult = await errore.tryAsync({
|
|
284
|
+
try: async () => {
|
|
285
|
+
const { sessionID } = input
|
|
286
|
+
const state = getOrCreateSession(sessionID)
|
|
287
|
+
|
|
288
|
+
// -- Onboarding tutorial injection --
|
|
289
|
+
// Runs before the non-synthetic text guard because the tutorial
|
|
290
|
+
// marker (TUTORIAL_WELCOME_TEXT) can appear in synthetic/system
|
|
291
|
+
// parts prepended by message-preprocessing.ts. The old separate
|
|
292
|
+
// plugin had no such guard, so this preserves that behavior.
|
|
293
|
+
const firstTextPart = output.parts.find((part) => {
|
|
294
|
+
return part.type === 'text'
|
|
295
|
+
})
|
|
296
|
+
if (firstTextPart && shouldInjectTutorial({ alreadyInjected: state.tutorialInjected, parts: output.parts })) {
|
|
297
|
+
state.tutorialInjected = true
|
|
298
|
+
output.parts.push({
|
|
299
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
300
|
+
sessionID,
|
|
301
|
+
messageID: firstTextPart.messageID,
|
|
302
|
+
type: 'text' as const,
|
|
303
|
+
text: `<system-reminder>\n${ONBOARDING_TUTORIAL_INSTRUCTIONS}\n</system-reminder>\n`,
|
|
304
|
+
synthetic: true,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// -- Find first non-synthetic user text part --
|
|
309
|
+
// All remaining injections (branch, pwd, memory, time gap) only
|
|
310
|
+
// apply to real user messages, not empty or synthetic-only messages.
|
|
311
|
+
const first = output.parts.find((part) => {
|
|
312
|
+
if (part.type !== 'text') {
|
|
313
|
+
return true
|
|
314
|
+
}
|
|
315
|
+
return part.synthetic !== true
|
|
316
|
+
})
|
|
317
|
+
if (!first || first.type !== 'text' || first.text.trim().length === 0) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const messageID = first.messageID
|
|
322
|
+
|
|
323
|
+
const latestAssistantMessage = state.latestAssistantMessage
|
|
324
|
+
|
|
325
|
+
// The plugin request directory is the current directory Otto asked
|
|
326
|
+
// OpenCode to operate on for this message. Keep the previous observed
|
|
327
|
+
// directory in session state to detect cwd changes without SDK calls.
|
|
328
|
+
const effectiveDirectory = directory
|
|
329
|
+
const previousDirectory = state.resolvedDirectory
|
|
330
|
+
state.resolvedDirectory = effectiveDirectory
|
|
331
|
+
|
|
332
|
+
// -- Branch / detached HEAD detection --
|
|
333
|
+
// Avoid spawning git subprocesses on every turn in the same directory.
|
|
334
|
+
// Refresh only on directory change or after a short TTL.
|
|
335
|
+
const previousGitState = state.gitState
|
|
336
|
+
const now = Date.now()
|
|
337
|
+
const shouldRefreshGitState =
|
|
338
|
+
!state.gitState ||
|
|
339
|
+
state.gitStateDirectory !== effectiveDirectory ||
|
|
340
|
+
now - state.gitStateCheckedAtMs >= GIT_STATE_CACHE_TTL_MS
|
|
341
|
+
const gitState = shouldRefreshGitState
|
|
342
|
+
? await resolveGitState({ directory: effectiveDirectory })
|
|
343
|
+
: state.gitState || null
|
|
344
|
+
if (shouldRefreshGitState) {
|
|
345
|
+
state.gitState = gitState || undefined
|
|
346
|
+
state.gitStateDirectory = effectiveDirectory
|
|
347
|
+
state.gitStateCheckedAtMs = now
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// -- Working directory change detection --
|
|
351
|
+
const pwdResult = shouldInjectPwd({
|
|
352
|
+
currentDir: effectiveDirectory,
|
|
353
|
+
previousDir:
|
|
354
|
+
previousDirectory && previousDirectory !== effectiveDirectory
|
|
355
|
+
? previousDirectory
|
|
356
|
+
: undefined,
|
|
357
|
+
announcedDir: state.announcedDirectory,
|
|
358
|
+
})
|
|
359
|
+
if (pwdResult.inject) {
|
|
360
|
+
state.announcedDirectory = effectiveDirectory
|
|
361
|
+
output.parts.push({
|
|
362
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
363
|
+
sessionID,
|
|
364
|
+
messageID,
|
|
365
|
+
type: 'text' as const,
|
|
366
|
+
text: pwdResult.text,
|
|
367
|
+
synthetic: true,
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
|
|
372
|
+
lastMemoryReminderAssistantMessageId:
|
|
373
|
+
state.lastMemoryReminderAssistantMessageId,
|
|
374
|
+
latestAssistantMessage,
|
|
375
|
+
})
|
|
376
|
+
if (memoryReminder.inject) {
|
|
377
|
+
output.parts.push({
|
|
378
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
379
|
+
sessionID,
|
|
380
|
+
messageID,
|
|
381
|
+
type: 'text' as const,
|
|
382
|
+
text: '<system-reminder>The previous assistant message was large. If the conversation had non-obvious learnings that prevent future mistakes and are not already in code comments or AGENTS.md, add them to MEMORY.md with concise titles and brief content (2-3 sentences max).</system-reminder>\n',
|
|
383
|
+
synthetic: true,
|
|
384
|
+
})
|
|
385
|
+
state.lastMemoryReminderAssistantMessageId =
|
|
386
|
+
memoryReminder.assistantMessageId
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// -- Branch injection (last synthetic part) --
|
|
390
|
+
const branchResult = shouldInjectBranch({
|
|
391
|
+
previousGitState,
|
|
392
|
+
currentGitState: gitState,
|
|
393
|
+
})
|
|
394
|
+
if (branchResult.inject) {
|
|
395
|
+
state.gitState = gitState!
|
|
396
|
+
output.parts.push({
|
|
397
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
398
|
+
sessionID,
|
|
399
|
+
messageID,
|
|
400
|
+
type: 'text' as const,
|
|
401
|
+
text: branchResult.text,
|
|
402
|
+
synthetic: true,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
catch: (error) => {
|
|
407
|
+
return new Error('context-awareness chat.message hook failed', { cause: error })
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
if (hookResult instanceof Error) {
|
|
411
|
+
logger.warn(
|
|
412
|
+
`[context-awareness-plugin] ${formatPluginErrorWithStack(hookResult)}`,
|
|
413
|
+
)
|
|
414
|
+
void notifyError(hookResult, 'context-awareness plugin chat.message hook failed')
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
// Keep session-local assistant metadata updated from event stream and
|
|
419
|
+
// clean up state when sessions are deleted.
|
|
420
|
+
event: async ({ event }) => {
|
|
421
|
+
const eventResult = await errore.tryAsync({
|
|
422
|
+
try: async () => {
|
|
423
|
+
if (event.type === 'message.updated') {
|
|
424
|
+
const info = event.properties?.info
|
|
425
|
+
if (!info || info.role !== 'assistant' || typeof info.id !== 'string') {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
const infoWithSession = info as { sessionID?: unknown }
|
|
429
|
+
const sessionID =
|
|
430
|
+
typeof infoWithSession.sessionID === 'string'
|
|
431
|
+
? infoWithSession.sessionID
|
|
432
|
+
: undefined
|
|
433
|
+
if (!sessionID) {
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
const state = getOrCreateSession(sessionID)
|
|
437
|
+
state.latestAssistantMessage = {
|
|
438
|
+
id: info.id,
|
|
439
|
+
role: info.role,
|
|
440
|
+
time: info.time,
|
|
441
|
+
tokens: info.tokens,
|
|
442
|
+
}
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (event.type !== 'session.deleted') {
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
const id = event.properties?.info?.id
|
|
450
|
+
if (!id) {
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
sessions.delete(id)
|
|
454
|
+
},
|
|
455
|
+
catch: (error) => {
|
|
456
|
+
return new Error('context-awareness event hook failed', { cause: error })
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
if (eventResult instanceof Error) {
|
|
460
|
+
logger.warn(
|
|
461
|
+
`[context-awareness-plugin] ${formatPluginErrorWithStack(eventResult)}`,
|
|
462
|
+
)
|
|
463
|
+
void notifyError(eventResult, 'context-awareness plugin event hook failed')
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export { contextAwarenessPlugin }
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Shared utilities for invoking the critique CLI and parsing its JSON output.
|
|
2
|
+
// Used by /diff command and footer diff link uploads.
|
|
3
|
+
|
|
4
|
+
import { execAsync } from './worktrees.js'
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
6
|
+
|
|
7
|
+
const logger = createLogger(LogPrefix.DIFF)
|
|
8
|
+
|
|
9
|
+
const CRITIQUE_TIMEOUT_MS = 30_000
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Shell-quote a string by wrapping in single quotes and escaping embedded
|
|
13
|
+
* single quotes. Prevents injection when interpolating into shell commands.
|
|
14
|
+
*/
|
|
15
|
+
function shellQuote(s: string): string {
|
|
16
|
+
return `'${s.replace(/'/g, "'\\''")}'`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CritiqueResult = {
|
|
20
|
+
url: string
|
|
21
|
+
id: string
|
|
22
|
+
error?: undefined
|
|
23
|
+
} | {
|
|
24
|
+
url?: undefined
|
|
25
|
+
id?: undefined
|
|
26
|
+
error: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse critique --json output. Critique prints progress to stderr and JSON
|
|
31
|
+
* to stdout. The JSON line contains { url, id } on success or { error } on
|
|
32
|
+
* failure. We scan all lines for the first valid JSON object with a url or
|
|
33
|
+
* error field, falling back to searching for a critique.work URL in the raw
|
|
34
|
+
* output.
|
|
35
|
+
*/
|
|
36
|
+
export function parseCritiqueOutput(output: string): CritiqueResult | undefined {
|
|
37
|
+
const lines = output.trim().split('\n')
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (!line.startsWith('{')) {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(line) as {
|
|
44
|
+
url?: string
|
|
45
|
+
id?: string
|
|
46
|
+
error?: string
|
|
47
|
+
}
|
|
48
|
+
if (parsed.error) {
|
|
49
|
+
return { error: parsed.error }
|
|
50
|
+
}
|
|
51
|
+
if (parsed.url && parsed.id) {
|
|
52
|
+
return { url: parsed.url, id: parsed.id }
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// not valid JSON, try next line
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Fallback: try to find a URL in the raw output
|
|
59
|
+
const urlMatch = output.match(/https?:\/\/critique\.work\/[^\s]+/)
|
|
60
|
+
if (urlMatch) {
|
|
61
|
+
const url = urlMatch[0]
|
|
62
|
+
// Extract ID from URL path: /v/{id}
|
|
63
|
+
const idMatch = url.match(/\/v\/([a-f0-9]+)/)
|
|
64
|
+
const id = idMatch?.[1]
|
|
65
|
+
if (id) {
|
|
66
|
+
return { url, id }
|
|
67
|
+
}
|
|
68
|
+
// URL without parseable id — return as error so callers don't build
|
|
69
|
+
// broken OG image URLs from an empty id
|
|
70
|
+
return { error: url }
|
|
71
|
+
}
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run critique on the current git working tree diff and return the result.
|
|
77
|
+
* Used by the /diff slash command.
|
|
78
|
+
*/
|
|
79
|
+
export async function uploadGitDiffViaCritique({
|
|
80
|
+
title,
|
|
81
|
+
cwd,
|
|
82
|
+
}: {
|
|
83
|
+
title: string
|
|
84
|
+
cwd: string
|
|
85
|
+
}): Promise<CritiqueResult | undefined> {
|
|
86
|
+
try {
|
|
87
|
+
const { stdout, stderr } = await execAsync(
|
|
88
|
+
`critique --web ${shellQuote(title)} --json`,
|
|
89
|
+
{ cwd, timeout: CRITIQUE_TIMEOUT_MS },
|
|
90
|
+
)
|
|
91
|
+
return parseCritiqueOutput(stdout || stderr)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// exec error includes stdout/stderr — try to parse JSON from it
|
|
94
|
+
const execError = error as {
|
|
95
|
+
stdout?: string
|
|
96
|
+
stderr?: string
|
|
97
|
+
message?: string
|
|
98
|
+
}
|
|
99
|
+
const output = execError.stdout || execError.stderr || ''
|
|
100
|
+
const parsed = parseCritiqueOutput(output)
|
|
101
|
+
if (parsed) {
|
|
102
|
+
return parsed
|
|
103
|
+
}
|
|
104
|
+
const message = execError.message || 'Unknown error'
|
|
105
|
+
if (message.includes('command not found') || message.includes('ENOENT')) {
|
|
106
|
+
return { error: 'critique not available' }
|
|
107
|
+
}
|
|
108
|
+
return { error: `Failed to generate diff: ${message.slice(0, 200)}` }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Upload a .patch file to critique.work via critique --stdin.
|
|
114
|
+
* Returns the critique URL on success, undefined on failure.
|
|
115
|
+
* Default timeout is 10s since this runs in the background (footer edit).
|
|
116
|
+
*/
|
|
117
|
+
export async function uploadPatchViaCritique({
|
|
118
|
+
patchPath,
|
|
119
|
+
title,
|
|
120
|
+
cwd,
|
|
121
|
+
timeoutMs = 10_000,
|
|
122
|
+
}: {
|
|
123
|
+
patchPath: string
|
|
124
|
+
title: string
|
|
125
|
+
cwd: string
|
|
126
|
+
timeoutMs?: number
|
|
127
|
+
}): Promise<string | undefined> {
|
|
128
|
+
try {
|
|
129
|
+
const { stdout } = await execAsync(
|
|
130
|
+
`critique --stdin --web ${shellQuote(title)} --json < ${shellQuote(patchPath)}`,
|
|
131
|
+
{ cwd, timeout: timeoutMs },
|
|
132
|
+
)
|
|
133
|
+
const result = parseCritiqueOutput(stdout)
|
|
134
|
+
return result?.url
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.error('critique upload failed:', error)
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
}
|