@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,9 @@
|
|
|
1
|
+
// Thin re-export shim for backward compatibility.
|
|
2
|
+
// Logic lives in:
|
|
3
|
+
// - session-handler/thread-session-runtime.ts (runtime class + registry)
|
|
4
|
+
// - session-handler/thread-runtime-state.ts (state transitions)
|
|
5
|
+
// - session-handler/model-utils.ts (getDefaultModel, types)
|
|
6
|
+
// - session-handler/agent-utils.ts (resolveValidatedAgentPreference)
|
|
7
|
+
// New code should import from the specific module directly.
|
|
8
|
+
export { getDefaultModel, } from './session-handler/model-utils.js';
|
|
9
|
+
export { resolveValidatedAgentPreference } from './session-handler/agent-utils.js';
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Session search helpers for kimaki CLI commands.
|
|
2
|
+
// Parses string/regex queries and builds readable snippets from matched content.
|
|
3
|
+
export function parseSessionSearchPattern(query) {
|
|
4
|
+
const trimmedQuery = query.trim();
|
|
5
|
+
if (!trimmedQuery) {
|
|
6
|
+
return new Error('Search query cannot be empty');
|
|
7
|
+
}
|
|
8
|
+
const regexMatch = trimmedQuery.match(/^\/([\s\S]+)\/([a-z]*)$/);
|
|
9
|
+
if (!regexMatch) {
|
|
10
|
+
return {
|
|
11
|
+
mode: 'literal',
|
|
12
|
+
raw: trimmedQuery,
|
|
13
|
+
normalizedNeedle: trimmedQuery.toLowerCase(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const pattern = regexMatch[1] || '';
|
|
17
|
+
const flags = regexMatch[2] || '';
|
|
18
|
+
try {
|
|
19
|
+
return {
|
|
20
|
+
mode: 'regex',
|
|
21
|
+
raw: trimmedQuery,
|
|
22
|
+
regex: new RegExp(pattern, flags),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return new Error(`Invalid regex query "${trimmedQuery}": ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function findFirstSessionSearchHit({ text, searchPattern, }) {
|
|
30
|
+
if (searchPattern.mode === 'literal') {
|
|
31
|
+
const index = text.toLowerCase().indexOf(searchPattern.normalizedNeedle);
|
|
32
|
+
if (index < 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
index,
|
|
37
|
+
length: searchPattern.raw.length,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
searchPattern.regex.lastIndex = 0;
|
|
41
|
+
const match = searchPattern.regex.exec(text);
|
|
42
|
+
if (!match || match.index < 0) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
index: match.index,
|
|
47
|
+
length: Math.max(match[0]?.length || 0, 1),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function buildSessionSearchSnippet({ text, hit, contextLength = 90, }) {
|
|
51
|
+
const start = Math.max(0, hit.index - contextLength);
|
|
52
|
+
const end = Math.min(text.length, hit.index + hit.length + contextLength);
|
|
53
|
+
const prefix = start > 0 ? '...' : '';
|
|
54
|
+
const suffix = end < text.length ? '...' : '';
|
|
55
|
+
const body = text
|
|
56
|
+
.slice(start, end)
|
|
57
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
58
|
+
.replace(/\s+/g, ' ')
|
|
59
|
+
.trim();
|
|
60
|
+
return `${prefix}${body}${suffix}`;
|
|
61
|
+
}
|
|
62
|
+
function stringifyUnknown(value) {
|
|
63
|
+
if (value === undefined || value === null) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return JSON.stringify(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return String(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function getPartSearchTexts(part) {
|
|
77
|
+
switch (part.type) {
|
|
78
|
+
case 'text':
|
|
79
|
+
return part.text ? [part.text] : [];
|
|
80
|
+
case 'reasoning':
|
|
81
|
+
return part.text ? [part.text] : [];
|
|
82
|
+
case 'tool': {
|
|
83
|
+
const inputText = stringifyUnknown(part.state.input);
|
|
84
|
+
const outputText = part.state.status === 'completed'
|
|
85
|
+
? stringifyUnknown(part.state.output)
|
|
86
|
+
: part.state.status === 'error'
|
|
87
|
+
? part.state.error || ''
|
|
88
|
+
: '';
|
|
89
|
+
return [`tool:${part.tool}`, inputText, outputText].filter((entry) => {
|
|
90
|
+
return entry.trim().length > 0;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
case 'file':
|
|
94
|
+
return [part.filename || '', part.url || ''].filter((entry) => {
|
|
95
|
+
return entry.trim().length > 0;
|
|
96
|
+
});
|
|
97
|
+
default:
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Tests for session search query parsing and snippet matching helpers.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { buildSessionSearchSnippet, findFirstSessionSearchHit, parseSessionSearchPattern, } from './session-search.js';
|
|
4
|
+
describe('session search helpers', () => {
|
|
5
|
+
test('returns error for invalid regex query', () => {
|
|
6
|
+
const parsed = parseSessionSearchPattern('/(unclosed/');
|
|
7
|
+
expect(parsed).toBeInstanceOf(Error);
|
|
8
|
+
});
|
|
9
|
+
test('returns snippets that include the matched substring', () => {
|
|
10
|
+
const cases = [
|
|
11
|
+
{
|
|
12
|
+
query: 'panic',
|
|
13
|
+
text: 'There was a PANIC in production',
|
|
14
|
+
expectedSubstring: 'PANIC',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
query: '/error\\s+42/i',
|
|
18
|
+
text: 'Request failed with ERROR 42 in worker',
|
|
19
|
+
expectedSubstring: 'ERROR 42',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
cases.forEach(({ query, text, expectedSubstring }) => {
|
|
23
|
+
const parsed = parseSessionSearchPattern(query);
|
|
24
|
+
if (parsed instanceof Error) {
|
|
25
|
+
throw parsed;
|
|
26
|
+
}
|
|
27
|
+
const hit = findFirstSessionSearchHit({ text, searchPattern: parsed });
|
|
28
|
+
expect(hit).toBeDefined();
|
|
29
|
+
if (!hit) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const snippet = buildSessionSearchSnippet({
|
|
33
|
+
text,
|
|
34
|
+
hit,
|
|
35
|
+
contextLength: 8,
|
|
36
|
+
});
|
|
37
|
+
expect(snippet.toUpperCase()).toContain(expectedSubstring.toUpperCase());
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Unit tests for deriveThreadNameFromSessionTitle — the pure helper that
|
|
2
|
+
// decides whether (and how) to rename a Discord thread based on an
|
|
3
|
+
// OpenCode session title. Kept focused and deterministic; no Discord mocks.
|
|
4
|
+
import { describe, test, expect } from 'vitest';
|
|
5
|
+
import { deriveThreadNameFromSessionTitle } from './session-handler/thread-session-runtime.js';
|
|
6
|
+
describe('deriveThreadNameFromSessionTitle', () => {
|
|
7
|
+
test('returns trimmed title for plain thread', () => {
|
|
8
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
9
|
+
sessionTitle: ' Fix auth bug ',
|
|
10
|
+
currentName: 'fix the auth',
|
|
11
|
+
})).toMatchInlineSnapshot(`"Fix auth bug"`);
|
|
12
|
+
});
|
|
13
|
+
test('preserves worktree prefix from current name', () => {
|
|
14
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
15
|
+
sessionTitle: 'Refactor queue',
|
|
16
|
+
currentName: '⬦ refactor queue old',
|
|
17
|
+
})).toMatchInlineSnapshot(`"⬦ Refactor queue"`);
|
|
18
|
+
});
|
|
19
|
+
test('ignores placeholder "New Session -" titles', () => {
|
|
20
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
21
|
+
sessionTitle: 'New Session - 2025-01-02',
|
|
22
|
+
currentName: 'whatever',
|
|
23
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
24
|
+
});
|
|
25
|
+
test('ignores case-insensitive placeholder titles', () => {
|
|
26
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
27
|
+
sessionTitle: 'new session -abc',
|
|
28
|
+
currentName: 'whatever',
|
|
29
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
30
|
+
});
|
|
31
|
+
test('returns undefined when candidate already matches current name', () => {
|
|
32
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
33
|
+
sessionTitle: 'Fix auth bug',
|
|
34
|
+
currentName: 'Fix auth bug',
|
|
35
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
36
|
+
});
|
|
37
|
+
test('returns undefined when candidate (with worktree prefix) already matches', () => {
|
|
38
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
39
|
+
sessionTitle: 'Refactor queue',
|
|
40
|
+
currentName: '⬦ Refactor queue',
|
|
41
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
42
|
+
});
|
|
43
|
+
test('truncates to 100 chars including worktree prefix', () => {
|
|
44
|
+
const result = deriveThreadNameFromSessionTitle({
|
|
45
|
+
sessionTitle: 'x'.repeat(200),
|
|
46
|
+
currentName: '⬦ seed',
|
|
47
|
+
});
|
|
48
|
+
expect(result?.length).toMatchInlineSnapshot(`100`);
|
|
49
|
+
expect(result?.startsWith('⬦ ')).toMatchInlineSnapshot(`true`);
|
|
50
|
+
});
|
|
51
|
+
test('truncates to 100 chars without prefix', () => {
|
|
52
|
+
const result = deriveThreadNameFromSessionTitle({
|
|
53
|
+
sessionTitle: 'y'.repeat(200),
|
|
54
|
+
currentName: 'seed',
|
|
55
|
+
});
|
|
56
|
+
expect(result?.length).toMatchInlineSnapshot(`100`);
|
|
57
|
+
});
|
|
58
|
+
test('returns undefined for empty string', () => {
|
|
59
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
60
|
+
sessionTitle: '',
|
|
61
|
+
currentName: 'seed',
|
|
62
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
63
|
+
});
|
|
64
|
+
test('returns undefined for whitespace-only title', () => {
|
|
65
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
66
|
+
sessionTitle: ' ',
|
|
67
|
+
currentName: 'seed',
|
|
68
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
69
|
+
});
|
|
70
|
+
test('returns undefined for null/undefined title', () => {
|
|
71
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
72
|
+
sessionTitle: null,
|
|
73
|
+
currentName: 'seed',
|
|
74
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
75
|
+
expect(deriveThreadNameFromSessionTitle({
|
|
76
|
+
sessionTitle: undefined,
|
|
77
|
+
currentName: 'seed',
|
|
78
|
+
})).toMatchInlineSnapshot(`undefined`);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Cross-platform startup service registration for kimaki daemon.
|
|
2
|
+
// Vendored from startup-run (MIT, github.com/vilicvane/startup-run) with
|
|
3
|
+
// significant simplifications: no abstract classes, no fs-extra, no winreg
|
|
4
|
+
// npm dep, no separate daemon process (kimaki's bin.ts already handles
|
|
5
|
+
// respawn/crash-loop). Just writes/deletes the platform service file.
|
|
6
|
+
//
|
|
7
|
+
// macOS: ~/Library/LaunchAgents/xyz.kimaki.plist (launchd)
|
|
8
|
+
// Linux: ~/.config/autostart/kimaki.desktop (XDG autostart)
|
|
9
|
+
// Windows: HKCU\Software\Microsoft\Windows\CurrentVersion\Run (registry)
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { execAsync } from './worktrees.js';
|
|
14
|
+
const SERVICE_NAME = 'xyz.kimaki';
|
|
15
|
+
function getServiceFilePath() {
|
|
16
|
+
switch (process.platform) {
|
|
17
|
+
case 'darwin':
|
|
18
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`);
|
|
19
|
+
case 'linux':
|
|
20
|
+
return path.join(os.homedir(), '.config', 'autostart', 'kimaki.desktop');
|
|
21
|
+
case 'win32':
|
|
22
|
+
// No file — registry key, return a descriptive string for status display
|
|
23
|
+
return 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\kimaki';
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function escapeXml(value) {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/&/g, '&')
|
|
31
|
+
.replace(/</g, '<')
|
|
32
|
+
.replace(/>/g, '>');
|
|
33
|
+
}
|
|
34
|
+
// Shell-escape a string for use in a Linux .desktop Exec= line.
|
|
35
|
+
// Wraps in double quotes if it contains spaces or special chars.
|
|
36
|
+
function shellEscape(value) {
|
|
37
|
+
if (/^[a-zA-Z0-9._/=-]+$/.test(value)) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
41
|
+
}
|
|
42
|
+
function buildMacOSPlist({ command, args, }) {
|
|
43
|
+
const segments = [command, ...args];
|
|
44
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
45
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
46
|
+
<plist version="1.0">
|
|
47
|
+
<dict>
|
|
48
|
+
<key>Label</key>
|
|
49
|
+
<string>${SERVICE_NAME}</string>
|
|
50
|
+
<key>ProgramArguments</key>
|
|
51
|
+
<array>
|
|
52
|
+
${segments.map((s) => ` <string>${escapeXml(s)}</string>`).join('\n')}
|
|
53
|
+
</array>
|
|
54
|
+
<key>RunAtLoad</key>
|
|
55
|
+
<true/>
|
|
56
|
+
<key>KeepAlive</key>
|
|
57
|
+
<false/>
|
|
58
|
+
</dict>
|
|
59
|
+
</plist>
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function buildLinuxDesktop({ command, args, }) {
|
|
63
|
+
const execLine = [command, ...args].map(shellEscape).join(' ');
|
|
64
|
+
return `[Desktop Entry]
|
|
65
|
+
Type=Application
|
|
66
|
+
Version=1.0
|
|
67
|
+
Name=Kimaki
|
|
68
|
+
Comment=Kimaki Discord Bot Daemon
|
|
69
|
+
Exec=${execLine}
|
|
70
|
+
StartupNotify=false
|
|
71
|
+
Terminal=false
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Register kimaki to start on user login.
|
|
76
|
+
* Writes the appropriate service file for the current platform.
|
|
77
|
+
*/
|
|
78
|
+
export async function enableStartupService({ command, args, }) {
|
|
79
|
+
const platform = process.platform;
|
|
80
|
+
if (platform === 'darwin') {
|
|
81
|
+
const filePath = getServiceFilePath();
|
|
82
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
fs.writeFileSync(filePath, buildMacOSPlist({ command, args }));
|
|
84
|
+
}
|
|
85
|
+
else if (platform === 'linux') {
|
|
86
|
+
const filePath = getServiceFilePath();
|
|
87
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
88
|
+
fs.writeFileSync(filePath, buildLinuxDesktop({ command, args }));
|
|
89
|
+
}
|
|
90
|
+
else if (platform === 'win32') {
|
|
91
|
+
const execLine = [command, ...args]
|
|
92
|
+
.map((s) => {
|
|
93
|
+
return s.includes(' ') ? `"${s}"` : s;
|
|
94
|
+
})
|
|
95
|
+
.join(' ');
|
|
96
|
+
await execAsync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v kimaki /t REG_SZ /d "${execLine}" /f`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Unregister kimaki from user login startup.
|
|
104
|
+
*/
|
|
105
|
+
export async function disableStartupService() {
|
|
106
|
+
const platform = process.platform;
|
|
107
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
108
|
+
const filePath = getServiceFilePath();
|
|
109
|
+
if (fs.existsSync(filePath)) {
|
|
110
|
+
fs.unlinkSync(filePath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (platform === 'win32') {
|
|
114
|
+
await execAsync(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v kimaki /f`).catch(() => {
|
|
115
|
+
// Key may not exist, ignore
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if kimaki is registered as a startup service.
|
|
124
|
+
*/
|
|
125
|
+
export async function isStartupServiceEnabled() {
|
|
126
|
+
const platform = process.platform;
|
|
127
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
128
|
+
return fs.existsSync(getServiceFilePath());
|
|
129
|
+
}
|
|
130
|
+
if (platform === 'win32') {
|
|
131
|
+
const result = await execAsync(`reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v kimaki`).catch(() => {
|
|
132
|
+
return null;
|
|
133
|
+
});
|
|
134
|
+
return result !== null;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get a human-readable description of the service location for status display.
|
|
140
|
+
*/
|
|
141
|
+
export function getServiceLocationDescription() {
|
|
142
|
+
const platform = process.platform;
|
|
143
|
+
if (platform === 'darwin') {
|
|
144
|
+
return `launchd: ${getServiceFilePath()}`;
|
|
145
|
+
}
|
|
146
|
+
if (platform === 'linux') {
|
|
147
|
+
return `XDG autostart: ${getServiceFilePath()}`;
|
|
148
|
+
}
|
|
149
|
+
if (platform === 'win32') {
|
|
150
|
+
return `registry: ${getServiceFilePath()}`;
|
|
151
|
+
}
|
|
152
|
+
return `unsupported platform: ${platform}`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Measures time-to-ready for the kimaki Discord bot startup.
|
|
2
|
+
// Used as a baseline to track startup performance and guide optimizations
|
|
3
|
+
// for scale-to-zero deployments where cold start time is critical.
|
|
4
|
+
//
|
|
5
|
+
// Measures each phase independently:
|
|
6
|
+
// 1. Hrana server start (DB + lock port)
|
|
7
|
+
// 2. Database init (Prisma connect via HTTP)
|
|
8
|
+
// 3. Discord.js client creation + login (Gateway READY)
|
|
9
|
+
// 4. startDiscordBot (event handlers + markDiscordGatewayReady)
|
|
10
|
+
// 5. OpenCode server startup (spawn + health poll)
|
|
11
|
+
// 6. Total wall-clock time from zero to "bot ready"
|
|
12
|
+
//
|
|
13
|
+
// Uses discord-digital-twin so Gateway READY is instant (no real Discord).
|
|
14
|
+
// OpenCode startup uses deterministic provider (no real LLM).
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import url from 'node:url';
|
|
18
|
+
import { describe, test, expect, afterAll } from 'vitest';
|
|
19
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
|
|
20
|
+
import { DigitalDiscord } from 'discord-digital-twin/src';
|
|
21
|
+
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
22
|
+
import { setDataDir } from './config.js';
|
|
23
|
+
import { startDiscordBot } from './discord-bot.js';
|
|
24
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, } from './database.js';
|
|
25
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
26
|
+
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
27
|
+
import { chooseLockPort, cleanupTestSessions, initTestGitRepo } from './test-utils.js';
|
|
28
|
+
function createRunDirectories() {
|
|
29
|
+
const root = path.resolve(process.cwd(), 'tmp', 'startup-time-e2e');
|
|
30
|
+
fs.mkdirSync(root, { recursive: true });
|
|
31
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
|
|
32
|
+
const projectDirectory = path.join(root, 'project');
|
|
33
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
34
|
+
initTestGitRepo(projectDirectory);
|
|
35
|
+
return { root, dataDir, projectDirectory };
|
|
36
|
+
}
|
|
37
|
+
function createDiscordJsClient({ restUrl }) {
|
|
38
|
+
return new Client({
|
|
39
|
+
intents: [
|
|
40
|
+
GatewayIntentBits.Guilds,
|
|
41
|
+
GatewayIntentBits.GuildMessages,
|
|
42
|
+
GatewayIntentBits.MessageContent,
|
|
43
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
44
|
+
],
|
|
45
|
+
partials: [
|
|
46
|
+
Partials.Channel,
|
|
47
|
+
Partials.Message,
|
|
48
|
+
Partials.User,
|
|
49
|
+
Partials.ThreadMember,
|
|
50
|
+
],
|
|
51
|
+
rest: {
|
|
52
|
+
api: restUrl,
|
|
53
|
+
version: '10',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function createMinimalMatchers() {
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
id: 'startup-test-reply',
|
|
61
|
+
priority: 10,
|
|
62
|
+
when: {
|
|
63
|
+
lastMessageRole: 'user',
|
|
64
|
+
rawPromptIncludes: 'startup-test',
|
|
65
|
+
},
|
|
66
|
+
then: {
|
|
67
|
+
parts: [
|
|
68
|
+
{ type: 'stream-start', warnings: [] },
|
|
69
|
+
{ type: 'text-start', id: 'startup-reply' },
|
|
70
|
+
{ type: 'text-delta', id: 'startup-reply', delta: 'ok' },
|
|
71
|
+
{ type: 'text-end', id: 'startup-reply' },
|
|
72
|
+
{
|
|
73
|
+
type: 'finish',
|
|
74
|
+
finishReason: 'stop',
|
|
75
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
const TEST_USER_ID = '900000000000000777';
|
|
83
|
+
const TEXT_CHANNEL_ID = '900000000000000778';
|
|
84
|
+
describe('startup time measurement', () => {
|
|
85
|
+
let directories;
|
|
86
|
+
let discord;
|
|
87
|
+
let botClient = null;
|
|
88
|
+
const testStartTime = Date.now();
|
|
89
|
+
afterAll(async () => {
|
|
90
|
+
if (directories) {
|
|
91
|
+
await cleanupTestSessions({
|
|
92
|
+
projectDirectory: directories.projectDirectory,
|
|
93
|
+
testStartTime,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (botClient) {
|
|
97
|
+
botClient.destroy();
|
|
98
|
+
}
|
|
99
|
+
await Promise.all([
|
|
100
|
+
stopOpencodeServer().catch(() => { }),
|
|
101
|
+
closeDatabase().catch(() => { }),
|
|
102
|
+
stopHranaServer().catch(() => { }),
|
|
103
|
+
discord?.stop().catch(() => { }),
|
|
104
|
+
]);
|
|
105
|
+
delete process.env['KIMAKI_LOCK_PORT'];
|
|
106
|
+
delete process.env['KIMAKI_DB_URL'];
|
|
107
|
+
if (directories) {
|
|
108
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
test('measures per-phase startup timings', async () => {
|
|
112
|
+
directories = createRunDirectories();
|
|
113
|
+
const lockPort = chooseLockPort({ key: 'startup-time-e2e' });
|
|
114
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
|
|
115
|
+
setDataDir(directories.dataDir);
|
|
116
|
+
const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
|
|
117
|
+
discord = new DigitalDiscord({
|
|
118
|
+
guild: {
|
|
119
|
+
name: 'Startup Time Guild',
|
|
120
|
+
ownerId: TEST_USER_ID,
|
|
121
|
+
},
|
|
122
|
+
channels: [
|
|
123
|
+
{
|
|
124
|
+
id: TEXT_CHANNEL_ID,
|
|
125
|
+
name: 'startup-time',
|
|
126
|
+
type: ChannelType.GuildText,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
users: [
|
|
130
|
+
{
|
|
131
|
+
id: TEST_USER_ID,
|
|
132
|
+
username: 'startup-tester',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
136
|
+
});
|
|
137
|
+
await discord.start();
|
|
138
|
+
// Write deterministic opencode config
|
|
139
|
+
const providerNpm = url
|
|
140
|
+
.pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
|
|
141
|
+
.toString();
|
|
142
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
143
|
+
providerName: 'deterministic-provider',
|
|
144
|
+
providerNpm,
|
|
145
|
+
model: 'deterministic-v2',
|
|
146
|
+
smallModel: 'deterministic-v2',
|
|
147
|
+
settings: {
|
|
148
|
+
strict: false,
|
|
149
|
+
matchers: createMinimalMatchers(),
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
153
|
+
// ── Phase timings ──
|
|
154
|
+
const totalStart = performance.now();
|
|
155
|
+
// Phase 1: Hrana server
|
|
156
|
+
const hranaStart = performance.now();
|
|
157
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
|
|
158
|
+
const hranaResult = await startHranaServer({ dbPath });
|
|
159
|
+
if (hranaResult instanceof Error) {
|
|
160
|
+
throw hranaResult;
|
|
161
|
+
}
|
|
162
|
+
process.env['KIMAKI_DB_URL'] = hranaResult;
|
|
163
|
+
const hranaMs = performance.now() - hranaStart;
|
|
164
|
+
// Phase 2: Database init
|
|
165
|
+
const dbStart = performance.now();
|
|
166
|
+
await initDatabase();
|
|
167
|
+
await setBotToken(discord.botUserId, discord.botToken);
|
|
168
|
+
await setChannelDirectory({
|
|
169
|
+
channelId: TEXT_CHANNEL_ID,
|
|
170
|
+
directory: directories.projectDirectory,
|
|
171
|
+
channelType: 'text',
|
|
172
|
+
});
|
|
173
|
+
const dbMs = performance.now() - dbStart;
|
|
174
|
+
// Phase 3+4: Discord.js login + startDiscordBot
|
|
175
|
+
// In the real cli.ts flow, login happens first (line 2077), then
|
|
176
|
+
// startDiscordBot is called with the already-logged-in client (line 2130).
|
|
177
|
+
// startDiscordBot calls login() again internally (line 1069) which is
|
|
178
|
+
// a no-op on already-connected clients. We measure them together since
|
|
179
|
+
// that's the real critical path.
|
|
180
|
+
const loginStart = performance.now();
|
|
181
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl });
|
|
182
|
+
// Don't pre-login — let startDiscordBot handle login internally.
|
|
183
|
+
// This avoids the double-login overhead that inflates measurements.
|
|
184
|
+
const loginMs = Math.round(performance.now() - loginStart);
|
|
185
|
+
const botStart = performance.now();
|
|
186
|
+
await startDiscordBot({
|
|
187
|
+
token: discord.botToken,
|
|
188
|
+
appId: discord.botUserId,
|
|
189
|
+
discordClient: botClient,
|
|
190
|
+
});
|
|
191
|
+
const botMs = performance.now() - botStart;
|
|
192
|
+
// Phase 5: OpenCode server startup (biggest bottleneck)
|
|
193
|
+
const opencodeStart = performance.now();
|
|
194
|
+
const opencodeResult = await initializeOpencodeForDirectory(directories.projectDirectory);
|
|
195
|
+
if (opencodeResult instanceof Error) {
|
|
196
|
+
throw opencodeResult;
|
|
197
|
+
}
|
|
198
|
+
const opencodeMs = performance.now() - opencodeStart;
|
|
199
|
+
const totalMs = performance.now() - totalStart;
|
|
200
|
+
const timings = {
|
|
201
|
+
hranaServerMs: Math.round(hranaMs),
|
|
202
|
+
databaseInitMs: Math.round(dbMs),
|
|
203
|
+
discordLoginMs: Math.round(loginMs),
|
|
204
|
+
startDiscordBotMs: Math.round(botMs),
|
|
205
|
+
opencodeServerMs: Math.round(opencodeMs),
|
|
206
|
+
totalMs: Math.round(totalMs),
|
|
207
|
+
};
|
|
208
|
+
// Print timings for CI/local visibility
|
|
209
|
+
console.log('\n┌─────────────────────────────────────────────┐');
|
|
210
|
+
console.log('│ Kimaki Startup Time Breakdown │');
|
|
211
|
+
console.log('├─────────────────────────────────────────────┤');
|
|
212
|
+
console.log(`│ Hrana server: ${String(timings.hranaServerMs).padStart(6)} ms │`);
|
|
213
|
+
console.log(`│ Database init: ${String(timings.databaseInitMs).padStart(6)} ms │`);
|
|
214
|
+
console.log(`│ Discord.js login: ${String(timings.discordLoginMs).padStart(6)} ms │`);
|
|
215
|
+
console.log(`│ startDiscordBot: ${String(timings.startDiscordBotMs).padStart(6)} ms │`);
|
|
216
|
+
console.log(`│ OpenCode server: ${String(timings.opencodeServerMs).padStart(6)} ms │`);
|
|
217
|
+
console.log('├─────────────────────────────────────────────┤');
|
|
218
|
+
console.log(`│ TOTAL: ${String(timings.totalMs).padStart(6)} ms │`);
|
|
219
|
+
console.log('└─────────────────────────────────────────────┘\n');
|
|
220
|
+
// Sanity assertions — these are baselines, not targets yet.
|
|
221
|
+
// Each phase should complete (no infinite hang).
|
|
222
|
+
expect(timings.hranaServerMs).toBeLessThan(5_000);
|
|
223
|
+
expect(timings.databaseInitMs).toBeLessThan(5_000);
|
|
224
|
+
expect(timings.discordLoginMs).toBeLessThan(10_000);
|
|
225
|
+
expect(timings.startDiscordBotMs).toBeLessThan(5_000);
|
|
226
|
+
expect(timings.opencodeServerMs).toBeLessThan(30_000);
|
|
227
|
+
expect(timings.totalMs).toBeLessThan(60_000);
|
|
228
|
+
// Verify the bot is actually functional by sending a message
|
|
229
|
+
// and getting a response (validates the full pipeline works)
|
|
230
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
231
|
+
content: 'startup-test ping',
|
|
232
|
+
});
|
|
233
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
234
|
+
timeout: 10_000,
|
|
235
|
+
});
|
|
236
|
+
const reply = await discord.thread(thread.id).waitForBotReply({
|
|
237
|
+
timeout: 30_000,
|
|
238
|
+
});
|
|
239
|
+
expect(reply.content.length).toBeGreaterThan(0);
|
|
240
|
+
expect(thread.id.length).toBeGreaterThan(0);
|
|
241
|
+
}, 120_000);
|
|
242
|
+
test('measures parallel startup (discord + opencode simultaneously)', async () => {
|
|
243
|
+
// This test reuses the infrastructure from test 1 (hrana, db already up)
|
|
244
|
+
// to measure what happens when we run Discord login + OpenCode in parallel.
|
|
245
|
+
// In a fresh cold start, hrana+db init would add ~50ms on top.
|
|
246
|
+
// Stop opencode server from test 1 so we get a fresh measurement
|
|
247
|
+
await stopOpencodeServer().catch(() => { });
|
|
248
|
+
// Destroy and recreate bot client for a clean login measurement
|
|
249
|
+
if (botClient) {
|
|
250
|
+
botClient.destroy();
|
|
251
|
+
botClient = null;
|
|
252
|
+
}
|
|
253
|
+
// ── Parallel phase: Discord login + OpenCode server simultaneously ──
|
|
254
|
+
const parallelStart = performance.now();
|
|
255
|
+
const [discordResult, opencodeResult] = await Promise.all([
|
|
256
|
+
// Discord path: create client, login, start bot
|
|
257
|
+
(async () => {
|
|
258
|
+
const loginStart = performance.now();
|
|
259
|
+
const client = createDiscordJsClient({ restUrl: discord.restUrl });
|
|
260
|
+
await startDiscordBot({
|
|
261
|
+
token: discord.botToken,
|
|
262
|
+
appId: discord.botUserId,
|
|
263
|
+
discordClient: client,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
client,
|
|
267
|
+
totalMs: Math.round(performance.now() - loginStart),
|
|
268
|
+
};
|
|
269
|
+
})(),
|
|
270
|
+
// OpenCode path: spawn server + wait for health
|
|
271
|
+
(async () => {
|
|
272
|
+
const start = performance.now();
|
|
273
|
+
const result = await initializeOpencodeForDirectory(directories.projectDirectory);
|
|
274
|
+
if (result instanceof Error) {
|
|
275
|
+
throw result;
|
|
276
|
+
}
|
|
277
|
+
return { ms: Math.round(performance.now() - start) };
|
|
278
|
+
})(),
|
|
279
|
+
]);
|
|
280
|
+
const parallelMs = Math.round(performance.now() - parallelStart);
|
|
281
|
+
botClient = discordResult.client;
|
|
282
|
+
console.log('\n┌─────────────────────────────────────────────┐');
|
|
283
|
+
console.log('│ Parallel Startup Time Breakdown │');
|
|
284
|
+
console.log('├─────────────────────────────────────────────┤');
|
|
285
|
+
console.log(`│ Discord login+bot: ${String(discordResult.totalMs).padStart(6)} ms │`);
|
|
286
|
+
console.log(`│ OpenCode server: ${String(opencodeResult.ms).padStart(6)} ms │`);
|
|
287
|
+
console.log('├─────────────────────────────────────────────┤');
|
|
288
|
+
console.log(`│ PARALLEL TOTAL: ${String(parallelMs).padStart(6)} ms │`);
|
|
289
|
+
console.log(`│ (vs sequential: ${String(discordResult.totalMs + opencodeResult.ms).padStart(6)} ms) │`);
|
|
290
|
+
console.log('└─────────────────────────────────────────────┘\n');
|
|
291
|
+
// Parallel total should be dominated by the slower path,
|
|
292
|
+
// not the sum of both.
|
|
293
|
+
const maxSingle = Math.max(discordResult.totalMs, opencodeResult.ms);
|
|
294
|
+
expect(parallelMs).toBeLessThan(maxSingle + 500);
|
|
295
|
+
}, 120_000);
|
|
296
|
+
});
|