@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,185 @@
|
|
|
1
|
+
// Unnest code blocks from list items for Discord.
|
|
2
|
+
// Discord doesn't render code blocks inside lists, so this hoists them
|
|
3
|
+
// to root level while preserving list structure.
|
|
4
|
+
|
|
5
|
+
import { Lexer, type Token, type Tokens } from 'marked'
|
|
6
|
+
|
|
7
|
+
type Segment =
|
|
8
|
+
| { type: 'list-item'; prefix: string; content: string }
|
|
9
|
+
| { type: 'code'; content: string }
|
|
10
|
+
|
|
11
|
+
export function unnestCodeBlocksFromLists(markdown: string): string {
|
|
12
|
+
const lexer = new Lexer()
|
|
13
|
+
const tokens = lexer.lex(markdown)
|
|
14
|
+
|
|
15
|
+
const result: string[] = []
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
18
|
+
const token = tokens[i]!
|
|
19
|
+
const next = tokens[i + 1]
|
|
20
|
+
|
|
21
|
+
const chunk = (() => {
|
|
22
|
+
if (token.type === 'list') {
|
|
23
|
+
const segments = processListToken(token as Tokens.List)
|
|
24
|
+
return renderSegments(segments)
|
|
25
|
+
}
|
|
26
|
+
return token.raw
|
|
27
|
+
})()
|
|
28
|
+
|
|
29
|
+
if (!chunk) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const nextRaw = next?.raw ?? ''
|
|
34
|
+
const needsNewline =
|
|
35
|
+
nextRaw &&
|
|
36
|
+
!chunk.endsWith('\n') &&
|
|
37
|
+
typeof nextRaw === 'string' &&
|
|
38
|
+
!nextRaw.startsWith('\n')
|
|
39
|
+
|
|
40
|
+
result.push(needsNewline ? chunk + '\n' : chunk)
|
|
41
|
+
}
|
|
42
|
+
return result.join('')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function processListToken(list: Tokens.List): Segment[] {
|
|
46
|
+
const segments: Segment[] = []
|
|
47
|
+
const start =
|
|
48
|
+
typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1
|
|
49
|
+
const prefix = list.ordered ? (i: number) => `${start + i}. ` : () => '- '
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
52
|
+
const item = list.items[i]!
|
|
53
|
+
const itemSegments = processListItem(item, prefix(i))
|
|
54
|
+
segments.push(...itemSegments)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return segments
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function processListItem(item: Tokens.ListItem, prefix: string): Segment[] {
|
|
61
|
+
const segments: Segment[] = []
|
|
62
|
+
let currentText: string[] = []
|
|
63
|
+
// Track if we've seen a code block - text after code uses continuation prefix
|
|
64
|
+
let seenCodeBlock = false
|
|
65
|
+
|
|
66
|
+
const taskMarker = item.task ? (item.checked ? '[x] ' : '[ ] ') : ''
|
|
67
|
+
let wroteFirstListItem = false
|
|
68
|
+
|
|
69
|
+
const flushText = (): void => {
|
|
70
|
+
const rawText = currentText.join('')
|
|
71
|
+
const text = rawText.trimEnd()
|
|
72
|
+
if (text.trim()) {
|
|
73
|
+
// After a code block, use '-' as continuation prefix to avoid repeating numbers
|
|
74
|
+
const effectivePrefix = seenCodeBlock ? '- ' : prefix
|
|
75
|
+
const marker = !wroteFirstListItem ? taskMarker : ''
|
|
76
|
+
const normalizedText = normalizeListItemText({
|
|
77
|
+
text,
|
|
78
|
+
isTaskItem: item.task,
|
|
79
|
+
})
|
|
80
|
+
segments.push({
|
|
81
|
+
type: 'list-item',
|
|
82
|
+
prefix: effectivePrefix,
|
|
83
|
+
content: marker + normalizedText,
|
|
84
|
+
})
|
|
85
|
+
wroteFirstListItem = true
|
|
86
|
+
}
|
|
87
|
+
currentText = []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const token of item.tokens) {
|
|
91
|
+
if (token.type === 'code') {
|
|
92
|
+
flushText()
|
|
93
|
+
const codeToken = token as Tokens.Code
|
|
94
|
+
const lang = codeToken.lang || ''
|
|
95
|
+
segments.push({
|
|
96
|
+
type: 'code',
|
|
97
|
+
content: '```' + lang + '\n' + codeToken.text + '\n```\n',
|
|
98
|
+
})
|
|
99
|
+
seenCodeBlock = true
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (token.type === 'list') {
|
|
104
|
+
flushText()
|
|
105
|
+
// Recursively process nested list - segments bubble up
|
|
106
|
+
const nestedSegments = processListToken(token as Tokens.List)
|
|
107
|
+
segments.push(...nestedSegments)
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
currentText.push(extractText(token))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
flushText()
|
|
115
|
+
|
|
116
|
+
// If no segments were created (empty item), return empty
|
|
117
|
+
if (segments.length === 0) {
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If item had no code blocks (all segments are list-items from this level),
|
|
122
|
+
// return original raw to preserve formatting
|
|
123
|
+
const hasCode = segments.some((s) => s.type === 'code')
|
|
124
|
+
if (!hasCode) {
|
|
125
|
+
return [{ type: 'list-item', prefix: '', content: item.raw }]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return segments
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractText(token: Token): string {
|
|
132
|
+
// Prefer raw to preserve newlines and markdown markers.
|
|
133
|
+
if ('raw' in token && typeof token.raw === 'string') {
|
|
134
|
+
return token.raw
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (token.type === 'text') {
|
|
138
|
+
return (token as Tokens.Text).text
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return ''
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeListItemText({
|
|
145
|
+
text,
|
|
146
|
+
isTaskItem,
|
|
147
|
+
}: {
|
|
148
|
+
text: string
|
|
149
|
+
isTaskItem: boolean
|
|
150
|
+
}): string {
|
|
151
|
+
const withoutIndent = text.replace(/^\s+/, '')
|
|
152
|
+
if (!isTaskItem) {
|
|
153
|
+
return withoutIndent
|
|
154
|
+
}
|
|
155
|
+
return withoutIndent.replace(/^\[(?: |x|X)\]\s+/, '')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderSegments(segments: Segment[]): string {
|
|
159
|
+
const result: string[] = []
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < segments.length; i++) {
|
|
162
|
+
const segment = segments[i]!
|
|
163
|
+
const prev = segments[i - 1]
|
|
164
|
+
|
|
165
|
+
if (segment.type === 'code') {
|
|
166
|
+
// Add newline before code if previous was a list item
|
|
167
|
+
if (prev && prev.type === 'list-item') {
|
|
168
|
+
result.push('\n')
|
|
169
|
+
}
|
|
170
|
+
result.push(segment.content)
|
|
171
|
+
} else {
|
|
172
|
+
// list-item
|
|
173
|
+
if (segment.prefix) {
|
|
174
|
+
result.push(segment.prefix + segment.content + '\n')
|
|
175
|
+
} else {
|
|
176
|
+
// Raw content (no prefix means it's original raw)
|
|
177
|
+
// Ensure raw ends with newline for proper separation from next segment
|
|
178
|
+
const raw = segment.content.trimEnd()
|
|
179
|
+
result.push(raw + '\n')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result.join('').trimEnd()
|
|
185
|
+
}
|
package/src/upgrade.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Kimaki self-upgrade utilities.
|
|
2
|
+
// Detects the package manager used to install kimaki, checks npm for newer versions,
|
|
3
|
+
// and runs the global upgrade command. Used by both CLI `kimaki upgrade` and
|
|
4
|
+
// the Discord `/upgrade-and-restart` command, plus background auto-upgrade on startup.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { createLogger, LogPrefix } from './logger.js'
|
|
9
|
+
import { execAsync } from './worktrees.js'
|
|
10
|
+
|
|
11
|
+
const logger = createLogger(LogPrefix.CLI)
|
|
12
|
+
|
|
13
|
+
type Pm = 'bun' | 'pnpm' | 'npm'
|
|
14
|
+
|
|
15
|
+
// Detects which package manager globally installed kimaki, used to run the
|
|
16
|
+
// correct `<pm> i -g kimaki@latest` upgrade command.
|
|
17
|
+
//
|
|
18
|
+
// Detection order:
|
|
19
|
+
// 1. npm_config_user_agent — set by npx/bunx/pnpm dlx, reliable for those cases
|
|
20
|
+
// 2. Realpath of the running script — resolve symlinks and check if the path
|
|
21
|
+
// lives under a known PM global directory (e.g. ~/.bun, ~/Library/pnpm,
|
|
22
|
+
// /usr/local/lib/node_modules). Inspired by sindresorhus/global-directory.
|
|
23
|
+
// 3. process.versions.bun — if the runtime itself is Bun, likely bun ecosystem
|
|
24
|
+
// 4. Default to npm — safest fallback since npm is the most common global installer
|
|
25
|
+
export function detectPm(): Pm {
|
|
26
|
+
const ua = process.env.npm_config_user_agent
|
|
27
|
+
if (ua?.startsWith('bun/')) {
|
|
28
|
+
return 'bun'
|
|
29
|
+
}
|
|
30
|
+
if (ua?.startsWith('pnpm/')) {
|
|
31
|
+
return 'pnpm'
|
|
32
|
+
}
|
|
33
|
+
if (ua?.startsWith('npm/')) {
|
|
34
|
+
return 'npm'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const scriptPath = resolveScriptRealpath()
|
|
38
|
+
if (scriptPath) {
|
|
39
|
+
const p = scriptPath.toLowerCase()
|
|
40
|
+
// bun global installs live under ~/.bun or $BUN_INSTALL
|
|
41
|
+
if (p.includes('.bun/') || p.includes('/bun/install/')) {
|
|
42
|
+
return 'bun'
|
|
43
|
+
}
|
|
44
|
+
// pnpm global installs live under ~/Library/pnpm, ~/.local/share/pnpm, or $PNPM_HOME
|
|
45
|
+
if (p.includes('/pnpm/')) {
|
|
46
|
+
return 'pnpm'
|
|
47
|
+
}
|
|
48
|
+
// npm global installs typically live under lib/node_modules/kimaki without
|
|
49
|
+
// any pnpm or bun path segments, so if we reach here it's likely npm
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (process.versions.bun) {
|
|
53
|
+
return 'bun'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return 'npm'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveScriptRealpath(): string | null {
|
|
60
|
+
try {
|
|
61
|
+
const script = process.argv[1]
|
|
62
|
+
if (!script) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
return fs.realpathSync(script)
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getCurrentVersion(): string {
|
|
72
|
+
const require = createRequire(import.meta.url)
|
|
73
|
+
const pkg = require('../package.json') as { version: string }
|
|
74
|
+
return pkg.version
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getLatestNpmVersion(): Promise<string | null> {
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch('https://registry.npmjs.org/kimaki/latest', {
|
|
80
|
+
signal: AbortSignal.timeout(15_000),
|
|
81
|
+
})
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
const data = (await res.json()) as { version: string }
|
|
86
|
+
return data.version
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Returns the new version string if upgraded, null if already up to date.
|
|
93
|
+
export async function upgrade(): Promise<string | null> {
|
|
94
|
+
const current = getCurrentVersion()
|
|
95
|
+
const latest = await getLatestNpmVersion()
|
|
96
|
+
if (!latest) {
|
|
97
|
+
throw new Error('Failed to check latest version from npm')
|
|
98
|
+
}
|
|
99
|
+
if (current === latest) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pm = detectPm()
|
|
104
|
+
logger.log(`Upgrading kimaki from v${current} to v${latest} using ${pm}...`)
|
|
105
|
+
await execAsync(`${pm} i -g kimaki@latest`, { timeout: 120_000 })
|
|
106
|
+
|
|
107
|
+
return latest
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fire-and-forget background upgrade check on bot startup.
|
|
111
|
+
// Only upgrades if a newer version is available. Errors are silently ignored.
|
|
112
|
+
export async function backgroundUpgradeKimaki(): Promise<void> {
|
|
113
|
+
try {
|
|
114
|
+
const current = getCurrentVersion()
|
|
115
|
+
const latest = await getLatestNpmVersion()
|
|
116
|
+
if (!latest || current === latest) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pm = detectPm()
|
|
121
|
+
logger.debug(`Background kimaki upgrade started: v${current} -> v${latest}`)
|
|
122
|
+
await execAsync(`${pm} i -g kimaki@latest`, { timeout: 120_000 })
|
|
123
|
+
logger.debug(`Background kimaki upgrade completed: v${latest}`)
|
|
124
|
+
} catch {
|
|
125
|
+
// silently ignored, non-critical
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// General utility functions for the bot.
|
|
2
|
+
// Includes Discord OAuth URL generation, array deduplication,
|
|
3
|
+
// abort error detection, and date/time formatting helpers.
|
|
4
|
+
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
import { PermissionsBitField } from 'discord.js'
|
|
7
|
+
import type { BotMode } from './database.js'
|
|
8
|
+
import * as errore from 'errore'
|
|
9
|
+
|
|
10
|
+
type GenerateInstallUrlOptions = {
|
|
11
|
+
clientId: string
|
|
12
|
+
permissions?: bigint[]
|
|
13
|
+
scopes?: string[]
|
|
14
|
+
guildId?: string
|
|
15
|
+
disableGuildSelect?: boolean
|
|
16
|
+
state?: string
|
|
17
|
+
redirectUri?: string
|
|
18
|
+
responseType?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function generateBotInstallUrl({
|
|
22
|
+
clientId,
|
|
23
|
+
permissions = [
|
|
24
|
+
PermissionsBitField.Flags.ViewChannel,
|
|
25
|
+
PermissionsBitField.Flags.ManageChannels,
|
|
26
|
+
PermissionsBitField.Flags.SendMessages,
|
|
27
|
+
PermissionsBitField.Flags.SendMessagesInThreads,
|
|
28
|
+
PermissionsBitField.Flags.CreatePublicThreads,
|
|
29
|
+
PermissionsBitField.Flags.ManageThreads,
|
|
30
|
+
PermissionsBitField.Flags.ReadMessageHistory,
|
|
31
|
+
PermissionsBitField.Flags.AddReactions,
|
|
32
|
+
PermissionsBitField.Flags.ManageMessages,
|
|
33
|
+
PermissionsBitField.Flags.UseExternalEmojis,
|
|
34
|
+
PermissionsBitField.Flags.AttachFiles,
|
|
35
|
+
PermissionsBitField.Flags.Connect,
|
|
36
|
+
PermissionsBitField.Flags.Speak,
|
|
37
|
+
PermissionsBitField.Flags.ManageRoles,
|
|
38
|
+
PermissionsBitField.Flags.ManageEvents,
|
|
39
|
+
PermissionsBitField.Flags.CreateEvents,
|
|
40
|
+
],
|
|
41
|
+
scopes = ['bot', 'applications.commands', 'identify', 'email'],
|
|
42
|
+
guildId,
|
|
43
|
+
disableGuildSelect = false,
|
|
44
|
+
state,
|
|
45
|
+
redirectUri,
|
|
46
|
+
responseType,
|
|
47
|
+
}: GenerateInstallUrlOptions): string {
|
|
48
|
+
const permissionsBitField = new PermissionsBitField(permissions)
|
|
49
|
+
const permissionsValue = permissionsBitField.bitfield.toString()
|
|
50
|
+
|
|
51
|
+
const url = new URL('https://discord.com/api/oauth2/authorize')
|
|
52
|
+
url.searchParams.set('client_id', clientId)
|
|
53
|
+
url.searchParams.set('permissions', permissionsValue)
|
|
54
|
+
url.searchParams.set('scope', scopes.join(' '))
|
|
55
|
+
|
|
56
|
+
if (guildId) {
|
|
57
|
+
url.searchParams.set('guild_id', guildId)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (disableGuildSelect) {
|
|
61
|
+
url.searchParams.set('disable_guild_select', 'true')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (state) {
|
|
65
|
+
url.searchParams.set('state', state)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (redirectUri) {
|
|
69
|
+
url.searchParams.set('redirect_uri', redirectUri)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (responseType) {
|
|
73
|
+
url.searchParams.set('response_type', responseType)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return url.toString()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const KIMAKI_GATEWAY_APP_ID =
|
|
80
|
+
process.env.KIMAKI_GATEWAY_APP_ID || '1477605701202481173'
|
|
81
|
+
export const KIMAKI_WEBSITE_URL = process.env.KIMAKI_WEBSITE_URL || 'https://kimaki.xyz'
|
|
82
|
+
|
|
83
|
+
export function generateDiscordInstallUrlForBot({
|
|
84
|
+
appId,
|
|
85
|
+
mode,
|
|
86
|
+
clientId,
|
|
87
|
+
clientSecret,
|
|
88
|
+
gatewayCallbackUrl,
|
|
89
|
+
reachableUrl,
|
|
90
|
+
}: {
|
|
91
|
+
appId: string
|
|
92
|
+
mode: BotMode
|
|
93
|
+
clientId: string | null
|
|
94
|
+
clientSecret: string | null
|
|
95
|
+
/** Optional external URL to redirect to after OAuth completes instead of the
|
|
96
|
+
* default success page. The website appends ?guild_id=<id> before redirecting. */
|
|
97
|
+
gatewayCallbackUrl?: string
|
|
98
|
+
/** When set (KIMAKI_INTERNET_REACHABLE_URL), the website stores this URL in
|
|
99
|
+
* gateway_clients.reachable_url so the gateway-proxy connects outbound. */
|
|
100
|
+
reachableUrl?: string
|
|
101
|
+
}): Error | string {
|
|
102
|
+
if (mode !== 'gateway') {
|
|
103
|
+
return generateBotInstallUrl({ clientId: appId })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!clientId || !clientSecret) {
|
|
107
|
+
return new Error('Gateway credentials are missing from local database')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// In gateway mode, redirect to the website's /discord-install route.
|
|
111
|
+
// This initiates the better-auth OAuth flow with clientId/clientSecret
|
|
112
|
+
// as additionalData, which better-auth stores in its verification table
|
|
113
|
+
// and recovers after Discord redirects back to the callback.
|
|
114
|
+
// Use a kimaki-specific callback field name to avoid ambiguity with
|
|
115
|
+
// better-auth's own callbackURL state field.
|
|
116
|
+
const url = new URL(`${KIMAKI_WEBSITE_URL}/discord-install`)
|
|
117
|
+
url.searchParams.set('clientId', clientId)
|
|
118
|
+
url.searchParams.set('clientSecret', clientSecret)
|
|
119
|
+
if (gatewayCallbackUrl) {
|
|
120
|
+
url.searchParams.set('kimakiCallbackUrl', gatewayCallbackUrl)
|
|
121
|
+
}
|
|
122
|
+
if (reachableUrl) {
|
|
123
|
+
url.searchParams.set('reachableUrl', reachableUrl)
|
|
124
|
+
}
|
|
125
|
+
return url.toString()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function deduplicateByKey<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
|
|
129
|
+
const seen = new Set<K>()
|
|
130
|
+
return arr.filter((item) => {
|
|
131
|
+
const key = keyFn(item)
|
|
132
|
+
if (seen.has(key)) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
seen.add(key)
|
|
136
|
+
return true
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Delegates to errore.isAbortError (walks cause chain for AbortError instances),
|
|
141
|
+
// then falls back to opencode server-specific abort patterns that aren't
|
|
142
|
+
// errore.AbortError but still represent aborted operations.
|
|
143
|
+
export function isAbortError(error: unknown): error is Error {
|
|
144
|
+
if (errore.isAbortError(error)) return true
|
|
145
|
+
if (!(error instanceof Error)) return false
|
|
146
|
+
return (
|
|
147
|
+
error.name === 'MessageAbortedError' ||
|
|
148
|
+
error.message?.includes('aborted') === true
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
153
|
+
|
|
154
|
+
const TIME_DIVISIONS: Array<{
|
|
155
|
+
amount: number
|
|
156
|
+
name: Intl.RelativeTimeFormatUnit
|
|
157
|
+
}> = [
|
|
158
|
+
{ amount: 60, name: 'seconds' },
|
|
159
|
+
{ amount: 60, name: 'minutes' },
|
|
160
|
+
{ amount: 24, name: 'hours' },
|
|
161
|
+
{ amount: 7, name: 'days' },
|
|
162
|
+
{ amount: 4.34524, name: 'weeks' },
|
|
163
|
+
{ amount: 12, name: 'months' },
|
|
164
|
+
{ amount: Number.POSITIVE_INFINITY, name: 'years' },
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
export function formatDistanceToNow(date: Date): string {
|
|
168
|
+
let duration = (date.getTime() - Date.now()) / 1000
|
|
169
|
+
|
|
170
|
+
for (const division of TIME_DIVISIONS) {
|
|
171
|
+
if (Math.abs(duration) < division.amount) {
|
|
172
|
+
return rtf.format(Math.round(duration), division.name)
|
|
173
|
+
}
|
|
174
|
+
duration /= division.amount
|
|
175
|
+
}
|
|
176
|
+
return rtf.format(Math.round(duration), 'years')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dtf = new Intl.DateTimeFormat('en-US', {
|
|
180
|
+
month: 'short',
|
|
181
|
+
day: 'numeric',
|
|
182
|
+
year: 'numeric',
|
|
183
|
+
hour: 'numeric',
|
|
184
|
+
minute: '2-digit',
|
|
185
|
+
hour12: true,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
export function formatDateTime(date: Date): string {
|
|
189
|
+
return dtf.format(date)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Comprehensive ANSI escape sequence regex covering CSI, OSC, and related sequences.
|
|
193
|
+
// Valid string terminator sequences are BEL, ESC\, and 0x9c.
|
|
194
|
+
const ANSI_REGEX = (() => {
|
|
195
|
+
const ST = '(?:\\u0007|\\u001B\\u005C|\\u009C)'
|
|
196
|
+
const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`
|
|
197
|
+
const csi =
|
|
198
|
+
'[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]'
|
|
199
|
+
return new RegExp(`${osc}|${csi}`, 'g')
|
|
200
|
+
})()
|
|
201
|
+
|
|
202
|
+
export function stripAnsi(str: string): string {
|
|
203
|
+
return str.replace(ANSI_REGEX, '')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function abbreviatePath(fullPath: string): string {
|
|
207
|
+
const home = os.homedir()
|
|
208
|
+
if (fullPath.startsWith(home)) {
|
|
209
|
+
return '~' + fullPath.slice(home.length)
|
|
210
|
+
}
|
|
211
|
+
return fullPath
|
|
212
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Voice attachment detection helpers.
|
|
2
|
+
// Normalizes Discord attachment heuristics for voice-message detection so
|
|
3
|
+
// message routing, transcription, and empty-prompt guards all agree even when
|
|
4
|
+
// Discord omits contentType on uploaded audio attachments.
|
|
5
|
+
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
const VOICE_ATTACHMENT_EXTENSIONS = new Set<string>([
|
|
9
|
+
'.m4a',
|
|
10
|
+
'.mp3',
|
|
11
|
+
'.mp4',
|
|
12
|
+
'.oga',
|
|
13
|
+
'.ogg',
|
|
14
|
+
'.opus',
|
|
15
|
+
'.wav',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export type VoiceAttachmentLike = {
|
|
19
|
+
contentType?: string | null
|
|
20
|
+
name?: string | null
|
|
21
|
+
duration?: number | null
|
|
22
|
+
waveform?: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getVoiceAttachmentMatchReason(
|
|
26
|
+
attachment: VoiceAttachmentLike,
|
|
27
|
+
): string | null {
|
|
28
|
+
const contentType = attachment.contentType?.trim().toLowerCase() || ''
|
|
29
|
+
if (contentType.startsWith('audio/')) {
|
|
30
|
+
return `contentType:${contentType}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof attachment.duration === 'number' && attachment.duration > 0) {
|
|
34
|
+
return `duration:${attachment.duration}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (attachment.waveform?.trim()) {
|
|
38
|
+
return 'waveform'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const extension = path.extname(attachment.name || '').toLowerCase()
|
|
42
|
+
if (VOICE_ATTACHMENT_EXTENSIONS.has(extension)) {
|
|
43
|
+
return `extension:${extension}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isVoiceAttachment(attachment: VoiceAttachmentLike): boolean {
|
|
50
|
+
return getVoiceAttachmentMatchReason(attachment) !== null
|
|
51
|
+
}
|