@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,181 @@
|
|
|
1
|
+
// HTML fragment parser for Discord-renderable components.
|
|
2
|
+
// Supports a small reusable subset today (text + button) so tables and other
|
|
3
|
+
// CV2 renderers can map inline HTML into Discord UI elements.
|
|
4
|
+
|
|
5
|
+
import { DomHandler, ElementType, Parser } from 'htmlparser2'
|
|
6
|
+
import type { ChildNode, Element, Text } from 'domhandler'
|
|
7
|
+
|
|
8
|
+
export type HtmlTextRenderable = {
|
|
9
|
+
type: 'text'
|
|
10
|
+
text: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type HtmlButtonRenderable = {
|
|
14
|
+
type: 'button'
|
|
15
|
+
id: string
|
|
16
|
+
label: string
|
|
17
|
+
variant: 'secondary' | 'primary' | 'success' | 'danger'
|
|
18
|
+
disabled: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type HtmlRenderable = HtmlTextRenderable | HtmlButtonRenderable
|
|
22
|
+
|
|
23
|
+
export function parseInlineHtmlRenderables({
|
|
24
|
+
html,
|
|
25
|
+
}: {
|
|
26
|
+
html: string
|
|
27
|
+
}): HtmlRenderable[] | Error {
|
|
28
|
+
let parseError: Error | undefined
|
|
29
|
+
let domNodes: ChildNode[] = []
|
|
30
|
+
|
|
31
|
+
const handler = new DomHandler(
|
|
32
|
+
(error, dom) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
parseError = new Error('Failed to parse HTML fragment', {
|
|
35
|
+
cause: error,
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
domNodes = dom
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
withStartIndices: false,
|
|
43
|
+
withEndIndices: false,
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const parser = new Parser(handler, {
|
|
48
|
+
xmlMode: false,
|
|
49
|
+
decodeEntities: false,
|
|
50
|
+
recognizeSelfClosing: true,
|
|
51
|
+
})
|
|
52
|
+
parser.write(html)
|
|
53
|
+
parser.end()
|
|
54
|
+
|
|
55
|
+
if (parseError) {
|
|
56
|
+
return parseError
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parseRenderableNodes({ nodes: domNodes })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRenderableNodes({
|
|
63
|
+
nodes,
|
|
64
|
+
}: {
|
|
65
|
+
nodes: ChildNode[]
|
|
66
|
+
}): HtmlRenderable[] | Error {
|
|
67
|
+
const renderables: HtmlRenderable[] = []
|
|
68
|
+
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
if (node.type === ElementType.Text) {
|
|
71
|
+
const textNode = node as Text
|
|
72
|
+
renderables.push({
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: textNode.data,
|
|
75
|
+
})
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (node.type === ElementType.Tag) {
|
|
80
|
+
const element = node as Element
|
|
81
|
+
if (element.name !== 'button') {
|
|
82
|
+
return new Error(`Unsupported HTML tag: <${element.name}>`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const buttonRenderable = parseButtonElement({ element })
|
|
86
|
+
if (buttonRenderable instanceof Error) {
|
|
87
|
+
return buttonRenderable
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
renderables.push(buttonRenderable)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (node.type === ElementType.Comment) {
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return new Error(`Unsupported HTML node type: ${node.type}`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return renderables
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseButtonElement({
|
|
105
|
+
element,
|
|
106
|
+
}: {
|
|
107
|
+
element: Element
|
|
108
|
+
}): HtmlButtonRenderable | Error {
|
|
109
|
+
const id = element.attribs.id?.trim()
|
|
110
|
+
if (!id) {
|
|
111
|
+
return new Error('<button> is missing required id attribute')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const label = extractNodeText({ nodes: element.children })
|
|
115
|
+
.replace(/\s+/g, ' ')
|
|
116
|
+
.trim()
|
|
117
|
+
if (!label) {
|
|
118
|
+
return new Error(`<button id="${id}"> is missing label text`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const variant = normalizeButtonVariant({
|
|
122
|
+
value: element.attribs.variant,
|
|
123
|
+
})
|
|
124
|
+
if (variant instanceof Error) {
|
|
125
|
+
return variant
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
type: 'button',
|
|
130
|
+
id,
|
|
131
|
+
label,
|
|
132
|
+
variant,
|
|
133
|
+
disabled: 'disabled' in element.attribs,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeButtonVariant({
|
|
138
|
+
value,
|
|
139
|
+
}: {
|
|
140
|
+
value?: string
|
|
141
|
+
}): HtmlButtonRenderable['variant'] | Error {
|
|
142
|
+
if (!value) {
|
|
143
|
+
return 'secondary'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (value === 'secondary') {
|
|
147
|
+
return value
|
|
148
|
+
}
|
|
149
|
+
if (value === 'primary') {
|
|
150
|
+
return value
|
|
151
|
+
}
|
|
152
|
+
if (value === 'success') {
|
|
153
|
+
return value
|
|
154
|
+
}
|
|
155
|
+
if (value === 'danger') {
|
|
156
|
+
return value
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new Error(`Unsupported <button> variant: ${value}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractNodeText({
|
|
163
|
+
nodes,
|
|
164
|
+
}: {
|
|
165
|
+
nodes: ChildNode[]
|
|
166
|
+
}): string {
|
|
167
|
+
const parts: string[] = []
|
|
168
|
+
|
|
169
|
+
for (const node of nodes) {
|
|
170
|
+
if (node.type === ElementType.Text) {
|
|
171
|
+
parts.push((node as Text).data)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (node.type === ElementType.Tag) {
|
|
176
|
+
parts.push(extractNodeText({ nodes: (node as Element).children }))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return parts.join('')
|
|
181
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Optimizes oversized images before they reach the LLM API.
|
|
2
|
+
// Prevents "image dimensions exceed max allowed" errors from Anthropic/Google/OpenAI.
|
|
3
|
+
// Hooks into tool.execute.after (read) and experimental.chat.messages.transform (clipboard paste).
|
|
4
|
+
// Uses sharp to resize images > 2000px and compress images > 4MB.
|
|
5
|
+
// Vendored from https://github.com/kargnas/opencode-large-image-optimizer, simplified to zero-config.
|
|
6
|
+
|
|
7
|
+
import type { Plugin } from '@opencode-ai/plugin'
|
|
8
|
+
|
|
9
|
+
// Conservative safe floor for Anthropic many-image requests (20+ images = 2000px limit).
|
|
10
|
+
// OpenCode resends history so image counts accumulate across turns — 2000px is safest.
|
|
11
|
+
const MAX_DIMENSION = 2000
|
|
12
|
+
// 4MB safe margin under Anthropic's 5MB limit
|
|
13
|
+
const MAX_FILE_SIZE = 4 * 1024 * 1024
|
|
14
|
+
const SUPPORTED_MIMES = new Set([
|
|
15
|
+
'image/png',
|
|
16
|
+
'image/jpeg',
|
|
17
|
+
'image/jpg',
|
|
18
|
+
'image/gif',
|
|
19
|
+
'image/webp',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
// sharp is an optionalDependency — lazy-load to avoid breaking all plugins if missing
|
|
23
|
+
type SharpFn = (input?: Buffer | string) => import('sharp').Sharp
|
|
24
|
+
|
|
25
|
+
let sharpFactory: SharpFn | null | undefined
|
|
26
|
+
|
|
27
|
+
async function getSharp(): Promise<SharpFn | null> {
|
|
28
|
+
if (sharpFactory !== undefined) {
|
|
29
|
+
return sharpFactory
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const mod = await import('sharp')
|
|
33
|
+
// sharp uses `export =` so it lands on .default in ESM interop
|
|
34
|
+
const fn = typeof mod === 'function' ? mod : (mod as { default: SharpFn }).default
|
|
35
|
+
if (typeof fn === 'function') {
|
|
36
|
+
sharpFactory = fn
|
|
37
|
+
} else {
|
|
38
|
+
sharpFactory = null
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
sharpFactory = null
|
|
42
|
+
}
|
|
43
|
+
return sharpFactory
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractBase64Data(dataUrl: string): string | null {
|
|
47
|
+
const match = dataUrl.match(/^data:[^;]+;base64,(.+)$/s)
|
|
48
|
+
if (match?.[1]) {
|
|
49
|
+
return match[1]
|
|
50
|
+
}
|
|
51
|
+
// raw base64 string (no data: prefix)
|
|
52
|
+
if (/^[A-Za-z0-9+/]+={0,2}$/.test(dataUrl)) {
|
|
53
|
+
return dataUrl
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface OptimizeResult {
|
|
59
|
+
dataUrl: string
|
|
60
|
+
mime: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function optimizeImage(
|
|
64
|
+
dataUrl: string,
|
|
65
|
+
mime: string,
|
|
66
|
+
): Promise<OptimizeResult | null> {
|
|
67
|
+
const sharp = await getSharp()
|
|
68
|
+
if (!sharp) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rawBase64 = extractBase64Data(dataUrl)
|
|
73
|
+
if (!rawBase64) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const inputBuffer = Buffer.from(rawBase64, 'base64')
|
|
78
|
+
if (inputBuffer.length === 0) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const metadata = await sharp(inputBuffer).metadata()
|
|
83
|
+
const width = metadata.width || 0
|
|
84
|
+
const height = metadata.height || 0
|
|
85
|
+
if (width === 0 || height === 0) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const needsResize = width > MAX_DIMENSION || height > MAX_DIMENSION
|
|
90
|
+
const needsCompress = inputBuffer.length > MAX_FILE_SIZE
|
|
91
|
+
if (!needsResize && !needsCompress) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let pipeline = sharp(inputBuffer)
|
|
96
|
+
let outputMime = mime
|
|
97
|
+
|
|
98
|
+
if (needsResize) {
|
|
99
|
+
pipeline = pipeline.resize(MAX_DIMENSION, MAX_DIMENSION, {
|
|
100
|
+
fit: 'inside',
|
|
101
|
+
withoutEnlargement: true,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let outputBuffer = await pipeline.toBuffer()
|
|
106
|
+
|
|
107
|
+
// if still over 4MB, convert to JPEG with progressive quality reduction
|
|
108
|
+
if (outputBuffer.length > MAX_FILE_SIZE) {
|
|
109
|
+
for (const quality of [100, 90, 80, 70]) {
|
|
110
|
+
outputBuffer = await sharp(outputBuffer)
|
|
111
|
+
.jpeg({ quality, mozjpeg: true })
|
|
112
|
+
.toBuffer()
|
|
113
|
+
outputMime = 'image/jpeg'
|
|
114
|
+
if (outputBuffer.length <= MAX_FILE_SIZE) {
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
dataUrl: `data:${outputMime};base64,${outputBuffer.toString('base64')}`,
|
|
122
|
+
mime: outputMime,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// runtime guard — tool.execute.after output type doesn't declare attachments
|
|
127
|
+
function hasAttachments(
|
|
128
|
+
value: unknown,
|
|
129
|
+
): value is { attachments: Array<{ mime?: string; url?: string }> } {
|
|
130
|
+
return (
|
|
131
|
+
typeof value === 'object' &&
|
|
132
|
+
value !== null &&
|
|
133
|
+
'attachments' in value &&
|
|
134
|
+
Array.isArray((value as { attachments?: unknown }).attachments)
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const imageOptimizerPlugin: Plugin = async () => {
|
|
139
|
+
return {
|
|
140
|
+
'tool.execute.after': async (input, output) => {
|
|
141
|
+
const tool = input.tool.toLowerCase()
|
|
142
|
+
|
|
143
|
+
// read tool: optimize image attachments
|
|
144
|
+
if (tool === 'read' && hasAttachments(output)) {
|
|
145
|
+
for (const att of output.attachments) {
|
|
146
|
+
if (
|
|
147
|
+
!att.mime ||
|
|
148
|
+
!att.url ||
|
|
149
|
+
!SUPPORTED_MIMES.has(att.mime.toLowerCase())
|
|
150
|
+
) {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
const result = await optimizeImage(att.url, att.mime).catch(
|
|
154
|
+
() => null,
|
|
155
|
+
)
|
|
156
|
+
if (result) {
|
|
157
|
+
att.url = result.dataUrl
|
|
158
|
+
att.mime = result.mime
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// clipboard paste: optimize file parts in message history
|
|
166
|
+
'experimental.chat.messages.transform': async (_input, output) => {
|
|
167
|
+
if (!output.messages || !Array.isArray(output.messages)) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
for (const msg of output.messages) {
|
|
171
|
+
if (!msg.parts || !Array.isArray(msg.parts)) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
for (const part of msg.parts) {
|
|
175
|
+
if (part.type !== 'file') {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
if (!SUPPORTED_MIMES.has(part.mime.toLowerCase())) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
const result = await optimizeImage(part.url, part.mime).catch(
|
|
182
|
+
() => null,
|
|
183
|
+
)
|
|
184
|
+
if (result) {
|
|
185
|
+
part.url = result.dataUrl
|
|
186
|
+
part.mime = result.mime
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { imageOptimizerPlugin }
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Image processing utilities for Discord attachments.
|
|
2
|
+
// Uses sharp (optional) to resize large images and heic-convert (optional) for HEIC support.
|
|
3
|
+
// Falls back gracefully if dependencies are not available.
|
|
4
|
+
|
|
5
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
6
|
+
|
|
7
|
+
const logger = createLogger(LogPrefix.FORMATTING)
|
|
8
|
+
|
|
9
|
+
const MAX_DIMENSION = 1500
|
|
10
|
+
const HEIC_MIME_TYPES = [
|
|
11
|
+
'image/heic',
|
|
12
|
+
'image/heif',
|
|
13
|
+
'image/heic-sequence',
|
|
14
|
+
'image/heif-sequence',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
type SharpModule = typeof import('sharp')
|
|
18
|
+
type HeicConvertFn = (options: {
|
|
19
|
+
buffer: ArrayBufferLike
|
|
20
|
+
format: 'JPEG' | 'PNG'
|
|
21
|
+
quality?: number
|
|
22
|
+
}) => Promise<ArrayBuffer>
|
|
23
|
+
|
|
24
|
+
let sharpModule: SharpModule | null | undefined = undefined
|
|
25
|
+
let heicConvertModule: HeicConvertFn | null | undefined = undefined
|
|
26
|
+
|
|
27
|
+
async function tryLoadSharp(): Promise<SharpModule | null> {
|
|
28
|
+
if (sharpModule !== undefined) {
|
|
29
|
+
return sharpModule
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
sharpModule = (await import('sharp')).default as unknown as SharpModule
|
|
33
|
+
logger.log('sharp loaded successfully')
|
|
34
|
+
return sharpModule
|
|
35
|
+
} catch {
|
|
36
|
+
logger.log('sharp not available, images will be sent at original size')
|
|
37
|
+
sharpModule = null
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function tryLoadHeicConvert(): Promise<HeicConvertFn | null> {
|
|
43
|
+
if (heicConvertModule !== undefined) {
|
|
44
|
+
return heicConvertModule
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const mod = await import('heic-convert')
|
|
48
|
+
heicConvertModule = mod.default as HeicConvertFn
|
|
49
|
+
logger.log('heic-convert loaded successfully')
|
|
50
|
+
return heicConvertModule
|
|
51
|
+
} catch {
|
|
52
|
+
logger.log('heic-convert not available, HEIC images will be sent as-is')
|
|
53
|
+
heicConvertModule = null
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isHeicMime(mime: string): boolean {
|
|
59
|
+
return HEIC_MIME_TYPES.includes(mime.toLowerCase())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function processImage(
|
|
63
|
+
buffer: Buffer,
|
|
64
|
+
mime: string,
|
|
65
|
+
): Promise<{ buffer: Buffer; mime: string }> {
|
|
66
|
+
// Skip non-images (PDFs, etc.)
|
|
67
|
+
if (!mime.startsWith('image/')) {
|
|
68
|
+
return { buffer, mime }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let workingBuffer = buffer
|
|
72
|
+
let workingMime = mime
|
|
73
|
+
|
|
74
|
+
// Handle HEIC conversion first (before sharp, since sharp doesn't support HEIC)
|
|
75
|
+
if (isHeicMime(mime)) {
|
|
76
|
+
const heicConvert = await tryLoadHeicConvert()
|
|
77
|
+
if (heicConvert) {
|
|
78
|
+
try {
|
|
79
|
+
const outputArrayBuffer = await heicConvert({
|
|
80
|
+
buffer: workingBuffer.buffer.slice(
|
|
81
|
+
workingBuffer.byteOffset,
|
|
82
|
+
workingBuffer.byteOffset + workingBuffer.byteLength,
|
|
83
|
+
),
|
|
84
|
+
format: 'JPEG',
|
|
85
|
+
quality: 0.85,
|
|
86
|
+
})
|
|
87
|
+
workingBuffer = Buffer.from(outputArrayBuffer)
|
|
88
|
+
workingMime = 'image/jpeg'
|
|
89
|
+
logger.log(
|
|
90
|
+
`Converted HEIC to JPEG (${buffer.length} → ${workingBuffer.length} bytes)`,
|
|
91
|
+
)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error('Failed to convert HEIC, sending original:', error)
|
|
94
|
+
return { buffer, mime }
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// No heic-convert available, return original (LLM might not support it)
|
|
98
|
+
logger.log(
|
|
99
|
+
'HEIC image detected but heic-convert not available, sending as-is',
|
|
100
|
+
)
|
|
101
|
+
return { buffer, mime }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Now process with sharp (resize + ensure JPEG output)
|
|
106
|
+
const sharp = await tryLoadSharp()
|
|
107
|
+
if (!sharp) {
|
|
108
|
+
return { buffer: workingBuffer, mime: workingMime }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const image = sharp(workingBuffer)
|
|
113
|
+
const metadata = await image.metadata()
|
|
114
|
+
const { width, height } = metadata
|
|
115
|
+
|
|
116
|
+
const needsResize =
|
|
117
|
+
width && height && (width > MAX_DIMENSION || height > MAX_DIMENSION)
|
|
118
|
+
|
|
119
|
+
if (!needsResize) {
|
|
120
|
+
// Still convert to JPEG for consistency (unless already JPEG from HEIC conversion)
|
|
121
|
+
const outputBuffer = await image.jpeg({ quality: 85 }).toBuffer()
|
|
122
|
+
logger.log(
|
|
123
|
+
`Converted image to JPEG: ${width}x${height} (${outputBuffer.length} bytes)`,
|
|
124
|
+
)
|
|
125
|
+
return { buffer: outputBuffer, mime: 'image/jpeg' }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Resize and convert to JPEG
|
|
129
|
+
const outputBuffer = await image
|
|
130
|
+
.resize(MAX_DIMENSION, MAX_DIMENSION, {
|
|
131
|
+
fit: 'inside',
|
|
132
|
+
withoutEnlargement: true,
|
|
133
|
+
})
|
|
134
|
+
.jpeg({ quality: 85 })
|
|
135
|
+
.toBuffer()
|
|
136
|
+
|
|
137
|
+
logger.log(
|
|
138
|
+
`Resized image: ${width}x${height} → max ${MAX_DIMENSION}px (${outputBuffer.length} bytes)`,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return { buffer: outputBuffer, mime: 'image/jpeg' }
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.error(
|
|
144
|
+
'Failed to process image with sharp, using working buffer:',
|
|
145
|
+
error,
|
|
146
|
+
)
|
|
147
|
+
return { buffer: workingBuffer, mime: workingMime }
|
|
148
|
+
}
|
|
149
|
+
}
|