@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,363 @@
|
|
|
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
|
+
import crypto from 'node:crypto';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import * as errore from 'errore';
|
|
22
|
+
import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
|
|
23
|
+
import { setDataDir } from './config.js';
|
|
24
|
+
import { initSentry, notifyError } from './sentry.js';
|
|
25
|
+
import { execAsync } from './exec-async.js';
|
|
26
|
+
import { condenseMemoryMd } from './condense-memory.js';
|
|
27
|
+
import { ONBOARDING_TUTORIAL_INSTRUCTIONS, TUTORIAL_WELCOME_TEXT, } from './onboarding-tutorial.js';
|
|
28
|
+
const logger = createPluginLogger('OPENCODE');
|
|
29
|
+
function createSessionState() {
|
|
30
|
+
return {
|
|
31
|
+
gitState: undefined,
|
|
32
|
+
memoryInjected: false,
|
|
33
|
+
lastMemoryReminderAssistantMessageId: undefined,
|
|
34
|
+
tutorialInjected: false,
|
|
35
|
+
resolvedDirectory: undefined,
|
|
36
|
+
announcedDirectory: undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// ── Pure derivation functions ────────────────────────────────────
|
|
40
|
+
// These take state + fresh input and return whether to inject.
|
|
41
|
+
// No side effects, no mutations — easy to test with fixtures.
|
|
42
|
+
export function shouldInjectBranch({ previousGitState, currentGitState, }) {
|
|
43
|
+
if (!currentGitState) {
|
|
44
|
+
return { inject: false };
|
|
45
|
+
}
|
|
46
|
+
if (previousGitState && previousGitState.key === currentGitState.key) {
|
|
47
|
+
return { inject: false };
|
|
48
|
+
}
|
|
49
|
+
const text = currentGitState.warning || `\n[current git branch is ${currentGitState.label}]`;
|
|
50
|
+
return { inject: true, text };
|
|
51
|
+
}
|
|
52
|
+
export function shouldInjectPwd({ currentDir, previousDir, announcedDir, }) {
|
|
53
|
+
if (announcedDir === currentDir) {
|
|
54
|
+
return { inject: false };
|
|
55
|
+
}
|
|
56
|
+
const priorDirectory = announcedDir || previousDir;
|
|
57
|
+
if (!priorDirectory || priorDirectory === currentDir) {
|
|
58
|
+
return { inject: false };
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
inject: true,
|
|
62
|
+
text: `\n[working directory changed. Previous working directory: ${priorDirectory}. ` +
|
|
63
|
+
`Current working directory: ${currentDir}. ` +
|
|
64
|
+
`You MUST read, write, and edit files only under ${currentDir}. ` +
|
|
65
|
+
`Do NOT read, write, or edit files under ${priorDirectory}.]`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000;
|
|
69
|
+
function getOutputTokenTotal(tokens) {
|
|
70
|
+
return Math.max(0, tokens.output + tokens.reasoning);
|
|
71
|
+
}
|
|
72
|
+
export function shouldInjectMemoryReminderFromLatestAssistant({ lastMemoryReminderAssistantMessageId, latestAssistantMessage, threshold = MEMORY_REMINDER_OUTPUT_TOKENS, }) {
|
|
73
|
+
if (!latestAssistantMessage) {
|
|
74
|
+
return { inject: false };
|
|
75
|
+
}
|
|
76
|
+
if (latestAssistantMessage.role !== 'assistant') {
|
|
77
|
+
return { inject: false };
|
|
78
|
+
}
|
|
79
|
+
if (typeof latestAssistantMessage.time?.completed !== 'number') {
|
|
80
|
+
return { inject: false };
|
|
81
|
+
}
|
|
82
|
+
if (!latestAssistantMessage.tokens) {
|
|
83
|
+
return { inject: false };
|
|
84
|
+
}
|
|
85
|
+
if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
|
|
86
|
+
return { inject: false };
|
|
87
|
+
}
|
|
88
|
+
const outputTokens = getOutputTokenTotal(latestAssistantMessage.tokens);
|
|
89
|
+
if (outputTokens < threshold) {
|
|
90
|
+
return { inject: false };
|
|
91
|
+
}
|
|
92
|
+
return { inject: true, assistantMessageId: latestAssistantMessage.id };
|
|
93
|
+
}
|
|
94
|
+
export function shouldInjectTutorial({ alreadyInjected, parts, }) {
|
|
95
|
+
if (alreadyInjected) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return parts.some((part) => {
|
|
99
|
+
return part.type === 'text' && part.text?.includes(TUTORIAL_WELCOME_TEXT);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// ── Impure helpers (I/O) ─────────────────────────────────────────
|
|
103
|
+
async function resolveGitState({ directory, }) {
|
|
104
|
+
const branchResult = await errore.tryAsync(() => {
|
|
105
|
+
return execAsync('git symbolic-ref --short HEAD', { cwd: directory });
|
|
106
|
+
});
|
|
107
|
+
if (!(branchResult instanceof Error)) {
|
|
108
|
+
const branch = branchResult.stdout.trim();
|
|
109
|
+
if (branch) {
|
|
110
|
+
return {
|
|
111
|
+
key: `branch:${branch}`,
|
|
112
|
+
kind: 'branch',
|
|
113
|
+
label: branch,
|
|
114
|
+
warning: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const shaResult = await errore.tryAsync(() => {
|
|
119
|
+
return execAsync('git rev-parse --short HEAD', { cwd: directory });
|
|
120
|
+
});
|
|
121
|
+
if (shaResult instanceof Error) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const shortSha = shaResult.stdout.trim();
|
|
125
|
+
if (!shortSha) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const superprojectResult = await errore.tryAsync(() => {
|
|
129
|
+
return execAsync('git rev-parse --show-superproject-working-tree', {
|
|
130
|
+
cwd: directory,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
const superproject = superprojectResult instanceof Error ? '' : superprojectResult.stdout.trim();
|
|
134
|
+
if (superproject) {
|
|
135
|
+
return {
|
|
136
|
+
key: `detached-submodule:${shortSha}`,
|
|
137
|
+
kind: 'detached-submodule',
|
|
138
|
+
label: `detached submodule @ ${shortSha}`,
|
|
139
|
+
warning: `\n[warning: submodule is in detached HEAD at ${shortSha}. ` +
|
|
140
|
+
'create or switch to a branch before committing.]',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
key: `detached-head:${shortSha}`,
|
|
145
|
+
kind: 'detached-head',
|
|
146
|
+
label: `detached HEAD @ ${shortSha}`,
|
|
147
|
+
warning: `\n[warning: repository is in detached HEAD at ${shortSha}. ` +
|
|
148
|
+
'create or switch to a branch before committing.]',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Resolve the last observed session directory via the SDK.
|
|
152
|
+
// Refreshed on every real user message because sessions can switch directories
|
|
153
|
+
// mid-thread and the pwd reminder must compare old vs new accurately.
|
|
154
|
+
async function resolveSessionDirectory({ client, sessionID, state, }) {
|
|
155
|
+
const previousDirectory = state.resolvedDirectory;
|
|
156
|
+
const result = await errore.tryAsync(() => {
|
|
157
|
+
return client.session.get({ path: { id: sessionID } });
|
|
158
|
+
});
|
|
159
|
+
if (result instanceof Error || !result.data?.directory) {
|
|
160
|
+
return {
|
|
161
|
+
currentDirectory: previousDirectory || null,
|
|
162
|
+
previousDirectory,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
state.resolvedDirectory = result.data.directory;
|
|
166
|
+
return {
|
|
167
|
+
currentDirectory: result.data.directory,
|
|
168
|
+
previousDirectory,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// ── Plugin ───────────────────────────────────────────────────────
|
|
172
|
+
const contextAwarenessPlugin = async ({ directory, client }) => {
|
|
173
|
+
initSentry();
|
|
174
|
+
const dataDir = process.env.KIMAKI_DATA_DIR;
|
|
175
|
+
if (dataDir) {
|
|
176
|
+
setDataDir(dataDir);
|
|
177
|
+
setPluginLogFilePath(dataDir);
|
|
178
|
+
}
|
|
179
|
+
// Single Map for all per-session state. One entry per session, one
|
|
180
|
+
// delete on cleanup — no parallel Maps that can drift out of sync.
|
|
181
|
+
const sessions = new Map();
|
|
182
|
+
function getOrCreateSession(sessionID) {
|
|
183
|
+
const existing = sessions.get(sessionID);
|
|
184
|
+
if (existing) {
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
|
187
|
+
const state = createSessionState();
|
|
188
|
+
sessions.set(sessionID, state);
|
|
189
|
+
return state;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
'chat.message': async (input, output) => {
|
|
193
|
+
const hookResult = await errore.tryAsync({
|
|
194
|
+
try: async () => {
|
|
195
|
+
const { sessionID } = input;
|
|
196
|
+
const state = getOrCreateSession(sessionID);
|
|
197
|
+
// -- Onboarding tutorial injection --
|
|
198
|
+
// Runs before the non-synthetic text guard because the tutorial
|
|
199
|
+
// marker (TUTORIAL_WELCOME_TEXT) can appear in synthetic/system
|
|
200
|
+
// parts prepended by message-preprocessing.ts. The old separate
|
|
201
|
+
// plugin had no such guard, so this preserves that behavior.
|
|
202
|
+
const firstTextPart = output.parts.find((part) => {
|
|
203
|
+
return part.type === 'text';
|
|
204
|
+
});
|
|
205
|
+
if (firstTextPart && shouldInjectTutorial({ alreadyInjected: state.tutorialInjected, parts: output.parts })) {
|
|
206
|
+
state.tutorialInjected = true;
|
|
207
|
+
output.parts.push({
|
|
208
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
209
|
+
sessionID,
|
|
210
|
+
messageID: firstTextPart.messageID,
|
|
211
|
+
type: 'text',
|
|
212
|
+
text: `<system-reminder>\n${ONBOARDING_TUTORIAL_INSTRUCTIONS}\n</system-reminder>`,
|
|
213
|
+
synthetic: true,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// -- Find first non-synthetic user text part --
|
|
217
|
+
// All remaining injections (branch, pwd, memory, time gap) only
|
|
218
|
+
// apply to real user messages, not empty or synthetic-only messages.
|
|
219
|
+
const first = output.parts.find((part) => {
|
|
220
|
+
if (part.type !== 'text') {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return part.synthetic !== true;
|
|
224
|
+
});
|
|
225
|
+
if (!first || first.type !== 'text' || first.text.trim().length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const messageID = first.messageID;
|
|
229
|
+
const latestAssistantMessageResult = await errore.tryAsync(() => {
|
|
230
|
+
return client.session.messages({
|
|
231
|
+
path: { id: sessionID },
|
|
232
|
+
query: { directory, limit: 20 },
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
const latestAssistantMessage = latestAssistantMessageResult instanceof Error
|
|
236
|
+
? undefined
|
|
237
|
+
: [...(latestAssistantMessageResult.data || [])]
|
|
238
|
+
.reverse()
|
|
239
|
+
.find((entry) => {
|
|
240
|
+
return entry.info.role === 'assistant';
|
|
241
|
+
})
|
|
242
|
+
?.info;
|
|
243
|
+
// -- Resolve session working directory --
|
|
244
|
+
const sessionDirectory = await resolveSessionDirectory({
|
|
245
|
+
client,
|
|
246
|
+
sessionID,
|
|
247
|
+
state,
|
|
248
|
+
});
|
|
249
|
+
// The plugin request directory is the current directory Kimaki asked
|
|
250
|
+
// OpenCode to operate on for this message. Prefer it over session.get()
|
|
251
|
+
// when they disagree so reminders and MEMORY/branch context follow the
|
|
252
|
+
// new worktree immediately after a folder switch.
|
|
253
|
+
const effectiveDirectory = directory;
|
|
254
|
+
// -- Branch / detached HEAD detection --
|
|
255
|
+
// Resolved early but injected last so it appears at the end of parts.
|
|
256
|
+
const gitState = await resolveGitState({ directory: effectiveDirectory });
|
|
257
|
+
// -- Working directory change detection --
|
|
258
|
+
const pwdResult = shouldInjectPwd({
|
|
259
|
+
currentDir: effectiveDirectory,
|
|
260
|
+
previousDir: sessionDirectory.previousDirectory ||
|
|
261
|
+
(sessionDirectory.currentDirectory !== effectiveDirectory
|
|
262
|
+
? sessionDirectory.currentDirectory || undefined
|
|
263
|
+
: undefined),
|
|
264
|
+
announcedDir: state.announcedDirectory,
|
|
265
|
+
});
|
|
266
|
+
if (pwdResult.inject) {
|
|
267
|
+
state.announcedDirectory = effectiveDirectory;
|
|
268
|
+
output.parts.push({
|
|
269
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
270
|
+
sessionID,
|
|
271
|
+
messageID,
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: pwdResult.text,
|
|
274
|
+
synthetic: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// -- MEMORY.md injection --
|
|
278
|
+
if (!state.memoryInjected) {
|
|
279
|
+
state.memoryInjected = true;
|
|
280
|
+
const memoryPath = path.join(effectiveDirectory, 'MEMORY.md');
|
|
281
|
+
const memoryContent = await fs.promises
|
|
282
|
+
.readFile(memoryPath, 'utf-8')
|
|
283
|
+
.catch(() => null);
|
|
284
|
+
if (memoryContent) {
|
|
285
|
+
const condensed = condenseMemoryMd(memoryContent);
|
|
286
|
+
output.parts.push({
|
|
287
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
288
|
+
sessionID,
|
|
289
|
+
messageID,
|
|
290
|
+
type: 'text',
|
|
291
|
+
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>`,
|
|
292
|
+
synthetic: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
|
|
297
|
+
lastMemoryReminderAssistantMessageId: state.lastMemoryReminderAssistantMessageId,
|
|
298
|
+
latestAssistantMessage,
|
|
299
|
+
});
|
|
300
|
+
if (memoryReminder.inject) {
|
|
301
|
+
output.parts.push({
|
|
302
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
303
|
+
sessionID,
|
|
304
|
+
messageID,
|
|
305
|
+
type: 'text',
|
|
306
|
+
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>',
|
|
307
|
+
synthetic: true,
|
|
308
|
+
});
|
|
309
|
+
state.lastMemoryReminderAssistantMessageId =
|
|
310
|
+
memoryReminder.assistantMessageId;
|
|
311
|
+
}
|
|
312
|
+
// -- Branch injection (last synthetic part) --
|
|
313
|
+
const branchResult = shouldInjectBranch({
|
|
314
|
+
previousGitState: state.gitState,
|
|
315
|
+
currentGitState: gitState,
|
|
316
|
+
});
|
|
317
|
+
if (branchResult.inject) {
|
|
318
|
+
state.gitState = gitState;
|
|
319
|
+
output.parts.push({
|
|
320
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
321
|
+
sessionID,
|
|
322
|
+
messageID,
|
|
323
|
+
type: 'text',
|
|
324
|
+
text: branchResult.text,
|
|
325
|
+
synthetic: true,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
catch: (error) => {
|
|
330
|
+
return new Error('context-awareness chat.message hook failed', { cause: error });
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
if (hookResult instanceof Error) {
|
|
334
|
+
logger.warn(`[context-awareness-plugin] ${formatPluginErrorWithStack(hookResult)}`);
|
|
335
|
+
void notifyError(hookResult, 'context-awareness plugin chat.message hook failed');
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
// Clean up per-session state when sessions are deleted.
|
|
339
|
+
// Single delete instead of parallel Map/Set deletes.
|
|
340
|
+
event: async ({ event }) => {
|
|
341
|
+
const cleanupResult = await errore.tryAsync({
|
|
342
|
+
try: async () => {
|
|
343
|
+
if (event.type !== 'session.deleted') {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const id = event.properties?.info?.id;
|
|
347
|
+
if (!id) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
sessions.delete(id);
|
|
351
|
+
},
|
|
352
|
+
catch: (error) => {
|
|
353
|
+
return new Error('context-awareness event hook failed', { cause: error });
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
if (cleanupResult instanceof Error) {
|
|
357
|
+
logger.warn(`[context-awareness-plugin] ${formatPluginErrorWithStack(cleanupResult)}`);
|
|
358
|
+
void notifyError(cleanupResult, 'context-awareness plugin event hook failed');
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
export { contextAwarenessPlugin };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Tests for context-awareness directory switch reminders.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { shouldInjectPwd, shouldInjectMemoryReminderFromLatestAssistant, } from './context-awareness-plugin.js';
|
|
4
|
+
describe('shouldInjectPwd', () => {
|
|
5
|
+
test('does not inject when current directory matches announced directory', () => {
|
|
6
|
+
const result = shouldInjectPwd({
|
|
7
|
+
currentDir: '/repo/worktree',
|
|
8
|
+
previousDir: '/repo/main',
|
|
9
|
+
announcedDir: '/repo/worktree',
|
|
10
|
+
});
|
|
11
|
+
expect(result).toMatchInlineSnapshot(`
|
|
12
|
+
{
|
|
13
|
+
"inject": false,
|
|
14
|
+
}
|
|
15
|
+
`);
|
|
16
|
+
});
|
|
17
|
+
test('does not inject without a previous directory to warn about', () => {
|
|
18
|
+
const result = shouldInjectPwd({
|
|
19
|
+
currentDir: '/repo/worktree',
|
|
20
|
+
previousDir: undefined,
|
|
21
|
+
announcedDir: undefined,
|
|
22
|
+
});
|
|
23
|
+
expect(result).toMatchInlineSnapshot(`
|
|
24
|
+
{
|
|
25
|
+
"inject": false,
|
|
26
|
+
}
|
|
27
|
+
`);
|
|
28
|
+
});
|
|
29
|
+
test('names previous and current directories in the correct order', () => {
|
|
30
|
+
const result = shouldInjectPwd({
|
|
31
|
+
currentDir: '/repo/worktree',
|
|
32
|
+
previousDir: '/repo/main',
|
|
33
|
+
announcedDir: undefined,
|
|
34
|
+
});
|
|
35
|
+
expect(result).toMatchInlineSnapshot(`
|
|
36
|
+
{
|
|
37
|
+
"inject": true,
|
|
38
|
+
"text": "
|
|
39
|
+
[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.]",
|
|
40
|
+
}
|
|
41
|
+
`);
|
|
42
|
+
});
|
|
43
|
+
test('prefers the last announced directory as the previous directory', () => {
|
|
44
|
+
const result = shouldInjectPwd({
|
|
45
|
+
currentDir: '/repo/worktree-b',
|
|
46
|
+
previousDir: '/repo/main',
|
|
47
|
+
announcedDir: '/repo/worktree-a',
|
|
48
|
+
});
|
|
49
|
+
expect(result).toMatchInlineSnapshot(`
|
|
50
|
+
{
|
|
51
|
+
"inject": true,
|
|
52
|
+
"text": "
|
|
53
|
+
[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.]",
|
|
54
|
+
}
|
|
55
|
+
`);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('shouldInjectMemoryReminderFromLatestAssistant', () => {
|
|
59
|
+
test('does not trigger before threshold', () => {
|
|
60
|
+
const result = shouldInjectMemoryReminderFromLatestAssistant({
|
|
61
|
+
latestAssistantMessage: {
|
|
62
|
+
id: 'msg_asst_1',
|
|
63
|
+
role: 'assistant',
|
|
64
|
+
time: { completed: 1 },
|
|
65
|
+
tokens: {
|
|
66
|
+
input: 1_000,
|
|
67
|
+
output: 3_000,
|
|
68
|
+
reasoning: 500,
|
|
69
|
+
cache: { read: 0, write: 0 },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
threshold: 10_000,
|
|
73
|
+
});
|
|
74
|
+
expect(result).toMatchInlineSnapshot(`
|
|
75
|
+
{
|
|
76
|
+
"inject": false,
|
|
77
|
+
}
|
|
78
|
+
`);
|
|
79
|
+
});
|
|
80
|
+
test('triggers when latest assistant message exceeds threshold', () => {
|
|
81
|
+
const result = shouldInjectMemoryReminderFromLatestAssistant({
|
|
82
|
+
latestAssistantMessage: {
|
|
83
|
+
id: 'msg_asst_2',
|
|
84
|
+
role: 'assistant',
|
|
85
|
+
time: { completed: 2 },
|
|
86
|
+
tokens: {
|
|
87
|
+
input: 2_000,
|
|
88
|
+
output: 2_200,
|
|
89
|
+
reasoning: 400,
|
|
90
|
+
cache: { read: 0, write: 0 },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
threshold: 2_000,
|
|
94
|
+
});
|
|
95
|
+
expect(result).toMatchInlineSnapshot(`
|
|
96
|
+
{
|
|
97
|
+
"assistantMessageId": "msg_asst_2",
|
|
98
|
+
"inject": true,
|
|
99
|
+
}
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
test('does not trigger again for the same reminded assistant message', () => {
|
|
103
|
+
const result = shouldInjectMemoryReminderFromLatestAssistant({
|
|
104
|
+
lastMemoryReminderAssistantMessageId: 'msg_asst_3',
|
|
105
|
+
latestAssistantMessage: {
|
|
106
|
+
id: 'msg_asst_3',
|
|
107
|
+
role: 'assistant',
|
|
108
|
+
time: { completed: 3 },
|
|
109
|
+
tokens: {
|
|
110
|
+
input: 2_000,
|
|
111
|
+
output: 2_200,
|
|
112
|
+
reasoning: 400,
|
|
113
|
+
cache: { read: 0, write: 0 },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
threshold: 10_000,
|
|
117
|
+
});
|
|
118
|
+
expect(result).toMatchInlineSnapshot(`
|
|
119
|
+
{
|
|
120
|
+
"inject": false,
|
|
121
|
+
}
|
|
122
|
+
`);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Shared utilities for invoking the critique CLI and parsing its JSON output.
|
|
2
|
+
// Used by /diff command and footer diff link uploads.
|
|
3
|
+
import { execAsync } from './worktrees.js';
|
|
4
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
5
|
+
const logger = createLogger(LogPrefix.DIFF);
|
|
6
|
+
const CRITIQUE_TIMEOUT_MS = 30_000;
|
|
7
|
+
/**
|
|
8
|
+
* Shell-quote a string by wrapping in single quotes and escaping embedded
|
|
9
|
+
* single quotes. Prevents injection when interpolating into shell commands.
|
|
10
|
+
*/
|
|
11
|
+
function shellQuote(s) {
|
|
12
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse critique --json output. Critique prints progress to stderr and JSON
|
|
16
|
+
* to stdout. The JSON line contains { url, id } on success or { error } on
|
|
17
|
+
* failure. We scan all lines for the first valid JSON object with a url or
|
|
18
|
+
* error field, falling back to searching for a critique.work URL in the raw
|
|
19
|
+
* output.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCritiqueOutput(output) {
|
|
22
|
+
const lines = output.trim().split('\n');
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (!line.startsWith('{')) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(line);
|
|
29
|
+
if (parsed.error) {
|
|
30
|
+
return { error: parsed.error };
|
|
31
|
+
}
|
|
32
|
+
if (parsed.url && parsed.id) {
|
|
33
|
+
return { url: parsed.url, id: parsed.id };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// not valid JSON, try next line
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Fallback: try to find a URL in the raw output
|
|
41
|
+
const urlMatch = output.match(/https?:\/\/critique\.work\/[^\s]+/);
|
|
42
|
+
if (urlMatch) {
|
|
43
|
+
const url = urlMatch[0];
|
|
44
|
+
// Extract ID from URL path: /v/{id}
|
|
45
|
+
const idMatch = url.match(/\/v\/([a-f0-9]+)/);
|
|
46
|
+
const id = idMatch?.[1];
|
|
47
|
+
if (id) {
|
|
48
|
+
return { url, id };
|
|
49
|
+
}
|
|
50
|
+
// URL without parseable id — return as error so callers don't build
|
|
51
|
+
// broken OG image URLs from an empty id
|
|
52
|
+
return { error: url };
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run critique on the current git working tree diff and return the result.
|
|
58
|
+
* Used by the /diff slash command.
|
|
59
|
+
*/
|
|
60
|
+
export async function uploadGitDiffViaCritique({ title, cwd, }) {
|
|
61
|
+
try {
|
|
62
|
+
const { stdout, stderr } = await execAsync(`critique --web ${shellQuote(title)} --json`, { cwd, timeout: CRITIQUE_TIMEOUT_MS });
|
|
63
|
+
return parseCritiqueOutput(stdout || stderr);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// exec error includes stdout/stderr — try to parse JSON from it
|
|
67
|
+
const execError = error;
|
|
68
|
+
const output = execError.stdout || execError.stderr || '';
|
|
69
|
+
const parsed = parseCritiqueOutput(output);
|
|
70
|
+
if (parsed) {
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
const message = execError.message || 'Unknown error';
|
|
74
|
+
if (message.includes('command not found') || message.includes('ENOENT')) {
|
|
75
|
+
return { error: 'critique not available' };
|
|
76
|
+
}
|
|
77
|
+
return { error: `Failed to generate diff: ${message.slice(0, 200)}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Upload a .patch file to critique.work via critique --stdin.
|
|
82
|
+
* Returns the critique URL on success, undefined on failure.
|
|
83
|
+
* Default timeout is 10s since this runs in the background (footer edit).
|
|
84
|
+
*/
|
|
85
|
+
export async function uploadPatchViaCritique({ patchPath, title, cwd, timeoutMs = 10_000, }) {
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await execAsync(`critique --stdin --web ${shellQuote(title)} --json < ${shellQuote(patchPath)}`, { cwd, timeout: timeoutMs });
|
|
88
|
+
const result = parseCritiqueOutput(stdout);
|
|
89
|
+
return result?.url;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.error('critique upload failed:', error);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|