@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,533 @@
|
|
|
1
|
+
// E2e capture tests for generating real OpenCode session-event JSONL fixtures.
|
|
2
|
+
// Uses opencode-cached-provider + Gemini to record real tool/lifecycle streams
|
|
3
|
+
// (task, interruption, permission, action buttons, and question flows).
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
|
|
7
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
|
|
8
|
+
import { DigitalDiscord } from 'discord-digital-twin/src';
|
|
9
|
+
import { CachedOpencodeProviderProxy } from 'opencode-cached-provider';
|
|
10
|
+
import { setDataDir } from './config.js';
|
|
11
|
+
import { store } from './store.js';
|
|
12
|
+
import { startDiscordBot } from './discord-bot.js';
|
|
13
|
+
import { closeDatabase, getChannelVerbosity, initDatabase, setBotToken, setChannelDirectory, setChannelVerbosity, } from './database.js';
|
|
14
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
15
|
+
import { chooseLockPort, cleanupTestSessions, initTestGitRepo } from './test-utils.js';
|
|
16
|
+
import { waitForBotMessageContaining, waitForBotReplyAfterUserMessage } from './test-utils.js';
|
|
17
|
+
import { stopOpencodeServer } from './opencode.js';
|
|
18
|
+
import { disposeRuntime, pendingPermissions } from './session-handler/thread-session-runtime.js';
|
|
19
|
+
import { pendingActionButtonContexts } from './commands/action-buttons.js';
|
|
20
|
+
import { pendingQuestionContexts } from './commands/ask-question.js';
|
|
21
|
+
const geminiApiKey = process.env['GEMINI_API_KEY'] ||
|
|
22
|
+
process.env['GOOGLE_GENERATIVE_AI_API_KEY'] ||
|
|
23
|
+
'';
|
|
24
|
+
const geminiModel = process.env['GEMINI_FLASH_MODEL'] || 'gemini-2.5-flash';
|
|
25
|
+
const shouldRunRealCapture = geminiApiKey.length > 0 && process.env['KIMAKI_RUN_REAL_EVENT_CAPTURE'] === '1';
|
|
26
|
+
const realCaptureTest = shouldRunRealCapture ? test : test.skip;
|
|
27
|
+
const TEST_USER_ID = '200000000000003001';
|
|
28
|
+
const TEXT_CHANNEL_ID = '200000000000003002';
|
|
29
|
+
function createRunDirectories() {
|
|
30
|
+
const root = path.resolve(process.cwd(), 'tmp', 'event-stream-real-capture-e2e');
|
|
31
|
+
fs.mkdirSync(root, { recursive: true });
|
|
32
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
|
|
33
|
+
const projectDirectory = path.join(root, 'project');
|
|
34
|
+
const providerCacheDbPath = path.join(root, 'provider-cache.db');
|
|
35
|
+
const sessionEventsDir = path.join(root, 'opencode-session-events');
|
|
36
|
+
const fixtureOutputDir = path.resolve(process.cwd(), 'src', 'session-handler', 'event-stream-fixtures');
|
|
37
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
38
|
+
initTestGitRepo(projectDirectory);
|
|
39
|
+
fs.mkdirSync(sessionEventsDir, { recursive: true });
|
|
40
|
+
return {
|
|
41
|
+
root,
|
|
42
|
+
dataDir,
|
|
43
|
+
projectDirectory,
|
|
44
|
+
providerCacheDbPath,
|
|
45
|
+
sessionEventsDir,
|
|
46
|
+
fixtureOutputDir,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createDiscordJsClient({ restUrl }) {
|
|
50
|
+
return new Client({
|
|
51
|
+
intents: [
|
|
52
|
+
GatewayIntentBits.Guilds,
|
|
53
|
+
GatewayIntentBits.GuildMessages,
|
|
54
|
+
GatewayIntentBits.MessageContent,
|
|
55
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
56
|
+
],
|
|
57
|
+
partials: [
|
|
58
|
+
Partials.Channel,
|
|
59
|
+
Partials.Message,
|
|
60
|
+
Partials.User,
|
|
61
|
+
Partials.ThreadMember,
|
|
62
|
+
],
|
|
63
|
+
rest: {
|
|
64
|
+
api: restUrl,
|
|
65
|
+
version: '10',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function readJsonlEvents(filePath) {
|
|
70
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
71
|
+
const lines = content.split('\n').filter((line) => {
|
|
72
|
+
return line.trim().length > 0;
|
|
73
|
+
});
|
|
74
|
+
return lines.map((line) => {
|
|
75
|
+
return JSON.parse(line);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function hasToolEvent({ events, tool }) {
|
|
79
|
+
return events.some((line) => {
|
|
80
|
+
if (line.event.type !== 'message.part.updated') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const part = line.event.properties.part;
|
|
84
|
+
if (part.type !== 'tool') {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return part.tool === tool;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function listJsonlFiles(directory) {
|
|
91
|
+
return fs.readdirSync(directory).filter((name) => {
|
|
92
|
+
return name.endsWith('.jsonl');
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async function waitForNewOrUpdatedSessionLog({ directory, before, timeoutMs, }) {
|
|
96
|
+
const start = Date.now();
|
|
97
|
+
while (Date.now() - start < timeoutMs) {
|
|
98
|
+
const files = listJsonlFiles(directory);
|
|
99
|
+
const changedFiles = files.filter((fileName) => {
|
|
100
|
+
const filePath = path.join(directory, fileName);
|
|
101
|
+
const stat = fs.statSync(filePath);
|
|
102
|
+
const previous = before.get(fileName);
|
|
103
|
+
if (!previous) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return stat.size > previous.size || stat.mtimeMs > previous.mtimeMs;
|
|
107
|
+
});
|
|
108
|
+
if (changedFiles.length > 0) {
|
|
109
|
+
const newest = [...changedFiles].sort((a, b) => {
|
|
110
|
+
const aMtime = fs.statSync(path.join(directory, a)).mtimeMs;
|
|
111
|
+
const bMtime = fs.statSync(path.join(directory, b)).mtimeMs;
|
|
112
|
+
return bMtime - aMtime;
|
|
113
|
+
})[0];
|
|
114
|
+
if (newest) {
|
|
115
|
+
return path.join(directory, newest);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await new Promise((resolve) => {
|
|
119
|
+
setTimeout(resolve, 200);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw new Error('Timed out waiting for changed session event log file');
|
|
123
|
+
}
|
|
124
|
+
async function waitForPendingPermission({ threadId, timeoutMs, }) {
|
|
125
|
+
const start = Date.now();
|
|
126
|
+
while (Date.now() - start < timeoutMs) {
|
|
127
|
+
const perms = pendingPermissions.get(threadId);
|
|
128
|
+
const first = perms ? [...perms.values()][0] : undefined;
|
|
129
|
+
if (first?.contextHash && first.messageId) {
|
|
130
|
+
return { contextHash: first.contextHash, messageId: first.messageId };
|
|
131
|
+
}
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
setTimeout(resolve, 100);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
throw new Error('Timed out waiting for pending permission context');
|
|
137
|
+
}
|
|
138
|
+
async function waitForPendingActionButtons({ threadId, timeoutMs, }) {
|
|
139
|
+
const start = Date.now();
|
|
140
|
+
while (Date.now() - start < timeoutMs) {
|
|
141
|
+
const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
|
|
142
|
+
return context.thread.id === threadId && !context.resolved && Boolean(context.messageId);
|
|
143
|
+
});
|
|
144
|
+
if (entry && entry[1].messageId) {
|
|
145
|
+
return { contextHash: entry[0], messageId: entry[1].messageId };
|
|
146
|
+
}
|
|
147
|
+
await new Promise((resolve) => {
|
|
148
|
+
setTimeout(resolve, 100);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
throw new Error('Timed out waiting for pending action buttons context');
|
|
152
|
+
}
|
|
153
|
+
async function waitForPendingQuestion({ discord, threadId, timeoutMs, }) {
|
|
154
|
+
const start = Date.now();
|
|
155
|
+
while (Date.now() - start < timeoutMs) {
|
|
156
|
+
const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
|
|
157
|
+
return context.thread.id === threadId;
|
|
158
|
+
});
|
|
159
|
+
if (entry) {
|
|
160
|
+
const [contextHash, context] = entry;
|
|
161
|
+
const questionMessage = await discord.thread(threadId).waitForMessage({
|
|
162
|
+
timeout: 10_000,
|
|
163
|
+
predicate: (message) => {
|
|
164
|
+
return message.author.id === discord.botUserId
|
|
165
|
+
&& message.content.includes('Choose one option');
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
if (questionMessage) {
|
|
169
|
+
return {
|
|
170
|
+
contextHash,
|
|
171
|
+
questionMessage,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await new Promise((resolve) => {
|
|
176
|
+
setTimeout(resolve, 100);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
throw new Error('Timed out waiting for pending question context');
|
|
180
|
+
}
|
|
181
|
+
describe('real event stream capture fixtures (cached provider)', () => {
|
|
182
|
+
const directories = createRunDirectories();
|
|
183
|
+
let lockPort = 0;
|
|
184
|
+
let previousDefaultVerbosity = null;
|
|
185
|
+
let testStartTime = Date.now();
|
|
186
|
+
let botClient = null;
|
|
187
|
+
const proxy = new CachedOpencodeProviderProxy({
|
|
188
|
+
cacheDbPath: directories.providerCacheDbPath,
|
|
189
|
+
targetBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
190
|
+
apiKey: geminiApiKey,
|
|
191
|
+
cacheMethods: ['POST'],
|
|
192
|
+
});
|
|
193
|
+
const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
|
|
194
|
+
const discord = new DigitalDiscord({
|
|
195
|
+
guild: {
|
|
196
|
+
name: 'Real Event Capture Guild',
|
|
197
|
+
ownerId: TEST_USER_ID,
|
|
198
|
+
},
|
|
199
|
+
channels: [
|
|
200
|
+
{
|
|
201
|
+
id: TEXT_CHANNEL_ID,
|
|
202
|
+
name: 'real-event-capture',
|
|
203
|
+
type: ChannelType.GuildText,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
users: [
|
|
207
|
+
{
|
|
208
|
+
id: TEST_USER_ID,
|
|
209
|
+
username: 'real-capture-user',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
213
|
+
});
|
|
214
|
+
async function captureFixture({ fixtureName, beforeFiles, assertEvents, }) {
|
|
215
|
+
const newLogPath = await waitForNewOrUpdatedSessionLog({
|
|
216
|
+
directory: directories.sessionEventsDir,
|
|
217
|
+
before: beforeFiles,
|
|
218
|
+
timeoutMs: 120_000,
|
|
219
|
+
});
|
|
220
|
+
const fixturePath = path.join(directories.fixtureOutputDir, fixtureName);
|
|
221
|
+
fs.copyFileSync(newLogPath, fixturePath);
|
|
222
|
+
const events = readJsonlEvents(fixturePath);
|
|
223
|
+
assertEvents(events);
|
|
224
|
+
}
|
|
225
|
+
function getSessionLogState() {
|
|
226
|
+
const files = listJsonlFiles(directories.sessionEventsDir);
|
|
227
|
+
return new Map(files.map((fileName) => {
|
|
228
|
+
const stat = fs.statSync(path.join(directories.sessionEventsDir, fileName));
|
|
229
|
+
return [fileName, { size: stat.size, mtimeMs: stat.mtimeMs }];
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
beforeAll(async () => {
|
|
233
|
+
testStartTime = Date.now();
|
|
234
|
+
lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID });
|
|
235
|
+
listJsonlFiles(directories.sessionEventsDir).forEach((fileName) => {
|
|
236
|
+
fs.rmSync(path.join(directories.sessionEventsDir, fileName), {
|
|
237
|
+
force: true,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
|
|
241
|
+
process.env['KIMAKI_LOG_OPENCODE_SESSION_EVENTS'] = '1';
|
|
242
|
+
process.env['KIMAKI_OPENCODE_SESSION_EVENTS_DIR'] = directories.sessionEventsDir;
|
|
243
|
+
setDataDir(directories.dataDir);
|
|
244
|
+
previousDefaultVerbosity = store.getState().defaultVerbosity;
|
|
245
|
+
store.setState({ defaultVerbosity: 'tools_and_text' });
|
|
246
|
+
await Promise.all([proxy.start(), discord.start()]);
|
|
247
|
+
const opencodeConfig = proxy.buildOpencodeConfig({
|
|
248
|
+
providerName: 'cached-google-real-events',
|
|
249
|
+
providerNpm: '@ai-sdk/google',
|
|
250
|
+
model: geminiModel,
|
|
251
|
+
smallModel: geminiModel,
|
|
252
|
+
});
|
|
253
|
+
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
254
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
|
|
255
|
+
const hranaResult = await startHranaServer({ dbPath });
|
|
256
|
+
if (hranaResult instanceof Error) {
|
|
257
|
+
throw hranaResult;
|
|
258
|
+
}
|
|
259
|
+
process.env['KIMAKI_DB_URL'] = hranaResult;
|
|
260
|
+
await initDatabase();
|
|
261
|
+
await setBotToken(discord.botUserId, discord.botToken);
|
|
262
|
+
await setChannelDirectory({
|
|
263
|
+
channelId: TEXT_CHANNEL_ID,
|
|
264
|
+
directory: directories.projectDirectory,
|
|
265
|
+
channelType: 'text',
|
|
266
|
+
});
|
|
267
|
+
await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text');
|
|
268
|
+
expect(await getChannelVerbosity(TEXT_CHANNEL_ID)).toBe('tools_and_text');
|
|
269
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl });
|
|
270
|
+
await startDiscordBot({
|
|
271
|
+
token: discord.botToken,
|
|
272
|
+
appId: discord.botUserId,
|
|
273
|
+
discordClient: botClient,
|
|
274
|
+
});
|
|
275
|
+
}, 180_000);
|
|
276
|
+
afterEach(async () => {
|
|
277
|
+
[...pendingActionButtonContexts.values()].forEach((context) => {
|
|
278
|
+
clearTimeout(context.timer);
|
|
279
|
+
});
|
|
280
|
+
pendingActionButtonContexts.clear();
|
|
281
|
+
pendingQuestionContexts.clear();
|
|
282
|
+
pendingPermissions.clear();
|
|
283
|
+
const threadIds = [...store.getState().threads.keys()];
|
|
284
|
+
threadIds.forEach((threadId) => {
|
|
285
|
+
disposeRuntime(threadId);
|
|
286
|
+
});
|
|
287
|
+
await cleanupTestSessions({
|
|
288
|
+
projectDirectory: directories.projectDirectory,
|
|
289
|
+
testStartTime,
|
|
290
|
+
});
|
|
291
|
+
}, 180_000);
|
|
292
|
+
afterAll(async () => {
|
|
293
|
+
await cleanupTestSessions({
|
|
294
|
+
projectDirectory: directories.projectDirectory,
|
|
295
|
+
testStartTime,
|
|
296
|
+
});
|
|
297
|
+
if (botClient) {
|
|
298
|
+
botClient.destroy();
|
|
299
|
+
}
|
|
300
|
+
await stopOpencodeServer();
|
|
301
|
+
await Promise.all([
|
|
302
|
+
closeDatabase().catch(() => {
|
|
303
|
+
return;
|
|
304
|
+
}),
|
|
305
|
+
stopHranaServer().catch(() => {
|
|
306
|
+
return;
|
|
307
|
+
}),
|
|
308
|
+
proxy.stop().catch(() => {
|
|
309
|
+
return;
|
|
310
|
+
}),
|
|
311
|
+
discord.stop().catch(() => {
|
|
312
|
+
return;
|
|
313
|
+
}),
|
|
314
|
+
]);
|
|
315
|
+
delete process.env['KIMAKI_LOCK_PORT'];
|
|
316
|
+
delete process.env['KIMAKI_DB_URL'];
|
|
317
|
+
delete process.env['KIMAKI_LOG_OPENCODE_SESSION_EVENTS'];
|
|
318
|
+
delete process.env['KIMAKI_OPENCODE_SESSION_EVENTS_DIR'];
|
|
319
|
+
if (previousDefaultVerbosity) {
|
|
320
|
+
store.setState({ defaultVerbosity: previousDefaultVerbosity });
|
|
321
|
+
}
|
|
322
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
323
|
+
}, 180_000);
|
|
324
|
+
realCaptureTest('capture real task flow fixture', async () => {
|
|
325
|
+
const beforeFiles = getSessionLogState();
|
|
326
|
+
const prompt = 'REAL_FIXTURE_TASK_NORMAL. First response MUST be exactly one tool call: tool `task` with {"description":"inspect repository","subagent_type":"general","prompt":"Read this repository and return exactly: task-subagent-done"}. Do not answer with plain text before the tool call. After the task result returns, respond with exactly: task-normal-done.';
|
|
327
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
328
|
+
content: prompt,
|
|
329
|
+
});
|
|
330
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
331
|
+
timeout: 120_000,
|
|
332
|
+
predicate: (t) => {
|
|
333
|
+
return t.name?.includes('REAL_FIXTURE_TASK_NORMAL') ?? false;
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
await waitForBotMessageContaining({
|
|
337
|
+
discord,
|
|
338
|
+
threadId: thread.id,
|
|
339
|
+
userId: TEST_USER_ID,
|
|
340
|
+
text: '┣ task',
|
|
341
|
+
timeout: 300_000,
|
|
342
|
+
});
|
|
343
|
+
await waitForBotMessageContaining({
|
|
344
|
+
discord,
|
|
345
|
+
threadId: thread.id,
|
|
346
|
+
userId: TEST_USER_ID,
|
|
347
|
+
text: 'task-normal-done',
|
|
348
|
+
timeout: 300_000,
|
|
349
|
+
});
|
|
350
|
+
await captureFixture({
|
|
351
|
+
fixtureName: 'real-session-task-normal.jsonl',
|
|
352
|
+
beforeFiles,
|
|
353
|
+
assertEvents: (events) => {
|
|
354
|
+
expect(events.length).toBeGreaterThan(0);
|
|
355
|
+
expect(hasToolEvent({ events, tool: 'task' })).toBe(true);
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
}, 900_000);
|
|
359
|
+
realCaptureTest('capture real task interruption fixture', async () => {
|
|
360
|
+
const beforeFiles = getSessionLogState();
|
|
361
|
+
const setupPrompt = 'REAL_FIXTURE_TASK_INTERRUPT_START. First response MUST call tool `task` with {"description":"long analysis","subagent_type":"general","prompt":"Perform a long analysis over many files and produce extensive notes"}. Do not send plain text before the tool call.';
|
|
362
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
363
|
+
content: setupPrompt,
|
|
364
|
+
});
|
|
365
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
366
|
+
timeout: 120_000,
|
|
367
|
+
predicate: (t) => {
|
|
368
|
+
return t.name?.includes('REAL_FIXTURE_TASK_INTERRUPT_START') ?? false;
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
await waitForBotMessageContaining({
|
|
372
|
+
discord,
|
|
373
|
+
threadId: thread.id,
|
|
374
|
+
userId: TEST_USER_ID,
|
|
375
|
+
text: '┣ task',
|
|
376
|
+
timeout: 300_000,
|
|
377
|
+
});
|
|
378
|
+
await discord.thread(thread.id).user(TEST_USER_ID).sendMessage({
|
|
379
|
+
content: 'REAL_FIXTURE_TASK_INTERRUPT_FOLLOWUP. Stop and reply with exactly: task-interrupt-done.',
|
|
380
|
+
});
|
|
381
|
+
await waitForBotReplyAfterUserMessage({
|
|
382
|
+
discord,
|
|
383
|
+
threadId: thread.id,
|
|
384
|
+
userId: TEST_USER_ID,
|
|
385
|
+
userMessageIncludes: 'REAL_FIXTURE_TASK_INTERRUPT_FOLLOWUP',
|
|
386
|
+
timeout: 300_000,
|
|
387
|
+
});
|
|
388
|
+
await captureFixture({
|
|
389
|
+
fixtureName: 'real-session-task-user-interruption.jsonl',
|
|
390
|
+
beforeFiles,
|
|
391
|
+
assertEvents: (events) => {
|
|
392
|
+
expect(events.length).toBeGreaterThan(0);
|
|
393
|
+
expect(hasToolEvent({ events, tool: 'task' })).toBe(true);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
}, 900_000);
|
|
397
|
+
realCaptureTest('capture real permission fixture for external path access', async () => {
|
|
398
|
+
const beforeFiles = getSessionLogState();
|
|
399
|
+
const prompt = 'REAL_FIXTURE_PERMISSION_EXTERNAL. Use bash (hasSideEffect false) to read this file outside the workspace: /Users/morse/.zprofile. Then summarize the first line.';
|
|
400
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
401
|
+
content: prompt,
|
|
402
|
+
});
|
|
403
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
404
|
+
timeout: 120_000,
|
|
405
|
+
predicate: (t) => {
|
|
406
|
+
return t.name?.includes('REAL_FIXTURE_PERMISSION_EXTERNAL') ?? false;
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const pending = await waitForPendingPermission({
|
|
410
|
+
threadId: thread.id,
|
|
411
|
+
timeoutMs: 300_000,
|
|
412
|
+
});
|
|
413
|
+
const interaction = await discord.thread(thread.id).user(TEST_USER_ID).clickButton({
|
|
414
|
+
messageId: pending.messageId,
|
|
415
|
+
customId: `permission_once:${pending.contextHash}`,
|
|
416
|
+
});
|
|
417
|
+
await discord.thread(thread.id).waitForInteractionAck({
|
|
418
|
+
interactionId: interaction.id,
|
|
419
|
+
timeout: 30_000,
|
|
420
|
+
});
|
|
421
|
+
await discord.thread(thread.id).waitForBotReply({ timeout: 300_000 });
|
|
422
|
+
await captureFixture({
|
|
423
|
+
fixtureName: 'real-session-permission-external-file.jsonl',
|
|
424
|
+
beforeFiles,
|
|
425
|
+
assertEvents: (events) => {
|
|
426
|
+
const hasPermissionAsked = events.some((line) => {
|
|
427
|
+
return line.event.type === 'permission.asked';
|
|
428
|
+
});
|
|
429
|
+
const hasPermissionReplied = events.some((line) => {
|
|
430
|
+
return line.event.type === 'permission.replied';
|
|
431
|
+
});
|
|
432
|
+
expect(hasPermissionAsked).toBe(true);
|
|
433
|
+
expect(hasPermissionReplied).toBe(true);
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}, 900_000);
|
|
437
|
+
realCaptureTest('capture real action buttons fixture', async () => {
|
|
438
|
+
const beforeFiles = getSessionLogState();
|
|
439
|
+
const prompt = 'REAL_FIXTURE_ACTION_BUTTONS. First response MUST call tool `kimaki_action_buttons` with {"buttons":[{"label":"Approve capture","color":"green"}]}. Do not send text before the tool call. After user clicks, reply exactly: action-buttons-done.';
|
|
440
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
441
|
+
content: prompt,
|
|
442
|
+
});
|
|
443
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
444
|
+
timeout: 120_000,
|
|
445
|
+
predicate: (t) => {
|
|
446
|
+
return t.name?.includes('REAL_FIXTURE_ACTION_BUTTONS') ?? false;
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
const action = await waitForPendingActionButtons({
|
|
450
|
+
threadId: thread.id,
|
|
451
|
+
timeoutMs: 300_000,
|
|
452
|
+
});
|
|
453
|
+
await waitForBotMessageContaining({
|
|
454
|
+
discord,
|
|
455
|
+
threadId: thread.id,
|
|
456
|
+
userId: TEST_USER_ID,
|
|
457
|
+
text: 'Action Required',
|
|
458
|
+
timeout: 300_000,
|
|
459
|
+
});
|
|
460
|
+
const interaction = await discord.thread(thread.id).user(TEST_USER_ID).clickButton({
|
|
461
|
+
messageId: action.messageId,
|
|
462
|
+
customId: `action_button:${action.contextHash}:0`,
|
|
463
|
+
});
|
|
464
|
+
await discord.thread(thread.id).waitForInteractionAck({
|
|
465
|
+
interactionId: interaction.id,
|
|
466
|
+
timeout: 30_000,
|
|
467
|
+
});
|
|
468
|
+
await waitForBotMessageContaining({
|
|
469
|
+
discord,
|
|
470
|
+
threadId: thread.id,
|
|
471
|
+
userId: TEST_USER_ID,
|
|
472
|
+
text: 'action-buttons-done',
|
|
473
|
+
timeout: 300_000,
|
|
474
|
+
});
|
|
475
|
+
await captureFixture({
|
|
476
|
+
fixtureName: 'real-session-action-buttons.jsonl',
|
|
477
|
+
beforeFiles,
|
|
478
|
+
assertEvents: (events) => {
|
|
479
|
+
expect(events.length).toBeGreaterThan(0);
|
|
480
|
+
const hasActionTool = hasToolEvent({ events, tool: 'kimaki_action_buttons' });
|
|
481
|
+
expect(hasActionTool).toBe(true);
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}, 900_000);
|
|
485
|
+
realCaptureTest('capture real question tool fixture', async () => {
|
|
486
|
+
const beforeFiles = getSessionLogState();
|
|
487
|
+
const prompt = 'REAL_FIXTURE_QUESTION_TOOL. First response MUST call tool `question` with {"questions":[{"question":"Choose one option","header":"Pick one","options":[{"label":"Alpha","description":"Alpha option"},{"label":"Beta","description":"Beta option"}]}]}. Do not send text before the tool call. After user selects, reply exactly: question-tool-done.';
|
|
488
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
489
|
+
content: prompt,
|
|
490
|
+
});
|
|
491
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
492
|
+
timeout: 120_000,
|
|
493
|
+
predicate: (t) => {
|
|
494
|
+
return t.name?.includes('REAL_FIXTURE_QUESTION_TOOL') ?? false;
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
const pending = await waitForPendingQuestion({
|
|
498
|
+
discord,
|
|
499
|
+
threadId: thread.id,
|
|
500
|
+
timeoutMs: 300_000,
|
|
501
|
+
});
|
|
502
|
+
const interaction = await discord.thread(thread.id).user(TEST_USER_ID).selectMenu({
|
|
503
|
+
messageId: pending.questionMessage.id,
|
|
504
|
+
customId: `ask_question:${pending.contextHash}:0`,
|
|
505
|
+
values: ['0'],
|
|
506
|
+
});
|
|
507
|
+
await discord.thread(thread.id).waitForInteractionAck({
|
|
508
|
+
interactionId: interaction.id,
|
|
509
|
+
timeout: 30_000,
|
|
510
|
+
});
|
|
511
|
+
await waitForBotMessageContaining({
|
|
512
|
+
discord,
|
|
513
|
+
threadId: thread.id,
|
|
514
|
+
userId: TEST_USER_ID,
|
|
515
|
+
text: 'question-tool-done',
|
|
516
|
+
timeout: 300_000,
|
|
517
|
+
});
|
|
518
|
+
await captureFixture({
|
|
519
|
+
fixtureName: 'real-session-question-tool.jsonl',
|
|
520
|
+
beforeFiles,
|
|
521
|
+
assertEvents: (events) => {
|
|
522
|
+
const hasQuestionAsked = events.some((line) => {
|
|
523
|
+
return line.event.type === 'question.asked';
|
|
524
|
+
});
|
|
525
|
+
const hasQuestionReplied = events.some((line) => {
|
|
526
|
+
return line.event.type === 'question.replied';
|
|
527
|
+
});
|
|
528
|
+
expect(hasQuestionAsked).toBe(true);
|
|
529
|
+
expect(hasQuestionReplied).toBe(true);
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}, 900_000);
|
|
533
|
+
});
|