@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,199 @@
|
|
|
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
|
+
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { expect, test } from 'vitest'
|
|
7
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
|
|
8
|
+
import { DigitalDiscord } from 'discord-digital-twin/src'
|
|
9
|
+
import { CachedOpencodeProviderProxy } from 'opencode-cached-provider'
|
|
10
|
+
import { setDataDir } from './config.js'
|
|
11
|
+
import { startDiscordBot } from './discord-bot.js'
|
|
12
|
+
import {
|
|
13
|
+
setBotToken,
|
|
14
|
+
initDatabase,
|
|
15
|
+
closeDatabase,
|
|
16
|
+
setChannelDirectory,
|
|
17
|
+
} from './database.js'
|
|
18
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js'
|
|
19
|
+
import { cleanupTestSessions, chooseLockPort, initTestGitRepo } from './test-utils.js'
|
|
20
|
+
import { stopOpencodeServer } from './opencode.js'
|
|
21
|
+
|
|
22
|
+
const geminiApiKey =
|
|
23
|
+
process.env['GEMINI_API_KEY'] ||
|
|
24
|
+
process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
|
|
25
|
+
''
|
|
26
|
+
const geminiModel = process.env['GEMINI_FLASH_MODEL'] || 'gemini-2.5-flash'
|
|
27
|
+
const e2eTest = geminiApiKey.length > 0 ? test : test.skip
|
|
28
|
+
|
|
29
|
+
function createRunDirectories() {
|
|
30
|
+
const root = path.resolve(process.cwd(), 'tmp', 'kimaki-digital-twin-e2e')
|
|
31
|
+
fs.mkdirSync(root, { recursive: true })
|
|
32
|
+
|
|
33
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
|
|
34
|
+
const projectDirectory = path.join(root, 'project')
|
|
35
|
+
const providerCacheDbPath = path.join(root, 'provider-cache.db')
|
|
36
|
+
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
37
|
+
initTestGitRepo(projectDirectory)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
root,
|
|
41
|
+
dataDir,
|
|
42
|
+
projectDirectory,
|
|
43
|
+
providerCacheDbPath,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createDiscordJsClient({ restUrl }: { restUrl: string }) {
|
|
48
|
+
return new Client({
|
|
49
|
+
intents: [
|
|
50
|
+
GatewayIntentBits.Guilds,
|
|
51
|
+
GatewayIntentBits.GuildMessages,
|
|
52
|
+
GatewayIntentBits.MessageContent,
|
|
53
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
54
|
+
],
|
|
55
|
+
partials: [
|
|
56
|
+
Partials.Channel,
|
|
57
|
+
Partials.Message,
|
|
58
|
+
Partials.User,
|
|
59
|
+
Partials.ThreadMember,
|
|
60
|
+
],
|
|
61
|
+
rest: {
|
|
62
|
+
api: restUrl,
|
|
63
|
+
version: '10',
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
e2eTest(
|
|
69
|
+
'onboarding then message creates thread and assistant reply via digital twin',
|
|
70
|
+
async () => {
|
|
71
|
+
const testStartTime = Date.now()
|
|
72
|
+
const directories = createRunDirectories()
|
|
73
|
+
const lockPort = chooseLockPort({ key: 'kimaki-digital-twin-e2e' })
|
|
74
|
+
|
|
75
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
|
|
76
|
+
setDataDir(directories.dataDir)
|
|
77
|
+
|
|
78
|
+
const proxy = new CachedOpencodeProviderProxy({
|
|
79
|
+
cacheDbPath: directories.providerCacheDbPath,
|
|
80
|
+
targetBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
81
|
+
apiKey: geminiApiKey,
|
|
82
|
+
cacheMethods: ['POST'],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const testUserId = '100000000000000777'
|
|
86
|
+
const textChannelId = '100000000000000778'
|
|
87
|
+
const digitalDiscordDbPath = path.join(
|
|
88
|
+
directories.dataDir,
|
|
89
|
+
'digital-discord.db',
|
|
90
|
+
)
|
|
91
|
+
const discord = new DigitalDiscord({
|
|
92
|
+
guild: {
|
|
93
|
+
name: 'Kimaki E2E Guild',
|
|
94
|
+
ownerId: testUserId,
|
|
95
|
+
},
|
|
96
|
+
channels: [
|
|
97
|
+
{
|
|
98
|
+
id: textChannelId,
|
|
99
|
+
name: 'kimaki-e2e',
|
|
100
|
+
type: ChannelType.GuildText,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
users: [
|
|
104
|
+
{
|
|
105
|
+
id: testUserId,
|
|
106
|
+
username: 'e2e-user',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
let botClient: Client | null = null
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await Promise.all([proxy.start(), discord.start()])
|
|
116
|
+
|
|
117
|
+
const opencodeConfig = proxy.buildOpencodeConfig({
|
|
118
|
+
providerName: 'cached-google',
|
|
119
|
+
providerNpm: '@ai-sdk/google',
|
|
120
|
+
model: geminiModel,
|
|
121
|
+
smallModel: geminiModel,
|
|
122
|
+
})
|
|
123
|
+
fs.writeFileSync(
|
|
124
|
+
path.join(directories.projectDirectory, 'opencode.json'),
|
|
125
|
+
JSON.stringify(opencodeConfig, null, 2),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
|
|
129
|
+
const hranaResult = await startHranaServer({ dbPath })
|
|
130
|
+
if (hranaResult instanceof Error) {
|
|
131
|
+
throw hranaResult
|
|
132
|
+
}
|
|
133
|
+
process.env['KIMAKI_DB_URL'] = hranaResult
|
|
134
|
+
await initDatabase()
|
|
135
|
+
await setBotToken(discord.botUserId, discord.botToken)
|
|
136
|
+
|
|
137
|
+
await setChannelDirectory({
|
|
138
|
+
channelId: textChannelId,
|
|
139
|
+
directory: directories.projectDirectory,
|
|
140
|
+
channelType: 'text',
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl })
|
|
144
|
+
await startDiscordBot({
|
|
145
|
+
token: discord.botToken,
|
|
146
|
+
appId: discord.botUserId,
|
|
147
|
+
discordClient: botClient,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await discord.channel(textChannelId).user(testUserId).sendMessage({
|
|
151
|
+
content: 'Reply with exactly: kimaki digital twin ok',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const createdThread = await discord.channel(textChannelId).waitForThread({
|
|
155
|
+
timeout: 60_000,
|
|
156
|
+
predicate: (thread) => {
|
|
157
|
+
return thread.name === 'Reply with exactly: kimaki digital twin ok'
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const botReply = await discord.thread(createdThread.id).waitForBotReply({
|
|
162
|
+
timeout: 120_000,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
expect(createdThread.id.length).toBeGreaterThan(0)
|
|
166
|
+
expect(botReply.content.trim().length).toBeGreaterThan(0)
|
|
167
|
+
} finally {
|
|
168
|
+
await cleanupTestSessions({
|
|
169
|
+
projectDirectory: directories.projectDirectory,
|
|
170
|
+
testStartTime,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if (botClient) {
|
|
174
|
+
botClient.destroy()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await stopOpencodeServer()
|
|
178
|
+
await Promise.all([
|
|
179
|
+
closeDatabase().catch(() => {
|
|
180
|
+
return
|
|
181
|
+
}),
|
|
182
|
+
stopHranaServer().catch(() => {
|
|
183
|
+
return
|
|
184
|
+
}),
|
|
185
|
+
proxy.stop().catch(() => {
|
|
186
|
+
return
|
|
187
|
+
}),
|
|
188
|
+
discord.stop().catch(() => {
|
|
189
|
+
return
|
|
190
|
+
}),
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
delete process.env['KIMAKI_LOCK_PORT']
|
|
194
|
+
delete process.env['KIMAKI_DB_URL']
|
|
195
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true })
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
360_000,
|
|
199
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
|
6
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { test, expect } from 'vitest'
|
|
11
|
+
import { resolveOpencodeCommand } from './opencode.js'
|
|
12
|
+
import { getSpawnCommandAndArgs } from './opencode-command.js'
|
|
13
|
+
import { chooseLockPort } from './test-utils.js'
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
|
|
17
|
+
async function waitForHealth({
|
|
18
|
+
port,
|
|
19
|
+
maxAttempts = 30,
|
|
20
|
+
}: {
|
|
21
|
+
port: number
|
|
22
|
+
maxAttempts?: number
|
|
23
|
+
}): Promise<boolean> {
|
|
24
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`)
|
|
27
|
+
if (response.status < 500) {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// connection refused, retry
|
|
32
|
+
}
|
|
33
|
+
await new Promise((resolve) => {
|
|
34
|
+
setTimeout(resolve, 1000)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test(
|
|
41
|
+
'opencode server loads plugin without errors',
|
|
42
|
+
async () => {
|
|
43
|
+
const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e')
|
|
44
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
45
|
+
|
|
46
|
+
const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' })
|
|
47
|
+
const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href
|
|
48
|
+
const stderrLines: string[] = []
|
|
49
|
+
const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home')
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
command,
|
|
53
|
+
args,
|
|
54
|
+
windowsVerbatimArguments,
|
|
55
|
+
} = getSpawnCommandAndArgs({
|
|
56
|
+
resolvedCommand: resolveOpencodeCommand(),
|
|
57
|
+
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const serverProcess: ChildProcess = spawn(command, args, {
|
|
61
|
+
stdio: 'pipe',
|
|
62
|
+
cwd: projectDir,
|
|
63
|
+
windowsVerbatimArguments,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
67
|
+
$schema: 'https://opencode.ai/config.json',
|
|
68
|
+
lsp: false,
|
|
69
|
+
formatter: false,
|
|
70
|
+
plugin: [pluginPath],
|
|
71
|
+
}),
|
|
72
|
+
OPENCODE_TEST_HOME: isolatedOpencodeRoot,
|
|
73
|
+
OPENCODE_CONFIG_DIR: path.join(isolatedOpencodeRoot, '.opencode-kimaki'),
|
|
74
|
+
XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
|
|
75
|
+
XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
|
|
76
|
+
XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
|
|
77
|
+
XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
82
|
+
stderrLines.push(...data.toString().split('\n').filter(Boolean))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const healthy = await waitForHealth({ port })
|
|
87
|
+
expect(healthy).toBe(true)
|
|
88
|
+
|
|
89
|
+
// Check no plugin-related errors in stderr
|
|
90
|
+
const pluginErrorPatterns = [
|
|
91
|
+
/plugin.*error/i,
|
|
92
|
+
/failed to load plugin/i,
|
|
93
|
+
/cannot find module/i,
|
|
94
|
+
/ERR_MODULE_NOT_FOUND/i,
|
|
95
|
+
/plugin.*failed/i,
|
|
96
|
+
/plugin.*crash/i,
|
|
97
|
+
]
|
|
98
|
+
const errorLines = stderrLines.filter((line) => {
|
|
99
|
+
return pluginErrorPatterns.some((pattern) => {
|
|
100
|
+
return pattern.test(line)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
expect(errorLines).toEqual([])
|
|
104
|
+
} finally {
|
|
105
|
+
serverProcess.kill('SIGTERM')
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
60_000,
|
|
109
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { test, expect, describe } from 'vitest'
|
|
2
|
+
import { condenseMemoryMd } from './condense-memory.js'
|
|
3
|
+
|
|
4
|
+
describe('condenseMemoryMd', () => {
|
|
5
|
+
test('multiple headings with body content', () => {
|
|
6
|
+
const content = [
|
|
7
|
+
'# Project Overview',
|
|
8
|
+
'',
|
|
9
|
+
'This is a big project with many things.',
|
|
10
|
+
'It does X, Y, and Z.',
|
|
11
|
+
'',
|
|
12
|
+
'## Auth Architecture',
|
|
13
|
+
'',
|
|
14
|
+
'JWT tokens with 15min expiry.',
|
|
15
|
+
'Refresh tokens in httpOnly cookies.',
|
|
16
|
+
'Session stored in Redis.',
|
|
17
|
+
'',
|
|
18
|
+
'## User Preferences',
|
|
19
|
+
'',
|
|
20
|
+
'- kebab-case filenames',
|
|
21
|
+
'- errore-style errors',
|
|
22
|
+
'- no emojis',
|
|
23
|
+
'',
|
|
24
|
+
'### API Conventions',
|
|
25
|
+
'',
|
|
26
|
+
'All routes return { data, error }.',
|
|
27
|
+
'Use spiceflow for the server.',
|
|
28
|
+
'',
|
|
29
|
+
].join('\n')
|
|
30
|
+
|
|
31
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
32
|
+
"1: # Project Overview
|
|
33
|
+
...
|
|
34
|
+
6: ## Auth Architecture
|
|
35
|
+
...
|
|
36
|
+
12: ## User Preferences
|
|
37
|
+
...
|
|
38
|
+
18: ### API Conventions
|
|
39
|
+
..."
|
|
40
|
+
`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('body text before first heading', () => {
|
|
44
|
+
const content = [
|
|
45
|
+
'Some preamble notes.',
|
|
46
|
+
'',
|
|
47
|
+
'# First Heading',
|
|
48
|
+
'',
|
|
49
|
+
'Content here.',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n')
|
|
52
|
+
|
|
53
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
54
|
+
"...
|
|
55
|
+
3: # First Heading
|
|
56
|
+
..."
|
|
57
|
+
`)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('no headings at all', () => {
|
|
61
|
+
const content = 'Just some notes.\nMore notes.\n'
|
|
62
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`"..."`)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('empty content', () => {
|
|
66
|
+
expect(condenseMemoryMd('')).toMatchInlineSnapshot(`""`)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('consecutive headings without body', () => {
|
|
70
|
+
const content = [
|
|
71
|
+
'# H1',
|
|
72
|
+
'## H2',
|
|
73
|
+
'### H3',
|
|
74
|
+
'',
|
|
75
|
+
'Some body.',
|
|
76
|
+
'',
|
|
77
|
+
].join('\n')
|
|
78
|
+
|
|
79
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
80
|
+
"1: # H1
|
|
81
|
+
2: ## H2
|
|
82
|
+
3: ### H3
|
|
83
|
+
..."
|
|
84
|
+
`)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('heading with code block body', () => {
|
|
88
|
+
const content = [
|
|
89
|
+
'# Config',
|
|
90
|
+
'',
|
|
91
|
+
'```json',
|
|
92
|
+
'{ "key": "value" }',
|
|
93
|
+
'```',
|
|
94
|
+
'',
|
|
95
|
+
'## Notes',
|
|
96
|
+
'',
|
|
97
|
+
'Some text.',
|
|
98
|
+
'',
|
|
99
|
+
].join('\n')
|
|
100
|
+
|
|
101
|
+
expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
|
|
102
|
+
"1: # Config
|
|
103
|
+
...
|
|
104
|
+
7: ## Notes
|
|
105
|
+
..."
|
|
106
|
+
`)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
|
|
12
|
+
export { ipcToolsPlugin } from './ipc-tools-plugin.js'
|
|
13
|
+
export { contextAwarenessPlugin } from './context-awareness-plugin.js'
|
|
14
|
+
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js'
|
|
15
|
+
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js'
|
|
16
|
+
export { imageOptimizerPlugin } from './image-optimizer-plugin.js'
|
|
17
|
+
export { kittyGraphicsPlugin } from 'kitty-graphics-agent'
|
|
18
|
+
export { injectionGuardInternal as injectionGuard } from 'opencode-injection-guard'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
import { limitHeadingDepth } from './limit-heading-depth.js'
|
|
3
|
+
|
|
4
|
+
test('converts h4 to h3', () => {
|
|
5
|
+
const input = '#### Fourth level heading'
|
|
6
|
+
const result = limitHeadingDepth(input)
|
|
7
|
+
expect(result).toMatchInlineSnapshot(`
|
|
8
|
+
"### Fourth level heading
|
|
9
|
+
"
|
|
10
|
+
`)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('converts h5 to h3', () => {
|
|
14
|
+
const input = '##### Fifth level heading'
|
|
15
|
+
const result = limitHeadingDepth(input)
|
|
16
|
+
expect(result).toMatchInlineSnapshot(`
|
|
17
|
+
"### Fifth level heading
|
|
18
|
+
"
|
|
19
|
+
`)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('converts h6 to h3', () => {
|
|
23
|
+
const input = '###### Sixth level heading'
|
|
24
|
+
const result = limitHeadingDepth(input)
|
|
25
|
+
expect(result).toMatchInlineSnapshot(`
|
|
26
|
+
"### Sixth level heading
|
|
27
|
+
"
|
|
28
|
+
`)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('preserves h3 unchanged', () => {
|
|
32
|
+
const input = '### Third level heading'
|
|
33
|
+
const result = limitHeadingDepth(input)
|
|
34
|
+
expect(result).toMatchInlineSnapshot(`"### Third level heading"`)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('preserves h2 unchanged', () => {
|
|
38
|
+
const input = '## Second level heading'
|
|
39
|
+
const result = limitHeadingDepth(input)
|
|
40
|
+
expect(result).toMatchInlineSnapshot(`"## Second level heading"`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('preserves h1 unchanged', () => {
|
|
44
|
+
const input = '# First level heading'
|
|
45
|
+
const result = limitHeadingDepth(input)
|
|
46
|
+
expect(result).toMatchInlineSnapshot(`"# First level heading"`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('handles multiple headings in document', () => {
|
|
50
|
+
const input = `# Title
|
|
51
|
+
|
|
52
|
+
Some text
|
|
53
|
+
|
|
54
|
+
## Section
|
|
55
|
+
|
|
56
|
+
### Subsection
|
|
57
|
+
|
|
58
|
+
#### Too deep
|
|
59
|
+
|
|
60
|
+
##### Even deeper
|
|
61
|
+
|
|
62
|
+
Regular paragraph
|
|
63
|
+
|
|
64
|
+
### Back to normal
|
|
65
|
+
`
|
|
66
|
+
const result = limitHeadingDepth(input)
|
|
67
|
+
expect(result).toMatchInlineSnapshot(`
|
|
68
|
+
"# Title
|
|
69
|
+
|
|
70
|
+
Some text
|
|
71
|
+
|
|
72
|
+
## Section
|
|
73
|
+
|
|
74
|
+
### Subsection
|
|
75
|
+
|
|
76
|
+
### Too deep
|
|
77
|
+
### Even deeper
|
|
78
|
+
Regular paragraph
|
|
79
|
+
|
|
80
|
+
### Back to normal
|
|
81
|
+
"
|
|
82
|
+
`)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('preserves heading with inline formatting', () => {
|
|
86
|
+
const input = '#### Heading with **bold** and `code`'
|
|
87
|
+
const result = limitHeadingDepth(input)
|
|
88
|
+
expect(result).toMatchInlineSnapshot(`
|
|
89
|
+
"### Heading with **bold** and \`code\`
|
|
90
|
+
"
|
|
91
|
+
`)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('handles empty markdown', () => {
|
|
95
|
+
const result = limitHeadingDepth('')
|
|
96
|
+
expect(result).toMatchInlineSnapshot(`""`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('handles markdown with no headings', () => {
|
|
100
|
+
const input = 'Just some text\n\nAnd more text'
|
|
101
|
+
const result = limitHeadingDepth(input)
|
|
102
|
+
expect(result).toMatchInlineSnapshot(`
|
|
103
|
+
"Just some text
|
|
104
|
+
|
|
105
|
+
And more text"
|
|
106
|
+
`)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('allows custom maxDepth', () => {
|
|
110
|
+
const input = '### Third level'
|
|
111
|
+
const result = limitHeadingDepth(input, 2)
|
|
112
|
+
expect(result).toMatchInlineSnapshot(`
|
|
113
|
+
"## Third level
|
|
114
|
+
"
|
|
115
|
+
`)
|
|
116
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
|
|
5
|
+
import { Lexer, type Tokens } from 'marked'
|
|
6
|
+
|
|
7
|
+
export function limitHeadingDepth(markdown: string, maxDepth = 3): string {
|
|
8
|
+
const lexer = new Lexer()
|
|
9
|
+
const tokens = lexer.lex(markdown)
|
|
10
|
+
|
|
11
|
+
let result = ''
|
|
12
|
+
for (const token of tokens) {
|
|
13
|
+
if (token.type === 'heading') {
|
|
14
|
+
const heading = token as Tokens.Heading
|
|
15
|
+
if (heading.depth > maxDepth) {
|
|
16
|
+
const hashes = '#'.repeat(maxDepth)
|
|
17
|
+
result += hashes + ' ' + heading.text + '\n'
|
|
18
|
+
} else {
|
|
19
|
+
result += token.raw
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
result += token.raw
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|