@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,295 @@
|
|
|
1
|
+
// Scheduled task runner for executing due `send --send-at` jobs in the bot process.
|
|
2
|
+
import { Routes } from 'discord.js';
|
|
3
|
+
import { createDiscordRest } from './discord-urls.js';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { claimScheduledTaskRunning, getDuePlannedScheduledTasks, markScheduledTaskCronRescheduled, markScheduledTaskCronRetry, markScheduledTaskFailed, markScheduledTaskOneShotCompleted, recoverStaleRunningScheduledTasks, } from './database.js';
|
|
6
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
7
|
+
import { notifyError } from './sentry.js';
|
|
8
|
+
import { getNextCronRun, getPromptPreview, parseScheduledTaskPayload, } from './task-schedule.js';
|
|
9
|
+
const taskLogger = createLogger(LogPrefix.TASK);
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null;
|
|
12
|
+
}
|
|
13
|
+
function parseMessageId(value) {
|
|
14
|
+
if (!isRecord(value)) {
|
|
15
|
+
return new Error('Discord response is not an object');
|
|
16
|
+
}
|
|
17
|
+
if (typeof value.id !== 'string') {
|
|
18
|
+
return new Error('Discord response is missing message ID');
|
|
19
|
+
}
|
|
20
|
+
return value.id;
|
|
21
|
+
}
|
|
22
|
+
async function executeThreadScheduledTask({ rest, task, payload, }) {
|
|
23
|
+
const marker = {
|
|
24
|
+
start: true,
|
|
25
|
+
scheduledKind: task.schedule_kind,
|
|
26
|
+
scheduledTaskId: task.id,
|
|
27
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
28
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
29
|
+
...(payload.username ? { username: payload.username } : {}),
|
|
30
|
+
...(payload.userId ? { userId: payload.userId } : {}),
|
|
31
|
+
...(payload.permissions?.length ? { permissions: payload.permissions } : {}),
|
|
32
|
+
...(payload.injectionGuardPatterns?.length
|
|
33
|
+
? { injectionGuardPatterns: payload.injectionGuardPatterns }
|
|
34
|
+
: {}),
|
|
35
|
+
};
|
|
36
|
+
const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }];
|
|
37
|
+
// Newline between prefix and prompt so leading /command detection can
|
|
38
|
+
// find the command on its own line.
|
|
39
|
+
const prefixedPrompt = `» **kimaki-cli:**\n${payload.prompt}`;
|
|
40
|
+
const postResult = await rest
|
|
41
|
+
.post(Routes.channelMessages(payload.threadId), {
|
|
42
|
+
body: {
|
|
43
|
+
content: prefixedPrompt,
|
|
44
|
+
embeds: embed,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
return new Error(`Failed to post scheduled thread task ${task.id}`, {
|
|
49
|
+
cause: error,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
if (postResult instanceof Error) {
|
|
53
|
+
return postResult;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function executeChannelScheduledTask({ rest, task, payload, }) {
|
|
57
|
+
const marker = payload.notifyOnly
|
|
58
|
+
? undefined
|
|
59
|
+
: {
|
|
60
|
+
start: true,
|
|
61
|
+
scheduledKind: task.schedule_kind,
|
|
62
|
+
scheduledTaskId: task.id,
|
|
63
|
+
...(payload.worktreeName ? { worktree: payload.worktreeName } : {}),
|
|
64
|
+
...(payload.cwd ? { cwd: payload.cwd } : {}),
|
|
65
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
66
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
67
|
+
...(payload.username ? { username: payload.username } : {}),
|
|
68
|
+
...(payload.userId ? { userId: payload.userId } : {}),
|
|
69
|
+
...(payload.permissions?.length ? { permissions: payload.permissions } : {}),
|
|
70
|
+
...(payload.injectionGuardPatterns?.length
|
|
71
|
+
? { injectionGuardPatterns: payload.injectionGuardPatterns }
|
|
72
|
+
: {}),
|
|
73
|
+
};
|
|
74
|
+
const embeds = marker
|
|
75
|
+
? [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
|
|
76
|
+
: undefined;
|
|
77
|
+
const starterResult = await rest
|
|
78
|
+
.post(Routes.channelMessages(payload.channelId), {
|
|
79
|
+
body: {
|
|
80
|
+
content: payload.prompt,
|
|
81
|
+
embeds,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
return new Error(`Failed to create starter message for task ${task.id}`, {
|
|
86
|
+
cause: error,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
if (starterResult instanceof Error) {
|
|
90
|
+
return starterResult;
|
|
91
|
+
}
|
|
92
|
+
const starterMessageId = parseMessageId(starterResult);
|
|
93
|
+
if (starterMessageId instanceof Error) {
|
|
94
|
+
return new Error(`Invalid starter message response for task ${task.id}`, {
|
|
95
|
+
cause: starterMessageId,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const threadName = (payload.name || getPromptPreview(payload.prompt)).slice(0, 100);
|
|
99
|
+
const threadResult = await rest
|
|
100
|
+
.post(Routes.threads(payload.channelId, starterMessageId), {
|
|
101
|
+
body: {
|
|
102
|
+
name: threadName,
|
|
103
|
+
auto_archive_duration: 1440,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
.catch((error) => {
|
|
107
|
+
return new Error(`Failed to create thread for task ${task.id}`, {
|
|
108
|
+
cause: error,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
if (threadResult instanceof Error) {
|
|
112
|
+
return threadResult;
|
|
113
|
+
}
|
|
114
|
+
if (!payload.userId) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const threadIdResult = parseMessageId(threadResult);
|
|
118
|
+
if (threadIdResult instanceof Error) {
|
|
119
|
+
return new Error(`Invalid thread response for task ${task.id}`, {
|
|
120
|
+
cause: threadIdResult,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const addMemberResult = await rest
|
|
124
|
+
.put(Routes.threadMembers(threadIdResult, payload.userId))
|
|
125
|
+
.catch((error) => {
|
|
126
|
+
return new Error(`Failed to add user to scheduled thread for task ${task.id}`, { cause: error });
|
|
127
|
+
});
|
|
128
|
+
if (addMemberResult instanceof Error) {
|
|
129
|
+
return addMemberResult;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function executeScheduledTask({ rest, task, }) {
|
|
133
|
+
const payloadResult = parseScheduledTaskPayload(task.payload_json);
|
|
134
|
+
if (payloadResult instanceof Error) {
|
|
135
|
+
return new Error(`Task ${task.id} has invalid payload`, {
|
|
136
|
+
cause: payloadResult,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (payloadResult.kind === 'thread') {
|
|
140
|
+
return executeThreadScheduledTask({
|
|
141
|
+
rest,
|
|
142
|
+
task,
|
|
143
|
+
payload: payloadResult,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return executeChannelScheduledTask({
|
|
147
|
+
rest,
|
|
148
|
+
task,
|
|
149
|
+
payload: payloadResult,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function finalizeSuccessfulTask({ task, completedAt, }) {
|
|
153
|
+
if (task.schedule_kind === 'at') {
|
|
154
|
+
await markScheduledTaskOneShotCompleted({ taskId: task.id, completedAt });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!task.cron_expr) {
|
|
158
|
+
await markScheduledTaskFailed({
|
|
159
|
+
taskId: task.id,
|
|
160
|
+
failedAt: completedAt,
|
|
161
|
+
errorMessage: 'Missing cron expression on cron task',
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Use stored timezone, falling back to UTC (not machine local) for consistency
|
|
166
|
+
const timezone = task.timezone || 'UTC';
|
|
167
|
+
const nextRunResult = getNextCronRun({
|
|
168
|
+
cronExpr: task.cron_expr,
|
|
169
|
+
timezone,
|
|
170
|
+
from: completedAt,
|
|
171
|
+
});
|
|
172
|
+
if (nextRunResult instanceof Error) {
|
|
173
|
+
await markScheduledTaskFailed({
|
|
174
|
+
taskId: task.id,
|
|
175
|
+
failedAt: completedAt,
|
|
176
|
+
errorMessage: nextRunResult.message,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await markScheduledTaskCronRescheduled({
|
|
181
|
+
taskId: task.id,
|
|
182
|
+
completedAt,
|
|
183
|
+
nextRunAt: nextRunResult,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function finalizeFailedTask({ task, failedAt, error, }) {
|
|
187
|
+
if (task.schedule_kind === 'cron' && task.cron_expr) {
|
|
188
|
+
// Use stored timezone, falling back to UTC (not machine local) for consistency
|
|
189
|
+
const timezone = task.timezone || 'UTC';
|
|
190
|
+
const nextRunResult = getNextCronRun({
|
|
191
|
+
cronExpr: task.cron_expr,
|
|
192
|
+
timezone,
|
|
193
|
+
from: failedAt,
|
|
194
|
+
});
|
|
195
|
+
if (!(nextRunResult instanceof Error)) {
|
|
196
|
+
await markScheduledTaskCronRetry({
|
|
197
|
+
taskId: task.id,
|
|
198
|
+
failedAt,
|
|
199
|
+
errorMessage: error.message,
|
|
200
|
+
nextRunAt: nextRunResult,
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await markScheduledTaskFailed({
|
|
206
|
+
taskId: task.id,
|
|
207
|
+
failedAt,
|
|
208
|
+
errorMessage: error.message,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function processDueTask({ rest, task, }) {
|
|
212
|
+
const startedAt = new Date();
|
|
213
|
+
const claimed = await claimScheduledTaskRunning({
|
|
214
|
+
taskId: task.id,
|
|
215
|
+
startedAt,
|
|
216
|
+
});
|
|
217
|
+
if (!claimed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const executeResult = await executeScheduledTask({ rest, task });
|
|
221
|
+
const finishedAt = new Date();
|
|
222
|
+
if (executeResult instanceof Error) {
|
|
223
|
+
taskLogger.warn(`[task-runner] task ${task.id} failed: ${formatErrorWithStack(executeResult)}`);
|
|
224
|
+
await finalizeFailedTask({
|
|
225
|
+
task,
|
|
226
|
+
failedAt: finishedAt,
|
|
227
|
+
error: executeResult,
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await finalizeSuccessfulTask({ task, completedAt: finishedAt });
|
|
232
|
+
}
|
|
233
|
+
async function runTaskRunnerTick({ rest, staleRunningMs, dueBatchSize, }) {
|
|
234
|
+
const staleBefore = new Date(Date.now() - staleRunningMs);
|
|
235
|
+
const recoveredCount = await recoverStaleRunningScheduledTasks({
|
|
236
|
+
staleBefore,
|
|
237
|
+
});
|
|
238
|
+
if (recoveredCount > 0) {
|
|
239
|
+
taskLogger.warn(`[task-runner] Recovered ${recoveredCount} stale running task(s)`);
|
|
240
|
+
}
|
|
241
|
+
const dueTasks = await getDuePlannedScheduledTasks({
|
|
242
|
+
now: new Date(),
|
|
243
|
+
limit: dueBatchSize,
|
|
244
|
+
});
|
|
245
|
+
await dueTasks.reduce(async (previous, task) => {
|
|
246
|
+
await previous;
|
|
247
|
+
await processDueTask({ rest, task });
|
|
248
|
+
}, Promise.resolve());
|
|
249
|
+
}
|
|
250
|
+
export function startTaskRunner({ token, pollIntervalMs = 5_000, staleRunningMs = 120_000, dueBatchSize = 20, }) {
|
|
251
|
+
const rest = createDiscordRest(token);
|
|
252
|
+
let stopped = false;
|
|
253
|
+
let ticking = false;
|
|
254
|
+
let tickPromise = null;
|
|
255
|
+
const tick = async () => {
|
|
256
|
+
if (stopped || ticking) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
ticking = true;
|
|
260
|
+
const currentTickPromise = runTaskRunnerTick({
|
|
261
|
+
rest,
|
|
262
|
+
staleRunningMs,
|
|
263
|
+
dueBatchSize,
|
|
264
|
+
}).catch((error) => {
|
|
265
|
+
return new Error('Task runner tick failed', { cause: error });
|
|
266
|
+
});
|
|
267
|
+
tickPromise = currentTickPromise.then(() => {
|
|
268
|
+
return;
|
|
269
|
+
});
|
|
270
|
+
const runResult = await currentTickPromise;
|
|
271
|
+
if (runResult instanceof Error) {
|
|
272
|
+
taskLogger.error(`[task-runner] ${formatErrorWithStack(runResult)}`);
|
|
273
|
+
void notifyError(runResult, 'Task runner tick failed');
|
|
274
|
+
}
|
|
275
|
+
ticking = false;
|
|
276
|
+
tickPromise = null;
|
|
277
|
+
};
|
|
278
|
+
const timer = setInterval(() => {
|
|
279
|
+
void tick();
|
|
280
|
+
}, pollIntervalMs);
|
|
281
|
+
void tick();
|
|
282
|
+
taskLogger.log(`[task-runner] started (interval=${pollIntervalMs}ms)`);
|
|
283
|
+
return async () => {
|
|
284
|
+
if (stopped) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
stopped = true;
|
|
288
|
+
clearInterval(timer);
|
|
289
|
+
if (tickPromise) {
|
|
290
|
+
await tickPromise;
|
|
291
|
+
tickPromise = null;
|
|
292
|
+
}
|
|
293
|
+
taskLogger.log('[task-runner] stopped');
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Scheduled task parsing utilities for `send --send-at` and task runner execution.
|
|
2
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
3
|
+
import * as errore from 'errore';
|
|
4
|
+
const UTC_SEND_AT_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?Z$/;
|
|
5
|
+
export function getLocalTimeZone() {
|
|
6
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
7
|
+
if (!tz) {
|
|
8
|
+
return 'UTC';
|
|
9
|
+
}
|
|
10
|
+
return tz;
|
|
11
|
+
}
|
|
12
|
+
export function getPromptPreview(prompt) {
|
|
13
|
+
const normalized = prompt.replace(/\s+/g, ' ').trim();
|
|
14
|
+
if (normalized.length <= 120) {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
return `${normalized.slice(0, 117)}...`;
|
|
18
|
+
}
|
|
19
|
+
function parseUtcSendAtDate({ value, now, }) {
|
|
20
|
+
const looksLikeDate = value.includes('T') || /^\d{4}-\d{2}-\d{2}/.test(value);
|
|
21
|
+
if (!looksLikeDate) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (!UTC_SEND_AT_DATE_REGEX.test(value)) {
|
|
25
|
+
return new Error(`--send-at date must be UTC ISO format ending with Z (example: 2026-03-01T09:00:00Z). Received: ${value}`);
|
|
26
|
+
}
|
|
27
|
+
const runAt = new Date(value);
|
|
28
|
+
if (Number.isNaN(runAt.getTime())) {
|
|
29
|
+
return new Error(`Invalid UTC date for --send-at: ${value}`);
|
|
30
|
+
}
|
|
31
|
+
if (runAt.getTime() <= now.getTime()) {
|
|
32
|
+
return new Error(`--send-at date must be in the future (UTC): ${value}`);
|
|
33
|
+
}
|
|
34
|
+
return runAt;
|
|
35
|
+
}
|
|
36
|
+
export function parseSendAtValue({ value, now, timezone, }) {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (!trimmed) {
|
|
39
|
+
return new Error('--send-at cannot be empty');
|
|
40
|
+
}
|
|
41
|
+
const utcDateResult = parseUtcSendAtDate({ value: trimmed, now });
|
|
42
|
+
if (utcDateResult instanceof Error) {
|
|
43
|
+
return utcDateResult;
|
|
44
|
+
}
|
|
45
|
+
if (utcDateResult) {
|
|
46
|
+
return {
|
|
47
|
+
scheduleKind: 'at',
|
|
48
|
+
runAt: utcDateResult,
|
|
49
|
+
cronExpr: null,
|
|
50
|
+
timezone: null,
|
|
51
|
+
nextRunAt: utcDateResult,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const looksLikeCron = trimmed.startsWith('@') || trimmed.split(/\s+/).length >= 5;
|
|
55
|
+
if (looksLikeCron) {
|
|
56
|
+
const nextRunAtResult = getNextCronRun({
|
|
57
|
+
cronExpr: trimmed,
|
|
58
|
+
timezone,
|
|
59
|
+
from: now,
|
|
60
|
+
});
|
|
61
|
+
if (!(nextRunAtResult instanceof Error)) {
|
|
62
|
+
return {
|
|
63
|
+
scheduleKind: 'cron',
|
|
64
|
+
runAt: null,
|
|
65
|
+
cronExpr: trimmed,
|
|
66
|
+
timezone,
|
|
67
|
+
nextRunAt: nextRunAtResult,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const cronResult = getNextCronRun({ cronExpr: trimmed, timezone, from: now });
|
|
72
|
+
if (cronResult instanceof Error) {
|
|
73
|
+
return new Error(`Invalid --send-at value: "${trimmed}". Use UTC ISO date/time ending in Z or a cron expression.`, {
|
|
74
|
+
cause: cronResult,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
scheduleKind: 'cron',
|
|
79
|
+
runAt: null,
|
|
80
|
+
cronExpr: trimmed,
|
|
81
|
+
timezone,
|
|
82
|
+
nextRunAt: cronResult,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function getNextCronRun({ cronExpr, timezone, from, }) {
|
|
86
|
+
const parsed = errore.try({
|
|
87
|
+
try: () => {
|
|
88
|
+
return CronExpressionParser.parse(cronExpr, {
|
|
89
|
+
currentDate: from,
|
|
90
|
+
tz: timezone,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
catch: (error) => {
|
|
94
|
+
return new Error(`Invalid cron expression: ${cronExpr}`, { cause: error });
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
if (parsed instanceof Error) {
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
const next = errore.try({
|
|
101
|
+
try: () => {
|
|
102
|
+
return parsed.next().toDate();
|
|
103
|
+
},
|
|
104
|
+
catch: (error) => {
|
|
105
|
+
return new Error(`Could not compute next run for cron: ${cronExpr}`, {
|
|
106
|
+
cause: error,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (next instanceof Error) {
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
export function serializeScheduledTaskPayload(payload) {
|
|
116
|
+
return JSON.stringify(payload);
|
|
117
|
+
}
|
|
118
|
+
function isRecord(value) {
|
|
119
|
+
return typeof value === 'object' && value !== null;
|
|
120
|
+
}
|
|
121
|
+
function asString(value) {
|
|
122
|
+
if (typeof value !== 'string') {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function asStringArray(value) {
|
|
128
|
+
if (!Array.isArray(value)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return value.filter((v) => {
|
|
132
|
+
return typeof v === 'string';
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function parseScheduledTaskPayload(payloadJson) {
|
|
136
|
+
const parsed = errore.try({
|
|
137
|
+
try: () => {
|
|
138
|
+
return JSON.parse(payloadJson);
|
|
139
|
+
},
|
|
140
|
+
catch: (error) => {
|
|
141
|
+
return new Error('Task payload is not valid JSON', { cause: error });
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
if (parsed instanceof Error) {
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(parsed)) {
|
|
148
|
+
return new Error('Task payload must be an object');
|
|
149
|
+
}
|
|
150
|
+
const kind = asString(parsed.kind);
|
|
151
|
+
if (kind === 'thread') {
|
|
152
|
+
const threadId = asString(parsed.threadId);
|
|
153
|
+
const prompt = asString(parsed.prompt);
|
|
154
|
+
const agent = asString(parsed.agent);
|
|
155
|
+
const model = asString(parsed.model);
|
|
156
|
+
const username = asString(parsed.username);
|
|
157
|
+
const userId = asString(parsed.userId);
|
|
158
|
+
const permissions = asStringArray(parsed.permissions);
|
|
159
|
+
const injectionGuardPatterns = asStringArray(parsed.injectionGuardPatterns);
|
|
160
|
+
if (!threadId || !prompt) {
|
|
161
|
+
return new Error('Thread task payload requires threadId and prompt');
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
kind: 'thread',
|
|
165
|
+
threadId,
|
|
166
|
+
prompt,
|
|
167
|
+
agent,
|
|
168
|
+
model,
|
|
169
|
+
username,
|
|
170
|
+
userId,
|
|
171
|
+
permissions,
|
|
172
|
+
injectionGuardPatterns,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (kind === 'channel') {
|
|
176
|
+
const channelId = asString(parsed.channelId);
|
|
177
|
+
const prompt = asString(parsed.prompt);
|
|
178
|
+
const nameValue = parsed.name;
|
|
179
|
+
const name = typeof nameValue === 'string' ? nameValue : null;
|
|
180
|
+
const notifyOnly = parsed.notifyOnly === true;
|
|
181
|
+
const worktreeName = asString(parsed.worktreeName);
|
|
182
|
+
const cwd = asString(parsed.cwd);
|
|
183
|
+
const agent = asString(parsed.agent);
|
|
184
|
+
const model = asString(parsed.model);
|
|
185
|
+
const username = asString(parsed.username);
|
|
186
|
+
const userId = asString(parsed.userId);
|
|
187
|
+
const permissions = asStringArray(parsed.permissions);
|
|
188
|
+
const injectionGuardPatterns = asStringArray(parsed.injectionGuardPatterns);
|
|
189
|
+
if (!channelId || !prompt) {
|
|
190
|
+
return new Error('Channel task payload requires channelId and prompt');
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
kind: 'channel',
|
|
194
|
+
channelId,
|
|
195
|
+
prompt,
|
|
196
|
+
name,
|
|
197
|
+
notifyOnly,
|
|
198
|
+
worktreeName,
|
|
199
|
+
cwd,
|
|
200
|
+
agent,
|
|
201
|
+
model,
|
|
202
|
+
username,
|
|
203
|
+
userId,
|
|
204
|
+
permissions,
|
|
205
|
+
injectionGuardPatterns,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return new Error('Task payload has unknown kind');
|
|
209
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Tests for scheduled task date/cron parsing and UTC validation rules.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { parseSendAtValue } from './task-schedule.js';
|
|
4
|
+
describe('parseSendAtValue', () => {
|
|
5
|
+
test('accepts UTC ISO date ending with Z', () => {
|
|
6
|
+
const now = new Date('2026-02-22T13:00:00Z');
|
|
7
|
+
const result = parseSendAtValue({
|
|
8
|
+
value: '2026-03-01T09:00:00Z',
|
|
9
|
+
now,
|
|
10
|
+
timezone: 'UTC',
|
|
11
|
+
});
|
|
12
|
+
expect(result).not.toBeInstanceOf(Error);
|
|
13
|
+
if (result instanceof Error) {
|
|
14
|
+
throw result;
|
|
15
|
+
}
|
|
16
|
+
expect(result.scheduleKind).toBe('at');
|
|
17
|
+
expect(result.runAt?.toISOString()).toBe('2026-03-01T09:00:00.000Z');
|
|
18
|
+
expect(result.nextRunAt.toISOString()).toBe('2026-03-01T09:00:00.000Z');
|
|
19
|
+
});
|
|
20
|
+
test('rejects ISO date with non-UTC offset', () => {
|
|
21
|
+
const now = new Date('2026-02-22T13:00:00Z');
|
|
22
|
+
const result = parseSendAtValue({
|
|
23
|
+
value: '2026-03-01T09:00:00+01:00',
|
|
24
|
+
now,
|
|
25
|
+
timezone: 'UTC',
|
|
26
|
+
});
|
|
27
|
+
expect(result).toBeInstanceOf(Error);
|
|
28
|
+
if (result instanceof Error) {
|
|
29
|
+
expect(result.message).toContain('must be UTC ISO format ending with Z');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
test('rejects local ISO date without timezone suffix', () => {
|
|
33
|
+
const now = new Date('2026-02-22T13:00:00Z');
|
|
34
|
+
const result = parseSendAtValue({
|
|
35
|
+
value: '2026-03-01T09:00:00',
|
|
36
|
+
now,
|
|
37
|
+
timezone: 'UTC',
|
|
38
|
+
});
|
|
39
|
+
expect(result).toBeInstanceOf(Error);
|
|
40
|
+
if (result instanceof Error) {
|
|
41
|
+
expect(result.message).toContain('must be UTC ISO format ending with Z');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
test('rejects UTC dates in the past', () => {
|
|
45
|
+
const now = new Date('2026-02-22T13:00:00Z');
|
|
46
|
+
const result = parseSendAtValue({
|
|
47
|
+
value: '2026-02-22T12:59:59Z',
|
|
48
|
+
now,
|
|
49
|
+
timezone: 'UTC',
|
|
50
|
+
});
|
|
51
|
+
expect(result).toBeInstanceOf(Error);
|
|
52
|
+
if (result instanceof Error) {
|
|
53
|
+
expect(result.message).toContain('must be in the future (UTC)');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
test('accepts cron expressions', () => {
|
|
57
|
+
const now = new Date('2026-02-22T13:00:00Z');
|
|
58
|
+
const result = parseSendAtValue({
|
|
59
|
+
value: '0 9 * * 1',
|
|
60
|
+
now,
|
|
61
|
+
timezone: 'UTC',
|
|
62
|
+
});
|
|
63
|
+
expect(result).not.toBeInstanceOf(Error);
|
|
64
|
+
if (result instanceof Error) {
|
|
65
|
+
throw result;
|
|
66
|
+
}
|
|
67
|
+
expect(result.scheduleKind).toBe('cron');
|
|
68
|
+
expect(result.cronExpr).toBe('0 9 * * 1');
|
|
69
|
+
expect(result.nextRunAt.toISOString()).toBe('2026-02-23T09:00:00.000Z');
|
|
70
|
+
});
|
|
71
|
+
});
|