@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,193 @@
|
|
|
1
|
+
// OpenCode plugin that provides IPC-based tools for Discord interaction:
|
|
2
|
+
// - kimaki_file_upload: prompts the Discord user to upload files via native picker
|
|
3
|
+
// - kimaki_action_buttons: shows clickable action buttons in the Discord thread
|
|
4
|
+
//
|
|
5
|
+
// Tools communicate with the bot process via IPC rows in SQLite (the plugin
|
|
6
|
+
// runs inside the OpenCode server process, not the bot process).
|
|
7
|
+
//
|
|
8
|
+
// Exported from kimaki-opencode-plugin.ts — each export is treated as a separate
|
|
9
|
+
// plugin by OpenCode's plugin loader.
|
|
10
|
+
import dedent from 'string-dedent';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { setDataDir } from './config.js';
|
|
13
|
+
import { createPluginLogger, setPluginLogFilePath } from './plugin-logger.js';
|
|
14
|
+
import { initSentry } from './sentry.js';
|
|
15
|
+
// Inlined from '@opencode-ai/plugin/tool' because the subpath value import
|
|
16
|
+
// fails at runtime in global npm installs (#35). Opencode loads this plugin
|
|
17
|
+
// file in its own process and resolves modules from kimaki's install dir,
|
|
18
|
+
// but the '/tool' subpath export isn't found by opencode's module resolver.
|
|
19
|
+
// The type-only imports above are fine (erased at compile time).
|
|
20
|
+
//
|
|
21
|
+
// NOTE: @opencode-ai/plugin bundles its own zod 4.1.x as a hard dependency
|
|
22
|
+
// while goke (used by cli.ts) requires zod 4.3.x. This version skew makes
|
|
23
|
+
// the Plugin return type structurally incompatible with our local tool()
|
|
24
|
+
// even though runtime behavior is identical. ipcToolsPlugin is cast to
|
|
25
|
+
// Plugin via unknown to bypass this purely type-level incompatibility.
|
|
26
|
+
function tool(input) {
|
|
27
|
+
return input;
|
|
28
|
+
}
|
|
29
|
+
const logger = createPluginLogger('OPENCODE');
|
|
30
|
+
const FILE_UPLOAD_TIMEOUT_MS = 6 * 60 * 1000;
|
|
31
|
+
const DEFAULT_FILE_UPLOAD_MAX_FILES = 5;
|
|
32
|
+
const ACTION_BUTTON_TIMEOUT_MS = 30 * 1000;
|
|
33
|
+
async function loadDatabaseModule() {
|
|
34
|
+
// The plugin-loading e2e test boots OpenCode directly without the bot-side
|
|
35
|
+
// Hrana env vars. Lazy-loading avoids pulling Prisma + libsql sqlite mode
|
|
36
|
+
// during plugin startup when no IPC tool is being executed yet.
|
|
37
|
+
return import('./database.js');
|
|
38
|
+
}
|
|
39
|
+
// @opencode-ai/plugin bundles zod 4.1.x as a hard dep; our code uses 4.3.x
|
|
40
|
+
// (required by goke for ~standard.jsonSchema). The Plugin return type is
|
|
41
|
+
// structurally incompatible due to _zod.version.minor skew even though
|
|
42
|
+
// runtime behavior is identical. `any` bypasses the type-level mismatch —
|
|
43
|
+
// opencode's plugin loader doesn't care about the zod version at runtime.
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
const ipcToolsPlugin = async () => {
|
|
46
|
+
initSentry();
|
|
47
|
+
const dataDir = process.env.KIMAKI_DATA_DIR;
|
|
48
|
+
if (dataDir) {
|
|
49
|
+
setDataDir(dataDir);
|
|
50
|
+
setPluginLogFilePath(dataDir);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
tool: {
|
|
54
|
+
kimaki_file_upload: tool({
|
|
55
|
+
description: 'Prompt the Discord user to upload files using a native file picker modal. ' +
|
|
56
|
+
'The user sees a button, clicks it, and gets a file upload dialog. ' +
|
|
57
|
+
'Returns the local file paths of downloaded files in the project directory. ' +
|
|
58
|
+
'Use this when you need the user to provide files (images, documents, configs, etc.). ' +
|
|
59
|
+
'IMPORTANT: Always call this tool last in your message, after all text parts.',
|
|
60
|
+
args: {
|
|
61
|
+
prompt: z
|
|
62
|
+
.string()
|
|
63
|
+
.describe('Message shown to the user explaining what files to upload'),
|
|
64
|
+
maxFiles: z
|
|
65
|
+
.number()
|
|
66
|
+
.min(1)
|
|
67
|
+
.max(10)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Maximum number of files the user can upload (1-10, default 5)'),
|
|
70
|
+
},
|
|
71
|
+
async execute({ prompt, maxFiles }, context) {
|
|
72
|
+
const { getPrisma, createIpcRequest, getIpcRequestById } = await loadDatabaseModule();
|
|
73
|
+
const prisma = await getPrisma();
|
|
74
|
+
const row = await prisma.thread_sessions.findFirst({
|
|
75
|
+
where: { session_id: context.sessionID },
|
|
76
|
+
select: { thread_id: true },
|
|
77
|
+
});
|
|
78
|
+
if (!row?.thread_id) {
|
|
79
|
+
return 'Could not find thread for current session';
|
|
80
|
+
}
|
|
81
|
+
const ipcRow = await createIpcRequest({
|
|
82
|
+
type: 'file_upload',
|
|
83
|
+
sessionId: context.sessionID,
|
|
84
|
+
threadId: row.thread_id,
|
|
85
|
+
payload: JSON.stringify({
|
|
86
|
+
prompt,
|
|
87
|
+
maxFiles: maxFiles || DEFAULT_FILE_UPLOAD_MAX_FILES,
|
|
88
|
+
directory: context.directory,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
const deadline = Date.now() + FILE_UPLOAD_TIMEOUT_MS;
|
|
92
|
+
const POLL_INTERVAL_MS = 300;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
await new Promise((resolve) => {
|
|
95
|
+
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
96
|
+
});
|
|
97
|
+
const updated = await getIpcRequestById({ id: ipcRow.id });
|
|
98
|
+
if (!updated || updated.status === 'cancelled') {
|
|
99
|
+
return 'File upload was cancelled';
|
|
100
|
+
}
|
|
101
|
+
if (updated.response) {
|
|
102
|
+
const parsed = JSON.parse(updated.response);
|
|
103
|
+
if (parsed.error) {
|
|
104
|
+
return `File upload failed: ${parsed.error}`;
|
|
105
|
+
}
|
|
106
|
+
const filePaths = parsed.filePaths || [];
|
|
107
|
+
if (filePaths.length === 0) {
|
|
108
|
+
return 'No files were uploaded (user may have cancelled or sent a new message)';
|
|
109
|
+
}
|
|
110
|
+
return `Files uploaded successfully:\n${filePaths.join('\n')}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return 'File upload timed out - user did not upload files within the time limit';
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
kimaki_action_buttons: tool({
|
|
117
|
+
description: dedent `
|
|
118
|
+
Show action buttons in the current Discord thread for quick confirmations.
|
|
119
|
+
Use this when the user can respond by clicking one of up to 3 buttons.
|
|
120
|
+
Prefer a single button whenever possible.
|
|
121
|
+
Default color is white (same visual style as permission deny button).
|
|
122
|
+
If you need more than 3 options, use the question tool instead.
|
|
123
|
+
IMPORTANT: Always call this tool last in your message, after all text parts.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
- buttons: [{"label":"Yes, proceed"}]
|
|
127
|
+
- buttons: [{"label":"Approve","color":"green"}]
|
|
128
|
+
- buttons: [
|
|
129
|
+
{"label":"Confirm","color":"blue"},
|
|
130
|
+
{"label":"Cancel","color":"white"}
|
|
131
|
+
]
|
|
132
|
+
`,
|
|
133
|
+
args: {
|
|
134
|
+
buttons: z
|
|
135
|
+
.array(z.object({
|
|
136
|
+
label: z
|
|
137
|
+
.string()
|
|
138
|
+
.min(1)
|
|
139
|
+
.max(80)
|
|
140
|
+
.describe('Button label shown to the user (1-80 chars)'),
|
|
141
|
+
color: z
|
|
142
|
+
.enum(['white', 'blue', 'green', 'red'])
|
|
143
|
+
.optional()
|
|
144
|
+
.describe('Optional button color. white is default and preferred for most confirmations.'),
|
|
145
|
+
}))
|
|
146
|
+
.min(1)
|
|
147
|
+
.max(3)
|
|
148
|
+
.describe('Array of 1-3 action buttons. Prefer one button whenever possible.'),
|
|
149
|
+
},
|
|
150
|
+
async execute({ buttons }, context) {
|
|
151
|
+
const { getPrisma, createIpcRequest, getIpcRequestById } = await loadDatabaseModule();
|
|
152
|
+
const prisma = await getPrisma();
|
|
153
|
+
const row = await prisma.thread_sessions.findFirst({
|
|
154
|
+
where: { session_id: context.sessionID },
|
|
155
|
+
select: { thread_id: true },
|
|
156
|
+
});
|
|
157
|
+
if (!row?.thread_id) {
|
|
158
|
+
return 'Could not find thread for current session';
|
|
159
|
+
}
|
|
160
|
+
const ipcRow = await createIpcRequest({
|
|
161
|
+
type: 'action_buttons',
|
|
162
|
+
sessionId: context.sessionID,
|
|
163
|
+
threadId: row.thread_id,
|
|
164
|
+
payload: JSON.stringify({
|
|
165
|
+
buttons,
|
|
166
|
+
directory: context.directory,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
const deadline = Date.now() + ACTION_BUTTON_TIMEOUT_MS;
|
|
170
|
+
const POLL_INTERVAL_MS = 200;
|
|
171
|
+
while (Date.now() < deadline) {
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
174
|
+
});
|
|
175
|
+
const updated = await getIpcRequestById({ id: ipcRow.id });
|
|
176
|
+
if (!updated || updated.status === 'cancelled') {
|
|
177
|
+
return 'Action button request was cancelled';
|
|
178
|
+
}
|
|
179
|
+
if (updated.response) {
|
|
180
|
+
const parsed = JSON.parse(updated.response);
|
|
181
|
+
if (parsed.error) {
|
|
182
|
+
return `Action button request failed: ${parsed.error}`;
|
|
183
|
+
}
|
|
184
|
+
return `Action button(s) shown: ${buttons.map((button) => button.label).join(', ')}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return 'Action button request timed out';
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
export { ipcToolsPlugin };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// End-to-end test using discord-digital-twin + real Kimaki bot runtime.
|
|
2
|
+
// Verifies onboarding channel creation, message -> thread creation, and assistant reply.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { expect, test } from 'vitest';
|
|
6
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
|
|
7
|
+
import { DigitalDiscord } from 'discord-digital-twin/src';
|
|
8
|
+
import { CachedOpencodeProviderProxy } from 'opencode-cached-provider';
|
|
9
|
+
import { setDataDir } from './config.js';
|
|
10
|
+
import { startDiscordBot } from './discord-bot.js';
|
|
11
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, } from './database.js';
|
|
12
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
13
|
+
import { cleanupTestSessions, chooseLockPort, initTestGitRepo } from './test-utils.js';
|
|
14
|
+
import { stopOpencodeServer } from './opencode.js';
|
|
15
|
+
const geminiApiKey = process.env['GEMINI_API_KEY'] ||
|
|
16
|
+
process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
|
|
17
|
+
'';
|
|
18
|
+
const geminiModel = process.env['GEMINI_FLASH_MODEL'] || 'gemini-2.5-flash';
|
|
19
|
+
const e2eTest = geminiApiKey.length > 0 ? test : test.skip;
|
|
20
|
+
function createRunDirectories() {
|
|
21
|
+
const root = path.resolve(process.cwd(), 'tmp', 'kimaki-digital-twin-e2e');
|
|
22
|
+
fs.mkdirSync(root, { recursive: true });
|
|
23
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
|
|
24
|
+
const projectDirectory = path.join(root, 'project');
|
|
25
|
+
const providerCacheDbPath = path.join(root, 'provider-cache.db');
|
|
26
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
27
|
+
initTestGitRepo(projectDirectory);
|
|
28
|
+
return {
|
|
29
|
+
root,
|
|
30
|
+
dataDir,
|
|
31
|
+
projectDirectory,
|
|
32
|
+
providerCacheDbPath,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function createDiscordJsClient({ restUrl }) {
|
|
36
|
+
return new Client({
|
|
37
|
+
intents: [
|
|
38
|
+
GatewayIntentBits.Guilds,
|
|
39
|
+
GatewayIntentBits.GuildMessages,
|
|
40
|
+
GatewayIntentBits.MessageContent,
|
|
41
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
42
|
+
],
|
|
43
|
+
partials: [
|
|
44
|
+
Partials.Channel,
|
|
45
|
+
Partials.Message,
|
|
46
|
+
Partials.User,
|
|
47
|
+
Partials.ThreadMember,
|
|
48
|
+
],
|
|
49
|
+
rest: {
|
|
50
|
+
api: restUrl,
|
|
51
|
+
version: '10',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
e2eTest('onboarding then message creates thread and assistant reply via digital twin', async () => {
|
|
56
|
+
const testStartTime = Date.now();
|
|
57
|
+
const directories = createRunDirectories();
|
|
58
|
+
const lockPort = chooseLockPort({ key: 'kimaki-digital-twin-e2e' });
|
|
59
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
|
|
60
|
+
setDataDir(directories.dataDir);
|
|
61
|
+
const proxy = new CachedOpencodeProviderProxy({
|
|
62
|
+
cacheDbPath: directories.providerCacheDbPath,
|
|
63
|
+
targetBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
64
|
+
apiKey: geminiApiKey,
|
|
65
|
+
cacheMethods: ['POST'],
|
|
66
|
+
});
|
|
67
|
+
const testUserId = '100000000000000777';
|
|
68
|
+
const textChannelId = '100000000000000778';
|
|
69
|
+
const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
|
|
70
|
+
const discord = new DigitalDiscord({
|
|
71
|
+
guild: {
|
|
72
|
+
name: 'Kimaki E2E Guild',
|
|
73
|
+
ownerId: testUserId,
|
|
74
|
+
},
|
|
75
|
+
channels: [
|
|
76
|
+
{
|
|
77
|
+
id: textChannelId,
|
|
78
|
+
name: 'kimaki-e2e',
|
|
79
|
+
type: ChannelType.GuildText,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
users: [
|
|
83
|
+
{
|
|
84
|
+
id: testUserId,
|
|
85
|
+
username: 'e2e-user',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
89
|
+
});
|
|
90
|
+
let botClient = null;
|
|
91
|
+
try {
|
|
92
|
+
await Promise.all([proxy.start(), discord.start()]);
|
|
93
|
+
const opencodeConfig = proxy.buildOpencodeConfig({
|
|
94
|
+
providerName: 'cached-google',
|
|
95
|
+
providerNpm: '@ai-sdk/google',
|
|
96
|
+
model: geminiModel,
|
|
97
|
+
smallModel: geminiModel,
|
|
98
|
+
});
|
|
99
|
+
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
100
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
|
|
101
|
+
const hranaResult = await startHranaServer({ dbPath });
|
|
102
|
+
if (hranaResult instanceof Error) {
|
|
103
|
+
throw hranaResult;
|
|
104
|
+
}
|
|
105
|
+
process.env['KIMAKI_DB_URL'] = hranaResult;
|
|
106
|
+
await initDatabase();
|
|
107
|
+
await setBotToken(discord.botUserId, discord.botToken);
|
|
108
|
+
await setChannelDirectory({
|
|
109
|
+
channelId: textChannelId,
|
|
110
|
+
directory: directories.projectDirectory,
|
|
111
|
+
channelType: 'text',
|
|
112
|
+
});
|
|
113
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl });
|
|
114
|
+
await startDiscordBot({
|
|
115
|
+
token: discord.botToken,
|
|
116
|
+
appId: discord.botUserId,
|
|
117
|
+
discordClient: botClient,
|
|
118
|
+
});
|
|
119
|
+
await discord.channel(textChannelId).user(testUserId).sendMessage({
|
|
120
|
+
content: 'Reply with exactly: kimaki digital twin ok',
|
|
121
|
+
});
|
|
122
|
+
const createdThread = await discord.channel(textChannelId).waitForThread({
|
|
123
|
+
timeout: 60_000,
|
|
124
|
+
predicate: (thread) => {
|
|
125
|
+
return thread.name === 'Reply with exactly: kimaki digital twin ok';
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const botReply = await discord.thread(createdThread.id).waitForBotReply({
|
|
129
|
+
timeout: 120_000,
|
|
130
|
+
});
|
|
131
|
+
expect(createdThread.id.length).toBeGreaterThan(0);
|
|
132
|
+
expect(botReply.content.trim().length).toBeGreaterThan(0);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
await cleanupTestSessions({
|
|
136
|
+
projectDirectory: directories.projectDirectory,
|
|
137
|
+
testStartTime,
|
|
138
|
+
});
|
|
139
|
+
if (botClient) {
|
|
140
|
+
botClient.destroy();
|
|
141
|
+
}
|
|
142
|
+
await stopOpencodeServer();
|
|
143
|
+
await Promise.all([
|
|
144
|
+
closeDatabase().catch(() => {
|
|
145
|
+
return;
|
|
146
|
+
}),
|
|
147
|
+
stopHranaServer().catch(() => {
|
|
148
|
+
return;
|
|
149
|
+
}),
|
|
150
|
+
proxy.stop().catch(() => {
|
|
151
|
+
return;
|
|
152
|
+
}),
|
|
153
|
+
discord.stop().catch(() => {
|
|
154
|
+
return;
|
|
155
|
+
}),
|
|
156
|
+
]);
|
|
157
|
+
delete process.env['KIMAKI_LOCK_PORT'];
|
|
158
|
+
delete process.env['KIMAKI_DB_URL'];
|
|
159
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
}, 360_000);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// E2e test for OpenCode plugin loading.
|
|
2
|
+
// Spawns `opencode serve` directly with our plugin in OPENCODE_CONFIG_CONTENT,
|
|
3
|
+
// waits for the health endpoint, then checks stderr for plugin errors.
|
|
4
|
+
// No Discord infrastructure needed — just the OpenCode server process.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { test, expect } from 'vitest';
|
|
10
|
+
import { resolveOpencodeCommand } from './opencode.js';
|
|
11
|
+
import { getSpawnCommandAndArgs } from './opencode-command.js';
|
|
12
|
+
import { chooseLockPort } from './test-utils.js';
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
async function waitForHealth({ port, maxAttempts = 30, }) {
|
|
15
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
|
|
18
|
+
if (response.status < 500) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// connection refused, retry
|
|
24
|
+
}
|
|
25
|
+
await new Promise((resolve) => {
|
|
26
|
+
setTimeout(resolve, 1000);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
test('opencode server loads plugin without errors', async () => {
|
|
32
|
+
const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e');
|
|
33
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
34
|
+
const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' });
|
|
35
|
+
const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href;
|
|
36
|
+
const stderrLines = [];
|
|
37
|
+
const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home');
|
|
38
|
+
const { command, args, windowsVerbatimArguments, } = getSpawnCommandAndArgs({
|
|
39
|
+
resolvedCommand: resolveOpencodeCommand(),
|
|
40
|
+
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
41
|
+
});
|
|
42
|
+
const serverProcess = spawn(command, args, {
|
|
43
|
+
stdio: 'pipe',
|
|
44
|
+
cwd: projectDir,
|
|
45
|
+
windowsVerbatimArguments,
|
|
46
|
+
env: {
|
|
47
|
+
...process.env,
|
|
48
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
49
|
+
$schema: 'https://opencode.ai/config.json',
|
|
50
|
+
lsp: false,
|
|
51
|
+
formatter: false,
|
|
52
|
+
plugin: [pluginPath],
|
|
53
|
+
}),
|
|
54
|
+
OPENCODE_TEST_HOME: isolatedOpencodeRoot,
|
|
55
|
+
OPENCODE_CONFIG_DIR: path.join(isolatedOpencodeRoot, '.opencode-kimaki'),
|
|
56
|
+
XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
|
|
57
|
+
XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
|
|
58
|
+
XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
|
|
59
|
+
XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
63
|
+
stderrLines.push(...data.toString().split('\n').filter(Boolean));
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const healthy = await waitForHealth({ port });
|
|
67
|
+
expect(healthy).toBe(true);
|
|
68
|
+
// Check no plugin-related errors in stderr
|
|
69
|
+
const pluginErrorPatterns = [
|
|
70
|
+
/plugin.*error/i,
|
|
71
|
+
/failed to load plugin/i,
|
|
72
|
+
/cannot find module/i,
|
|
73
|
+
/ERR_MODULE_NOT_FOUND/i,
|
|
74
|
+
/plugin.*failed/i,
|
|
75
|
+
/plugin.*crash/i,
|
|
76
|
+
];
|
|
77
|
+
const errorLines = stderrLines.filter((line) => {
|
|
78
|
+
return pluginErrorPatterns.some((pattern) => {
|
|
79
|
+
return pattern.test(line);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
expect(errorLines).toEqual([]);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
serverProcess.kill('SIGTERM');
|
|
86
|
+
}
|
|
87
|
+
}, 60_000);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// OpenCode plugin entry point for Kimaki Discord bot.
|
|
2
|
+
// Each export is treated as a separate plugin by OpenCode's plugin loader.
|
|
3
|
+
// CRITICAL: never export utility functions from this file — only plugin
|
|
4
|
+
// initializer functions. OpenCode calls every export as a plugin.
|
|
5
|
+
//
|
|
6
|
+
// Plugins are split into focused modules:
|
|
7
|
+
// - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
|
|
8
|
+
// - context-awareness-plugin: branch, pwd, memory, time gap, onboarding tutorial
|
|
9
|
+
// - opencode-interrupt-plugin: interrupt queued messages at step boundaries
|
|
10
|
+
// - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
|
|
11
|
+
export { ipcToolsPlugin } from './ipc-tools-plugin.js';
|
|
12
|
+
export { contextAwarenessPlugin } from './context-awareness-plugin.js';
|
|
13
|
+
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
|
|
14
|
+
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
|
|
15
|
+
export { imageOptimizerPlugin } from './image-optimizer-plugin.js';
|
|
16
|
+
export { kittyGraphicsPlugin } from 'kitty-graphics-agent';
|
|
17
|
+
export { injectionGuardInternal as injectionGuard } from 'opencode-injection-guard';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { test, expect, describe } from 'vitest';
|
|
2
|
+
import { condenseMemoryMd } from './condense-memory.js';
|
|
3
|
+
describe('condenseMemoryMd', () => {
|
|
4
|
+
test('multiple headings with body content', () => {
|
|
5
|
+
const content = [
|
|
6
|
+
'# Project Overview',
|
|
7
|
+
'',
|
|
8
|
+
'This is a big project with many things.',
|
|
9
|
+
'It does X, Y, and Z.',
|
|
10
|
+
'',
|
|
11
|
+
'## Auth Architecture',
|
|
12
|
+
'',
|
|
13
|
+
'JWT tokens with 15min expiry.',
|
|
14
|
+
'Refresh tokens in httpOnly cookies.',
|
|
15
|
+
'Session stored in Redis.',
|
|
16
|
+
'',
|
|
17
|
+
'## User Preferences',
|
|
18
|
+
'',
|
|
19
|
+
'- kebab-case filenames',
|
|
20
|
+
'- errore-style errors',
|
|
21
|
+
'- no emojis',
|
|
22
|
+
'',
|
|
23
|
+
'### API Conventions',
|
|
24
|
+
'',
|
|
25
|
+
'All routes return { data, error }.',
|
|
26
|
+
'Use spiceflow for the server.',
|
|
27
|
+
'',
|
|
28
|
+
].join('\n');
|
|
29
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
30
|
+
"1: # Project Overview
|
|
31
|
+
...
|
|
32
|
+
6: ## Auth Architecture
|
|
33
|
+
...
|
|
34
|
+
12: ## User Preferences
|
|
35
|
+
...
|
|
36
|
+
18: ### API Conventions
|
|
37
|
+
..."
|
|
38
|
+
`);
|
|
39
|
+
});
|
|
40
|
+
test('body text before first heading', () => {
|
|
41
|
+
const content = [
|
|
42
|
+
'Some preamble notes.',
|
|
43
|
+
'',
|
|
44
|
+
'# First Heading',
|
|
45
|
+
'',
|
|
46
|
+
'Content here.',
|
|
47
|
+
'',
|
|
48
|
+
].join('\n');
|
|
49
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
50
|
+
"...
|
|
51
|
+
3: # First Heading
|
|
52
|
+
..."
|
|
53
|
+
`);
|
|
54
|
+
});
|
|
55
|
+
test('no headings at all', () => {
|
|
56
|
+
const content = 'Just some notes.\nMore notes.\n';
|
|
57
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`"..."`);
|
|
58
|
+
});
|
|
59
|
+
test('empty content', () => {
|
|
60
|
+
expect(condenseMemoryMd('')).toMatchInlineSnapshot(`""`);
|
|
61
|
+
});
|
|
62
|
+
test('consecutive headings without body', () => {
|
|
63
|
+
const content = [
|
|
64
|
+
'# H1',
|
|
65
|
+
'## H2',
|
|
66
|
+
'### H3',
|
|
67
|
+
'',
|
|
68
|
+
'Some body.',
|
|
69
|
+
'',
|
|
70
|
+
].join('\n');
|
|
71
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
72
|
+
"1: # H1
|
|
73
|
+
2: ## H2
|
|
74
|
+
3: ### H3
|
|
75
|
+
..."
|
|
76
|
+
`);
|
|
77
|
+
});
|
|
78
|
+
test('heading with code block body', () => {
|
|
79
|
+
const content = [
|
|
80
|
+
'# Config',
|
|
81
|
+
'',
|
|
82
|
+
'```json',
|
|
83
|
+
'{ "key": "value" }',
|
|
84
|
+
'```',
|
|
85
|
+
'',
|
|
86
|
+
'## Notes',
|
|
87
|
+
'',
|
|
88
|
+
'Some text.',
|
|
89
|
+
'',
|
|
90
|
+
].join('\n');
|
|
91
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
92
|
+
"1: # Config
|
|
93
|
+
...
|
|
94
|
+
7: ## Notes
|
|
95
|
+
..."
|
|
96
|
+
`);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Limit heading depth for Discord.
|
|
2
|
+
// Discord only supports headings up to ### (h3), so this converts
|
|
3
|
+
// ####, #####, etc. to ### to maintain consistent rendering.
|
|
4
|
+
import { Lexer } from 'marked';
|
|
5
|
+
export function limitHeadingDepth(markdown, maxDepth = 3) {
|
|
6
|
+
const lexer = new Lexer();
|
|
7
|
+
const tokens = lexer.lex(markdown);
|
|
8
|
+
let result = '';
|
|
9
|
+
for (const token of tokens) {
|
|
10
|
+
if (token.type === 'heading') {
|
|
11
|
+
const heading = token;
|
|
12
|
+
if (heading.depth > maxDepth) {
|
|
13
|
+
const hashes = '#'.repeat(maxDepth);
|
|
14
|
+
result += hashes + ' ' + heading.text + '\n';
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
result += token.raw;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
result += token.raw;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { limitHeadingDepth } from './limit-heading-depth.js';
|
|
3
|
+
test('converts h4 to h3', () => {
|
|
4
|
+
const input = '#### Fourth level heading';
|
|
5
|
+
const result = limitHeadingDepth(input);
|
|
6
|
+
expect(result).toMatchInlineSnapshot(`
|
|
7
|
+
"### Fourth level heading
|
|
8
|
+
"
|
|
9
|
+
`);
|
|
10
|
+
});
|
|
11
|
+
test('converts h5 to h3', () => {
|
|
12
|
+
const input = '##### Fifth level heading';
|
|
13
|
+
const result = limitHeadingDepth(input);
|
|
14
|
+
expect(result).toMatchInlineSnapshot(`
|
|
15
|
+
"### Fifth level heading
|
|
16
|
+
"
|
|
17
|
+
`);
|
|
18
|
+
});
|
|
19
|
+
test('converts h6 to h3', () => {
|
|
20
|
+
const input = '###### Sixth level heading';
|
|
21
|
+
const result = limitHeadingDepth(input);
|
|
22
|
+
expect(result).toMatchInlineSnapshot(`
|
|
23
|
+
"### Sixth level heading
|
|
24
|
+
"
|
|
25
|
+
`);
|
|
26
|
+
});
|
|
27
|
+
test('preserves h3 unchanged', () => {
|
|
28
|
+
const input = '### Third level heading';
|
|
29
|
+
const result = limitHeadingDepth(input);
|
|
30
|
+
expect(result).toMatchInlineSnapshot(`"### Third level heading"`);
|
|
31
|
+
});
|
|
32
|
+
test('preserves h2 unchanged', () => {
|
|
33
|
+
const input = '## Second level heading';
|
|
34
|
+
const result = limitHeadingDepth(input);
|
|
35
|
+
expect(result).toMatchInlineSnapshot(`"## Second level heading"`);
|
|
36
|
+
});
|
|
37
|
+
test('preserves h1 unchanged', () => {
|
|
38
|
+
const input = '# First level heading';
|
|
39
|
+
const result = limitHeadingDepth(input);
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`"# First level heading"`);
|
|
41
|
+
});
|
|
42
|
+
test('handles multiple headings in document', () => {
|
|
43
|
+
const input = `# Title
|
|
44
|
+
|
|
45
|
+
Some text
|
|
46
|
+
|
|
47
|
+
## Section
|
|
48
|
+
|
|
49
|
+
### Subsection
|
|
50
|
+
|
|
51
|
+
#### Too deep
|
|
52
|
+
|
|
53
|
+
##### Even deeper
|
|
54
|
+
|
|
55
|
+
Regular paragraph
|
|
56
|
+
|
|
57
|
+
### Back to normal
|
|
58
|
+
`;
|
|
59
|
+
const result = limitHeadingDepth(input);
|
|
60
|
+
expect(result).toMatchInlineSnapshot(`
|
|
61
|
+
"# Title
|
|
62
|
+
|
|
63
|
+
Some text
|
|
64
|
+
|
|
65
|
+
## Section
|
|
66
|
+
|
|
67
|
+
### Subsection
|
|
68
|
+
|
|
69
|
+
### Too deep
|
|
70
|
+
### Even deeper
|
|
71
|
+
Regular paragraph
|
|
72
|
+
|
|
73
|
+
### Back to normal
|
|
74
|
+
"
|
|
75
|
+
`);
|
|
76
|
+
});
|
|
77
|
+
test('preserves heading with inline formatting', () => {
|
|
78
|
+
const input = '#### Heading with **bold** and `code`';
|
|
79
|
+
const result = limitHeadingDepth(input);
|
|
80
|
+
expect(result).toMatchInlineSnapshot(`
|
|
81
|
+
"### Heading with **bold** and \`code\`
|
|
82
|
+
"
|
|
83
|
+
`);
|
|
84
|
+
});
|
|
85
|
+
test('handles empty markdown', () => {
|
|
86
|
+
const result = limitHeadingDepth('');
|
|
87
|
+
expect(result).toMatchInlineSnapshot(`""`);
|
|
88
|
+
});
|
|
89
|
+
test('handles markdown with no headings', () => {
|
|
90
|
+
const input = 'Just some text\n\nAnd more text';
|
|
91
|
+
const result = limitHeadingDepth(input);
|
|
92
|
+
expect(result).toMatchInlineSnapshot(`
|
|
93
|
+
"Just some text
|
|
94
|
+
|
|
95
|
+
And more text"
|
|
96
|
+
`);
|
|
97
|
+
});
|
|
98
|
+
test('allows custom maxDepth', () => {
|
|
99
|
+
const input = '### Third level';
|
|
100
|
+
const result = limitHeadingDepth(input, 2);
|
|
101
|
+
expect(result).toMatchInlineSnapshot(`
|
|
102
|
+
"## Third level
|
|
103
|
+
"
|
|
104
|
+
`);
|
|
105
|
+
});
|