@otto-assistant/bridge 0.4.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +2 -0
- package/dist/agent-model.e2e.test.js +755 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/anthropic-auth-plugin.js +728 -0
- package/dist/anthropic-auth-plugin.test.js +125 -0
- package/dist/anthropic-auth-state.js +231 -0
- package/dist/bin.js +90 -0
- package/dist/channel-management.js +227 -0
- package/dist/cli-parsing.test.js +137 -0
- package/dist/cli-send-thread.e2e.test.js +356 -0
- package/dist/cli.js +3276 -0
- package/dist/commands/abort.js +65 -0
- package/dist/commands/action-buttons.js +245 -0
- package/dist/commands/add-project.js +113 -0
- package/dist/commands/agent.js +335 -0
- package/dist/commands/ask-question.js +274 -0
- package/dist/commands/btw.js +116 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +130 -0
- package/dist/commands/diff.js +63 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +220 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +885 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/commands/mention-mode.js +44 -0
- package/dist/commands/merge-worktree.js +159 -0
- package/dist/commands/model-variant.js +364 -0
- package/dist/commands/model.js +776 -0
- package/dist/commands/new-worktree.js +366 -0
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/permissions.js +274 -0
- package/dist/commands/queue.js +206 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +127 -0
- package/dist/commands/resume.js +149 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/screenshare.js +303 -0
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +176 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +305 -0
- package/dist/commands/unset-model.js +138 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +155 -0
- package/dist/commands/verbosity.js +125 -0
- package/dist/commands/worktree-settings.js +43 -0
- package/dist/commands/worktrees.js +410 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +94 -0
- package/dist/context-awareness-plugin.js +363 -0
- package/dist/context-awareness-plugin.test.js +124 -0
- package/dist/critique-utils.js +95 -0
- package/dist/database.js +1310 -0
- package/dist/db.js +251 -0
- package/dist/db.test.js +138 -0
- package/dist/debounce-timeout.js +28 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +1008 -0
- package/dist/discord-command-registration.js +524 -0
- package/dist/discord-urls.js +81 -0
- package/dist/discord-utils.js +591 -0
- package/dist/discord-utils.test.js +134 -0
- package/dist/errors.js +157 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/event-stream-real-capture.e2e.test.js +533 -0
- package/dist/eventsource-parser.test.js +327 -0
- package/dist/exec-async.js +26 -0
- package/dist/external-opencode-sync.js +480 -0
- package/dist/format-tables.js +302 -0
- package/dist/format-tables.test.js +308 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +113 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +483 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +37 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +52 -0
- package/dist/generated/internal/class.js +49 -0
- package/dist/generated/internal/prismaNamespace.js +253 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_events.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +122 -0
- package/dist/hrana-server.js +263 -0
- package/dist/hrana-server.test.js +370 -0
- package/dist/html-actions.js +123 -0
- package/dist/html-actions.test.js +70 -0
- package/dist/html-components.js +117 -0
- package/dist/html-components.test.js +34 -0
- package/dist/image-optimizer-plugin.js +153 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +397 -0
- package/dist/ipc-polling.js +252 -0
- package/dist/ipc-tools-plugin.js +193 -0
- package/dist/kimaki-digital-twin.e2e.test.js +161 -0
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
- package/dist/kimaki-opencode-plugin.js +17 -0
- package/dist/kimaki-opencode-plugin.test.js +98 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +165 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +257 -0
- package/dist/message-finish-field.e2e.test.js +165 -0
- package/dist/message-formatting.js +413 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/message-preprocessing.js +330 -0
- package/dist/onboarding-tutorial.js +172 -0
- package/dist/onboarding-welcome.js +37 -0
- package/dist/openai-realtime.js +224 -0
- package/dist/opencode-command-detection.js +65 -0
- package/dist/opencode-command-detection.test.js +240 -0
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +361 -0
- package/dist/opencode-interrupt-plugin.test.js +458 -0
- package/dist/opencode.js +861 -0
- package/dist/otto/branding.js +22 -0
- package/dist/otto/index.js +21 -0
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/patch-text-parser.js +97 -0
- package/dist/plugin-logger.js +59 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +293 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +786 -0
- package/dist/queue-advanced-footer.e2e.test.js +472 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
- package/dist/queue-advanced-question.e2e.test.js +261 -0
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
- package/dist/queue-advanced-typing.e2e.test.js +153 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/queue-question-select-drain.e2e.test.js +120 -0
- package/dist/runtime-idle-sweeper.js +52 -0
- package/dist/runtime-lifecycle.e2e.test.js +508 -0
- package/dist/sentry.js +23 -0
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +420 -0
- package/dist/session-handler/event-stream-state.test.js +563 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/thread-runtime-state.js +104 -0
- package/dist/session-handler/thread-session-runtime.js +3258 -0
- package/dist/session-handler.js +9 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/session-title-rename.test.js +80 -0
- package/dist/startup-service.js +153 -0
- package/dist/startup-time.e2e.test.js +296 -0
- package/dist/store.js +17 -0
- package/dist/system-message.js +613 -0
- package/dist/system-message.test.js +602 -0
- package/dist/task-runner.js +295 -0
- package/dist/task-schedule.js +209 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/test-utils.js +299 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +999 -0
- package/dist/tools.js +357 -0
- package/dist/undo-redo.e2e.test.js +161 -0
- package/dist/unnest-code-blocks.js +146 -0
- package/dist/unnest-code-blocks.test.js +673 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +144 -0
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +646 -0
- package/dist/voice-message.e2e.test.js +1021 -0
- package/dist/voice.js +447 -0
- package/dist/voice.test.js +235 -0
- package/dist/wait-session.js +94 -0
- package/dist/websockify.js +69 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/dist/worktree-utils.js +3 -0
- package/dist/worktrees.js +929 -0
- package/dist/worktrees.test.js +189 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +98 -0
- package/schema.prisma +295 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +112 -0
- package/skills/egaki/SKILL.md +100 -0
- package/skills/errore/SKILL.md +647 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +644 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/lintcn/SKILL.md +873 -0
- package/skills/new-skill/SKILL.md +211 -0
- package/skills/npm-package/SKILL.md +239 -0
- package/skills/playwriter/SKILL.md +35 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/usecomputer/SKILL.md +264 -0
- package/skills/x-articles/SKILL.md +554 -0
- package/skills/zele/SKILL.md +112 -0
- package/skills/zustand-centralized-state/SKILL.md +1004 -0
- package/src/agent-model.e2e.test.ts +976 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +283 -0
- package/src/ai-tool.ts +39 -0
- package/src/anthropic-auth-plugin.test.ts +159 -0
- package/src/anthropic-auth-plugin.ts +861 -0
- package/src/anthropic-auth-state.ts +282 -0
- package/src/bin.ts +111 -0
- package/src/channel-management.ts +334 -0
- package/src/cli-parsing.test.ts +195 -0
- package/src/cli-send-thread.e2e.test.ts +464 -0
- package/src/cli.ts +4581 -0
- package/src/commands/abort.ts +89 -0
- package/src/commands/action-buttons.ts +364 -0
- package/src/commands/add-project.ts +149 -0
- package/src/commands/agent.ts +473 -0
- package/src/commands/ask-question.ts +390 -0
- package/src/commands/btw.ts +164 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +190 -0
- package/src/commands/diff.ts +91 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +321 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +1173 -0
- package/src/commands/mcp.ts +307 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/commands/mention-mode.ts +68 -0
- package/src/commands/merge-worktree.ts +223 -0
- package/src/commands/model-variant.ts +483 -0
- package/src/commands/model.ts +1053 -0
- package/src/commands/new-worktree.ts +510 -0
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/permissions.ts +397 -0
- package/src/commands/queue.ts +271 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +230 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +366 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +227 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/tasks.ts +293 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +386 -0
- package/src/commands/unset-model.ts +173 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +198 -0
- package/src/commands/verbosity.ts +173 -0
- package/src/commands/worktree-settings.ts +70 -0
- package/src/commands/worktrees.ts +552 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +111 -0
- package/src/context-awareness-plugin.test.ts +142 -0
- package/src/context-awareness-plugin.ts +510 -0
- package/src/critique-utils.ts +139 -0
- package/src/database.ts +1876 -0
- package/src/db.test.ts +162 -0
- package/src/db.ts +286 -0
- package/src/debounce-timeout.ts +43 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +1330 -0
- package/src/discord-command-registration.ts +693 -0
- package/src/discord-urls.ts +88 -0
- package/src/discord-utils.test.ts +153 -0
- package/src/discord-utils.ts +800 -0
- package/src/errors.ts +201 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/event-stream-real-capture.e2e.test.ts +692 -0
- package/src/eventsource-parser.test.ts +351 -0
- package/src/exec-async.ts +35 -0
- package/src/external-opencode-sync.ts +685 -0
- package/src/format-tables.test.ts +335 -0
- package/src/format-tables.ts +445 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +172 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +640 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +114 -0
- package/src/generated/client.ts +138 -0
- package/src/generated/commonInputTypes.ts +736 -0
- package/src/generated/enums.ts +88 -0
- package/src/generated/internal/class.ts +384 -0
- package/src/generated/internal/prismaNamespace.ts +2386 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1656 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +1859 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1228 -0
- package/src/generated/models/channel_worktrees.ts +1300 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1781 -0
- package/src/generated/models/thread_worktrees.ts +1356 -0
- package/src/generated/models.ts +30 -0
- package/src/heap-monitor.ts +152 -0
- package/src/hrana-server.test.ts +434 -0
- package/src/hrana-server.ts +314 -0
- package/src/html-actions.test.ts +87 -0
- package/src/html-actions.ts +174 -0
- package/src/html-components.test.ts +38 -0
- package/src/html-components.ts +181 -0
- package/src/image-optimizer-plugin.ts +194 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +576 -0
- package/src/ipc-polling.ts +326 -0
- package/src/ipc-tools-plugin.ts +236 -0
- package/src/kimaki-digital-twin.e2e.test.ts +199 -0
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
- package/src/kimaki-opencode-plugin.test.ts +108 -0
- package/src/kimaki-opencode-plugin.ts +18 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +208 -0
- package/src/markdown.test.ts +308 -0
- package/src/markdown.ts +410 -0
- package/src/message-finish-field.e2e.test.ts +192 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +533 -0
- package/src/message-preprocessing.ts +455 -0
- package/src/onboarding-tutorial.ts +176 -0
- package/src/onboarding-welcome.ts +49 -0
- package/src/openai-realtime.ts +358 -0
- package/src/opencode-command-detection.test.ts +307 -0
- package/src/opencode-command-detection.ts +76 -0
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +677 -0
- package/src/opencode-interrupt-plugin.ts +477 -0
- package/src/opencode.ts +1110 -0
- package/src/otto/branding.ts +23 -0
- package/src/otto/index.ts +22 -0
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/patch-text-parser.ts +107 -0
- package/src/plugin-logger.ts +68 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +382 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +873 -0
- package/src/queue-advanced-footer.e2e.test.ts +576 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
- package/src/queue-advanced-question.e2e.test.ts +316 -0
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
- package/src/queue-advanced-typing.e2e.test.ts +199 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/queue-question-select-drain.e2e.test.ts +152 -0
- package/src/runtime-idle-sweeper.ts +76 -0
- package/src/runtime-lifecycle.e2e.test.ts +641 -0
- package/src/schema.sql +173 -0
- package/src/sentry.ts +26 -0
- package/src/session-handler/agent-utils.ts +97 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +645 -0
- package/src/session-handler/event-stream-state.ts +608 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +212 -0
- package/src/session-handler/thread-session-runtime.ts +4281 -0
- package/src/session-handler.ts +15 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/session-title-rename.test.ts +112 -0
- package/src/startup-service.ts +200 -0
- package/src/startup-time.e2e.test.ts +373 -0
- package/src/store.ts +122 -0
- package/src/system-message.test.ts +612 -0
- package/src/system-message.ts +723 -0
- package/src/task-runner.ts +421 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +311 -0
- package/src/test-utils.ts +435 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +1219 -0
- package/src/tools.ts +430 -0
- package/src/undici.d.ts +12 -0
- package/src/undo-redo.e2e.test.ts +209 -0
- package/src/unnest-code-blocks.test.ts +713 -0
- package/src/unnest-code-blocks.ts +185 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +212 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +908 -0
- package/src/voice-message.e2e.test.ts +1255 -0
- package/src/voice.test.ts +281 -0
- package/src/voice.ts +627 -0
- package/src/wait-session.ts +147 -0
- package/src/websockify.ts +101 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
- package/src/worktree-utils.ts +4 -0
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1294 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Tests for context-awareness directory switch reminders.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
shouldInjectPwd,
|
|
6
|
+
shouldInjectMemoryReminderFromLatestAssistant,
|
|
7
|
+
} from './context-awareness-plugin.js'
|
|
8
|
+
|
|
9
|
+
describe('shouldInjectPwd', () => {
|
|
10
|
+
test('does not inject when current directory matches announced directory', () => {
|
|
11
|
+
const result = shouldInjectPwd({
|
|
12
|
+
currentDir: '/repo/worktree',
|
|
13
|
+
previousDir: '/repo/main',
|
|
14
|
+
announcedDir: '/repo/worktree',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
expect(result).toMatchInlineSnapshot(`
|
|
18
|
+
{
|
|
19
|
+
"inject": false,
|
|
20
|
+
}
|
|
21
|
+
`)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('does not inject without a previous directory to warn about', () => {
|
|
25
|
+
const result = shouldInjectPwd({
|
|
26
|
+
currentDir: '/repo/worktree',
|
|
27
|
+
previousDir: undefined,
|
|
28
|
+
announcedDir: undefined,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(result).toMatchInlineSnapshot(`
|
|
32
|
+
{
|
|
33
|
+
"inject": false,
|
|
34
|
+
}
|
|
35
|
+
`)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('names previous and current directories in the correct order', () => {
|
|
39
|
+
const result = shouldInjectPwd({
|
|
40
|
+
currentDir: '/repo/worktree',
|
|
41
|
+
previousDir: '/repo/main',
|
|
42
|
+
announcedDir: undefined,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(result).toMatchInlineSnapshot(`
|
|
46
|
+
{
|
|
47
|
+
"inject": true,
|
|
48
|
+
"text": "
|
|
49
|
+
[working directory changed. Previous working directory: /repo/main. Current working directory: /repo/worktree. You MUST read, write, and edit files only under /repo/worktree. Do NOT read, write, or edit files under /repo/main.]",
|
|
50
|
+
}
|
|
51
|
+
`)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('prefers the last announced directory as the previous directory', () => {
|
|
55
|
+
const result = shouldInjectPwd({
|
|
56
|
+
currentDir: '/repo/worktree-b',
|
|
57
|
+
previousDir: '/repo/main',
|
|
58
|
+
announcedDir: '/repo/worktree-a',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result).toMatchInlineSnapshot(`
|
|
62
|
+
{
|
|
63
|
+
"inject": true,
|
|
64
|
+
"text": "
|
|
65
|
+
[working directory changed. Previous working directory: /repo/worktree-a. Current working directory: /repo/worktree-b. You MUST read, write, and edit files only under /repo/worktree-b. Do NOT read, write, or edit files under /repo/worktree-a.]",
|
|
66
|
+
}
|
|
67
|
+
`)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('shouldInjectMemoryReminderFromLatestAssistant', () => {
|
|
72
|
+
test('does not trigger before threshold', () => {
|
|
73
|
+
const result = shouldInjectMemoryReminderFromLatestAssistant({
|
|
74
|
+
latestAssistantMessage: {
|
|
75
|
+
id: 'msg_asst_1',
|
|
76
|
+
role: 'assistant',
|
|
77
|
+
time: { completed: 1 },
|
|
78
|
+
tokens: {
|
|
79
|
+
input: 1_000,
|
|
80
|
+
output: 3_000,
|
|
81
|
+
reasoning: 500,
|
|
82
|
+
cache: { read: 0, write: 0 },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
threshold: 10_000,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(result).toMatchInlineSnapshot(`
|
|
89
|
+
{
|
|
90
|
+
"inject": false,
|
|
91
|
+
}
|
|
92
|
+
`)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('triggers when latest assistant message exceeds threshold', () => {
|
|
96
|
+
const result = shouldInjectMemoryReminderFromLatestAssistant({
|
|
97
|
+
latestAssistantMessage: {
|
|
98
|
+
id: 'msg_asst_2',
|
|
99
|
+
role: 'assistant',
|
|
100
|
+
time: { completed: 2 },
|
|
101
|
+
tokens: {
|
|
102
|
+
input: 2_000,
|
|
103
|
+
output: 2_200,
|
|
104
|
+
reasoning: 400,
|
|
105
|
+
cache: { read: 0, write: 0 },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
threshold: 2_000,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(result).toMatchInlineSnapshot(`
|
|
112
|
+
{
|
|
113
|
+
"assistantMessageId": "msg_asst_2",
|
|
114
|
+
"inject": true,
|
|
115
|
+
}
|
|
116
|
+
`)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('does not trigger again for the same reminded assistant message', () => {
|
|
120
|
+
const result = shouldInjectMemoryReminderFromLatestAssistant({
|
|
121
|
+
lastMemoryReminderAssistantMessageId: 'msg_asst_3',
|
|
122
|
+
latestAssistantMessage: {
|
|
123
|
+
id: 'msg_asst_3',
|
|
124
|
+
role: 'assistant',
|
|
125
|
+
time: { completed: 3 },
|
|
126
|
+
tokens: {
|
|
127
|
+
input: 2_000,
|
|
128
|
+
output: 2_200,
|
|
129
|
+
reasoning: 400,
|
|
130
|
+
cache: { read: 0, write: 0 },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
threshold: 10_000,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(result).toMatchInlineSnapshot(`
|
|
137
|
+
{
|
|
138
|
+
"inject": false,
|
|
139
|
+
}
|
|
140
|
+
`)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,510 @@
|
|
|
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 table of contents on first message
|
|
5
|
+
// - MEMORY.md reminder after a large assistant reply
|
|
6
|
+
// - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
|
|
7
|
+
//
|
|
8
|
+
// Synthetic parts are hidden from the TUI but sent to the model, keeping it
|
|
9
|
+
// aware of context changes without cluttering the UI.
|
|
10
|
+
//
|
|
11
|
+
// State design: all per-session mutable state is encapsulated in a single
|
|
12
|
+
// SessionState object per session ID. One Map, one delete() on cleanup.
|
|
13
|
+
// Decision logic is extracted into pure functions that take state + input
|
|
14
|
+
// and return whether to inject — making them testable without mocking.
|
|
15
|
+
//
|
|
16
|
+
// Exported from kimaki-opencode-plugin.ts — each export is treated as a separate
|
|
17
|
+
// plugin by OpenCode's plugin loader.
|
|
18
|
+
|
|
19
|
+
import type { Plugin } from '@opencode-ai/plugin'
|
|
20
|
+
import crypto from 'node:crypto'
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
import * as errore from 'errore'
|
|
24
|
+
import {
|
|
25
|
+
createPluginLogger,
|
|
26
|
+
formatPluginErrorWithStack,
|
|
27
|
+
setPluginLogFilePath,
|
|
28
|
+
} from './plugin-logger.js'
|
|
29
|
+
import { setDataDir } from './config.js'
|
|
30
|
+
import { initSentry, notifyError } from './sentry.js'
|
|
31
|
+
import { execAsync } from './exec-async.js'
|
|
32
|
+
import { condenseMemoryMd } from './condense-memory.js'
|
|
33
|
+
import {
|
|
34
|
+
ONBOARDING_TUTORIAL_INSTRUCTIONS,
|
|
35
|
+
TUTORIAL_WELCOME_TEXT,
|
|
36
|
+
} from './onboarding-tutorial.js'
|
|
37
|
+
|
|
38
|
+
const logger = createPluginLogger('OPENCODE')
|
|
39
|
+
|
|
40
|
+
// ── Types ────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
type GitState = {
|
|
43
|
+
key: string
|
|
44
|
+
kind: 'branch' | 'detached-head' | 'detached-submodule'
|
|
45
|
+
label: string
|
|
46
|
+
warning: string | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// All per-session mutable state in one place. One Map entry, one delete.
|
|
50
|
+
type SessionState = {
|
|
51
|
+
gitState: GitState | undefined
|
|
52
|
+
memoryInjected: boolean
|
|
53
|
+
lastMemoryReminderAssistantMessageId: string | undefined
|
|
54
|
+
tutorialInjected: boolean
|
|
55
|
+
// Last directory observed via session.get(). Refreshed on each real user
|
|
56
|
+
// message so directory-change reminders compare the latest observed session
|
|
57
|
+
// directory against the current request directory.
|
|
58
|
+
resolvedDirectory: string | undefined
|
|
59
|
+
// Last directory we announced via pwd injection.
|
|
60
|
+
announcedDirectory: string | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createSessionState(): SessionState {
|
|
64
|
+
return {
|
|
65
|
+
gitState: undefined,
|
|
66
|
+
memoryInjected: false,
|
|
67
|
+
lastMemoryReminderAssistantMessageId: undefined,
|
|
68
|
+
tutorialInjected: false,
|
|
69
|
+
resolvedDirectory: undefined,
|
|
70
|
+
announcedDirectory: undefined,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Minimal type for the opencode plugin client (v1 SDK style with path objects).
|
|
75
|
+
type PluginClient = {
|
|
76
|
+
session: {
|
|
77
|
+
get: (params: { path: { id: string } }) => Promise<{ data?: { directory?: string } }>
|
|
78
|
+
messages: (params: {
|
|
79
|
+
path: { id: string }
|
|
80
|
+
query?: { directory?: string; limit?: number }
|
|
81
|
+
}) => Promise<{ data?: Array<{ info: AssistantMessageInfo }> }>
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Pure derivation functions ────────────────────────────────────
|
|
86
|
+
// These take state + fresh input and return whether to inject.
|
|
87
|
+
// No side effects, no mutations — easy to test with fixtures.
|
|
88
|
+
|
|
89
|
+
export function shouldInjectBranch({
|
|
90
|
+
previousGitState,
|
|
91
|
+
currentGitState,
|
|
92
|
+
}: {
|
|
93
|
+
previousGitState: GitState | undefined
|
|
94
|
+
currentGitState: GitState | null
|
|
95
|
+
}): { inject: false } | { inject: true; text: string } {
|
|
96
|
+
if (!currentGitState) {
|
|
97
|
+
return { inject: false }
|
|
98
|
+
}
|
|
99
|
+
if (previousGitState && previousGitState.key === currentGitState.key) {
|
|
100
|
+
return { inject: false }
|
|
101
|
+
}
|
|
102
|
+
const text = currentGitState.warning || `\n[current git branch is ${currentGitState.label}]`
|
|
103
|
+
return { inject: true, text }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function shouldInjectPwd({
|
|
107
|
+
currentDir,
|
|
108
|
+
previousDir,
|
|
109
|
+
announcedDir,
|
|
110
|
+
}: {
|
|
111
|
+
currentDir: string
|
|
112
|
+
previousDir: string | undefined
|
|
113
|
+
announcedDir: string | undefined
|
|
114
|
+
}): { inject: false } | { inject: true; text: string } {
|
|
115
|
+
if (announcedDir === currentDir) {
|
|
116
|
+
return { inject: false }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const priorDirectory = announcedDir || previousDir
|
|
120
|
+
if (!priorDirectory || priorDirectory === currentDir) {
|
|
121
|
+
return { inject: false }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
inject: true,
|
|
126
|
+
text:
|
|
127
|
+
`\n[working directory changed. Previous working directory: ${priorDirectory}. ` +
|
|
128
|
+
`Current working directory: ${currentDir}. ` +
|
|
129
|
+
`You MUST read, write, and edit files only under ${currentDir}. ` +
|
|
130
|
+
`Do NOT read, write, or edit files under ${priorDirectory}.]`,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000
|
|
135
|
+
|
|
136
|
+
type AssistantTokenUsage = {
|
|
137
|
+
input: number
|
|
138
|
+
output: number
|
|
139
|
+
reasoning: number
|
|
140
|
+
cache: { read: number; write: number }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type AssistantMessageInfo = {
|
|
144
|
+
id: string
|
|
145
|
+
role: string
|
|
146
|
+
time?: { completed?: number; created?: number }
|
|
147
|
+
tokens?: AssistantTokenUsage
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getOutputTokenTotal(tokens: AssistantTokenUsage): number {
|
|
151
|
+
return Math.max(0, tokens.output + tokens.reasoning)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function shouldInjectMemoryReminderFromLatestAssistant({
|
|
155
|
+
lastMemoryReminderAssistantMessageId,
|
|
156
|
+
latestAssistantMessage,
|
|
157
|
+
threshold = MEMORY_REMINDER_OUTPUT_TOKENS,
|
|
158
|
+
}: {
|
|
159
|
+
lastMemoryReminderAssistantMessageId?: string
|
|
160
|
+
latestAssistantMessage: AssistantMessageInfo | undefined
|
|
161
|
+
threshold?: number
|
|
162
|
+
}): { inject: false } | { inject: true; assistantMessageId: string } {
|
|
163
|
+
if (!latestAssistantMessage) {
|
|
164
|
+
return { inject: false }
|
|
165
|
+
}
|
|
166
|
+
if (latestAssistantMessage.role !== 'assistant') {
|
|
167
|
+
return { inject: false }
|
|
168
|
+
}
|
|
169
|
+
if (typeof latestAssistantMessage.time?.completed !== 'number') {
|
|
170
|
+
return { inject: false }
|
|
171
|
+
}
|
|
172
|
+
if (!latestAssistantMessage.tokens) {
|
|
173
|
+
return { inject: false }
|
|
174
|
+
}
|
|
175
|
+
if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
|
|
176
|
+
return { inject: false }
|
|
177
|
+
}
|
|
178
|
+
const outputTokens = getOutputTokenTotal(latestAssistantMessage.tokens)
|
|
179
|
+
if (outputTokens < threshold) {
|
|
180
|
+
return { inject: false }
|
|
181
|
+
}
|
|
182
|
+
return { inject: true, assistantMessageId: latestAssistantMessage.id }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function shouldInjectTutorial({
|
|
186
|
+
alreadyInjected,
|
|
187
|
+
parts,
|
|
188
|
+
}: {
|
|
189
|
+
alreadyInjected: boolean
|
|
190
|
+
parts: Array<{ type: string; text?: string }>
|
|
191
|
+
}): boolean {
|
|
192
|
+
if (alreadyInjected) {
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
return parts.some((part) => {
|
|
196
|
+
return part.type === 'text' && part.text?.includes(TUTORIAL_WELCOME_TEXT)
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Impure helpers (I/O) ─────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async function resolveGitState({
|
|
203
|
+
directory,
|
|
204
|
+
}: {
|
|
205
|
+
directory: string
|
|
206
|
+
}): Promise<GitState | null> {
|
|
207
|
+
const branchResult = await errore.tryAsync(() => {
|
|
208
|
+
return execAsync('git symbolic-ref --short HEAD', { cwd: directory })
|
|
209
|
+
})
|
|
210
|
+
if (!(branchResult instanceof Error)) {
|
|
211
|
+
const branch = branchResult.stdout.trim()
|
|
212
|
+
if (branch) {
|
|
213
|
+
return {
|
|
214
|
+
key: `branch:${branch}`,
|
|
215
|
+
kind: 'branch',
|
|
216
|
+
label: branch,
|
|
217
|
+
warning: null,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const shaResult = await errore.tryAsync(() => {
|
|
223
|
+
return execAsync('git rev-parse --short HEAD', { cwd: directory })
|
|
224
|
+
})
|
|
225
|
+
if (shaResult instanceof Error) {
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const shortSha = shaResult.stdout.trim()
|
|
230
|
+
if (!shortSha) {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const superprojectResult = await errore.tryAsync(() => {
|
|
235
|
+
return execAsync('git rev-parse --show-superproject-working-tree', {
|
|
236
|
+
cwd: directory,
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
const superproject =
|
|
240
|
+
superprojectResult instanceof Error ? '' : superprojectResult.stdout.trim()
|
|
241
|
+
if (superproject) {
|
|
242
|
+
return {
|
|
243
|
+
key: `detached-submodule:${shortSha}`,
|
|
244
|
+
kind: 'detached-submodule',
|
|
245
|
+
label: `detached submodule @ ${shortSha}`,
|
|
246
|
+
warning:
|
|
247
|
+
`\n[warning: submodule is in detached HEAD at ${shortSha}. ` +
|
|
248
|
+
'create or switch to a branch before committing.]',
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
key: `detached-head:${shortSha}`,
|
|
254
|
+
kind: 'detached-head',
|
|
255
|
+
label: `detached HEAD @ ${shortSha}`,
|
|
256
|
+
warning:
|
|
257
|
+
`\n[warning: repository is in detached HEAD at ${shortSha}. ` +
|
|
258
|
+
'create or switch to a branch before committing.]',
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Resolve the last observed session directory via the SDK.
|
|
263
|
+
// Refreshed on every real user message because sessions can switch directories
|
|
264
|
+
// mid-thread and the pwd reminder must compare old vs new accurately.
|
|
265
|
+
async function resolveSessionDirectory({
|
|
266
|
+
client,
|
|
267
|
+
sessionID,
|
|
268
|
+
state,
|
|
269
|
+
}: {
|
|
270
|
+
client: PluginClient
|
|
271
|
+
sessionID: string
|
|
272
|
+
state: SessionState
|
|
273
|
+
}): Promise<{
|
|
274
|
+
currentDirectory: string | null
|
|
275
|
+
previousDirectory: string | undefined
|
|
276
|
+
}> {
|
|
277
|
+
const previousDirectory = state.resolvedDirectory
|
|
278
|
+
const result = await errore.tryAsync(() => {
|
|
279
|
+
return client.session.get({ path: { id: sessionID } })
|
|
280
|
+
})
|
|
281
|
+
if (result instanceof Error || !result.data?.directory) {
|
|
282
|
+
return {
|
|
283
|
+
currentDirectory: previousDirectory || null,
|
|
284
|
+
previousDirectory,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
state.resolvedDirectory = result.data.directory
|
|
288
|
+
return {
|
|
289
|
+
currentDirectory: result.data.directory,
|
|
290
|
+
previousDirectory,
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Plugin ───────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
const contextAwarenessPlugin: Plugin = async ({ directory, client }) => {
|
|
297
|
+
initSentry()
|
|
298
|
+
|
|
299
|
+
const dataDir = process.env.KIMAKI_DATA_DIR
|
|
300
|
+
if (dataDir) {
|
|
301
|
+
setDataDir(dataDir)
|
|
302
|
+
setPluginLogFilePath(dataDir)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Single Map for all per-session state. One entry per session, one
|
|
306
|
+
// delete on cleanup — no parallel Maps that can drift out of sync.
|
|
307
|
+
const sessions = new Map<string, SessionState>()
|
|
308
|
+
|
|
309
|
+
function getOrCreateSession(sessionID: string): SessionState {
|
|
310
|
+
const existing = sessions.get(sessionID)
|
|
311
|
+
if (existing) {
|
|
312
|
+
return existing
|
|
313
|
+
}
|
|
314
|
+
const state = createSessionState()
|
|
315
|
+
sessions.set(sessionID, state)
|
|
316
|
+
return state
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
'chat.message': async (input, output) => {
|
|
321
|
+
const hookResult = await errore.tryAsync({
|
|
322
|
+
try: async () => {
|
|
323
|
+
const { sessionID } = input
|
|
324
|
+
const state = getOrCreateSession(sessionID)
|
|
325
|
+
|
|
326
|
+
// -- Onboarding tutorial injection --
|
|
327
|
+
// Runs before the non-synthetic text guard because the tutorial
|
|
328
|
+
// marker (TUTORIAL_WELCOME_TEXT) can appear in synthetic/system
|
|
329
|
+
// parts prepended by message-preprocessing.ts. The old separate
|
|
330
|
+
// plugin had no such guard, so this preserves that behavior.
|
|
331
|
+
const firstTextPart = output.parts.find((part) => {
|
|
332
|
+
return part.type === 'text'
|
|
333
|
+
})
|
|
334
|
+
if (firstTextPart && shouldInjectTutorial({ alreadyInjected: state.tutorialInjected, parts: output.parts })) {
|
|
335
|
+
state.tutorialInjected = true
|
|
336
|
+
output.parts.push({
|
|
337
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
338
|
+
sessionID,
|
|
339
|
+
messageID: firstTextPart.messageID,
|
|
340
|
+
type: 'text' as const,
|
|
341
|
+
text: `<system-reminder>\n${ONBOARDING_TUTORIAL_INSTRUCTIONS}\n</system-reminder>`,
|
|
342
|
+
synthetic: true,
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// -- Find first non-synthetic user text part --
|
|
347
|
+
// All remaining injections (branch, pwd, memory, time gap) only
|
|
348
|
+
// apply to real user messages, not empty or synthetic-only messages.
|
|
349
|
+
const first = output.parts.find((part) => {
|
|
350
|
+
if (part.type !== 'text') {
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
return part.synthetic !== true
|
|
354
|
+
})
|
|
355
|
+
if (!first || first.type !== 'text' || first.text.trim().length === 0) {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const messageID = first.messageID
|
|
360
|
+
|
|
361
|
+
const latestAssistantMessageResult = await errore.tryAsync(() => {
|
|
362
|
+
return client.session.messages({
|
|
363
|
+
path: { id: sessionID },
|
|
364
|
+
query: { directory, limit: 20 },
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
const latestAssistantMessage =
|
|
368
|
+
latestAssistantMessageResult instanceof Error
|
|
369
|
+
? undefined
|
|
370
|
+
: [...(latestAssistantMessageResult.data || [])]
|
|
371
|
+
.reverse()
|
|
372
|
+
.find((entry) => {
|
|
373
|
+
return entry.info.role === 'assistant'
|
|
374
|
+
})
|
|
375
|
+
?.info
|
|
376
|
+
|
|
377
|
+
// -- Resolve session working directory --
|
|
378
|
+
const sessionDirectory = await resolveSessionDirectory({
|
|
379
|
+
client,
|
|
380
|
+
sessionID,
|
|
381
|
+
state,
|
|
382
|
+
})
|
|
383
|
+
// The plugin request directory is the current directory Kimaki asked
|
|
384
|
+
// OpenCode to operate on for this message. Prefer it over session.get()
|
|
385
|
+
// when they disagree so reminders and MEMORY/branch context follow the
|
|
386
|
+
// new worktree immediately after a folder switch.
|
|
387
|
+
const effectiveDirectory = directory
|
|
388
|
+
|
|
389
|
+
// -- Branch / detached HEAD detection --
|
|
390
|
+
// Resolved early but injected last so it appears at the end of parts.
|
|
391
|
+
const gitState = await resolveGitState({ directory: effectiveDirectory })
|
|
392
|
+
|
|
393
|
+
// -- Working directory change detection --
|
|
394
|
+
const pwdResult = shouldInjectPwd({
|
|
395
|
+
currentDir: effectiveDirectory,
|
|
396
|
+
previousDir:
|
|
397
|
+
sessionDirectory.previousDirectory ||
|
|
398
|
+
(sessionDirectory.currentDirectory !== effectiveDirectory
|
|
399
|
+
? sessionDirectory.currentDirectory || undefined
|
|
400
|
+
: undefined),
|
|
401
|
+
announcedDir: state.announcedDirectory,
|
|
402
|
+
})
|
|
403
|
+
if (pwdResult.inject) {
|
|
404
|
+
state.announcedDirectory = effectiveDirectory
|
|
405
|
+
output.parts.push({
|
|
406
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
407
|
+
sessionID,
|
|
408
|
+
messageID,
|
|
409
|
+
type: 'text' as const,
|
|
410
|
+
text: pwdResult.text,
|
|
411
|
+
synthetic: true,
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// -- MEMORY.md injection --
|
|
416
|
+
if (!state.memoryInjected) {
|
|
417
|
+
state.memoryInjected = true
|
|
418
|
+
const memoryPath = path.join(effectiveDirectory, 'MEMORY.md')
|
|
419
|
+
const memoryContent = await fs.promises
|
|
420
|
+
.readFile(memoryPath, 'utf-8')
|
|
421
|
+
.catch(() => null)
|
|
422
|
+
if (memoryContent) {
|
|
423
|
+
const condensed = condenseMemoryMd(memoryContent)
|
|
424
|
+
output.parts.push({
|
|
425
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
426
|
+
sessionID,
|
|
427
|
+
messageID,
|
|
428
|
+
type: 'text' as const,
|
|
429
|
+
text: `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`,
|
|
430
|
+
synthetic: true,
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
|
|
436
|
+
lastMemoryReminderAssistantMessageId:
|
|
437
|
+
state.lastMemoryReminderAssistantMessageId,
|
|
438
|
+
latestAssistantMessage,
|
|
439
|
+
})
|
|
440
|
+
if (memoryReminder.inject) {
|
|
441
|
+
output.parts.push({
|
|
442
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
443
|
+
sessionID,
|
|
444
|
+
messageID,
|
|
445
|
+
type: 'text' as const,
|
|
446
|
+
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>',
|
|
447
|
+
synthetic: true,
|
|
448
|
+
})
|
|
449
|
+
state.lastMemoryReminderAssistantMessageId =
|
|
450
|
+
memoryReminder.assistantMessageId
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// -- Branch injection (last synthetic part) --
|
|
454
|
+
const branchResult = shouldInjectBranch({
|
|
455
|
+
previousGitState: state.gitState,
|
|
456
|
+
currentGitState: gitState,
|
|
457
|
+
})
|
|
458
|
+
if (branchResult.inject) {
|
|
459
|
+
state.gitState = gitState!
|
|
460
|
+
output.parts.push({
|
|
461
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
462
|
+
sessionID,
|
|
463
|
+
messageID,
|
|
464
|
+
type: 'text' as const,
|
|
465
|
+
text: branchResult.text,
|
|
466
|
+
synthetic: true,
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
catch: (error) => {
|
|
471
|
+
return new Error('context-awareness chat.message hook failed', { cause: error })
|
|
472
|
+
},
|
|
473
|
+
})
|
|
474
|
+
if (hookResult instanceof Error) {
|
|
475
|
+
logger.warn(
|
|
476
|
+
`[context-awareness-plugin] ${formatPluginErrorWithStack(hookResult)}`,
|
|
477
|
+
)
|
|
478
|
+
void notifyError(hookResult, 'context-awareness plugin chat.message hook failed')
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
// Clean up per-session state when sessions are deleted.
|
|
483
|
+
// Single delete instead of parallel Map/Set deletes.
|
|
484
|
+
event: async ({ event }) => {
|
|
485
|
+
const cleanupResult = await errore.tryAsync({
|
|
486
|
+
try: async () => {
|
|
487
|
+
if (event.type !== 'session.deleted') {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
const id = event.properties?.info?.id
|
|
491
|
+
if (!id) {
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
sessions.delete(id)
|
|
495
|
+
},
|
|
496
|
+
catch: (error) => {
|
|
497
|
+
return new Error('context-awareness event hook failed', { cause: error })
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
if (cleanupResult instanceof Error) {
|
|
501
|
+
logger.warn(
|
|
502
|
+
`[context-awareness-plugin] ${formatPluginErrorWithStack(cleanupResult)}`,
|
|
503
|
+
)
|
|
504
|
+
void notifyError(cleanupResult, 'context-awareness plugin event hook failed')
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export { contextAwarenessPlugin }
|