@vellumai/assistant 0.8.1 → 0.8.2
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/ARCHITECTURE.md +2 -7
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +5 -0
- package/docker-init-apt-root.sh +94 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +325 -3
- package/package.json +3 -1
- package/scripts/generate-openapi.ts +83 -10
- package/scripts/sync-llm-catalog.ts +2 -2
- package/scripts/sync-web-search-catalog.ts +47 -25
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +45 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +21 -0
- package/src/__tests__/config-set-route.test.ts +80 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/context-search-conversations-source.test.ts +117 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +1 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-error.test.ts +42 -3
- package/src/__tests__/conversation-fork-crud.test.ts +82 -0
- package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
- package/src/__tests__/conversation-lifecycle.test.ts +173 -0
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +4 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +3 -2
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +60 -36
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +73 -0
- package/src/__tests__/host-app-control-proxy.test.ts +266 -10
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
- package/src/__tests__/inference-profile-reaper.test.ts +4 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
- package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
- package/src/__tests__/injector-chain.test.ts +10 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-catalog-parity.test.ts +55 -13
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +31 -29
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/oauth-commands-routes.test.ts +168 -16
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/openai-provider.test.ts +24 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +10 -36
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +0 -1
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
- package/src/__tests__/plugin-types.test.ts +3 -13
- package/src/__tests__/process-message-background-slack.test.ts +8 -1
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-routes.test.ts +50 -3
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
- package/src/__tests__/skill-load-tool.test.ts +27 -89
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/system-prompt.test.ts +737 -63
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/user-plugin-loader.test.ts +0 -7
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/types.ts +108 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/commands/__tests__/schedules.test.ts +491 -0
- package/src/cli/commands/changelog.ts +106 -42
- package/src/cli/commands/conversations.ts +102 -17
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/notifications.ts +329 -317
- package/src/cli/commands/plugins.ts +185 -0
- package/src/cli/commands/schedules.ts +391 -0
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/install-from-github.ts +304 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +38 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -0
- package/src/config/feature-flag-registry.json +41 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +7 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/channels.ts +8 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1047 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/context/window-manager.ts +197 -1520
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
- package/src/daemon/approval-generators.ts +8 -6
- package/src/daemon/config-watcher.ts +94 -31
- package/src/daemon/conversation-agent-loop.ts +169 -9
- package/src/daemon/conversation-error.ts +171 -37
- package/src/daemon/conversation-lifecycle.ts +53 -40
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +49 -12
- package/src/daemon/conversation-runtime-assembly.ts +16 -1
- package/src/daemon/conversation-slash.ts +12 -5
- package/src/daemon/conversation-store.ts +11 -4
- package/src/daemon/conversation-tool-setup.ts +39 -7
- package/src/daemon/conversation.ts +33 -1
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +111 -108
- package/src/daemon/history-repair.ts +28 -1
- package/src/daemon/host-app-control-proxy.ts +98 -23
- package/src/daemon/lifecycle.ts +45 -35
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +49 -0
- package/src/daemon/message-protocol.ts +1 -0
- package/src/daemon/message-types/conversations.ts +25 -0
- package/src/daemon/message-types/messages.ts +61 -0
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pkb-reminder-builder.test.ts +1 -1
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +21 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +29 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +41 -0
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +22 -0
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
- package/src/memory/__tests__/conversation-queries.test.ts +263 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +42 -10
- package/src/memory/context-search/sources/conversations.ts +62 -2
- package/src/memory/context-search/sources/workspace.ts +4 -0
- package/src/memory/conversation-crud.ts +63 -19
- package/src/memory/conversation-queries.ts +110 -10
- package/src/memory/db-init.ts +6 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +4 -4
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
- package/src/memory/graph/conversation-graph-memory.ts +31 -15
- package/src/memory/graph/tools.ts +3 -3
- package/src/memory/indexer.ts +34 -29
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
- package/src/memory/jobs/embed-concept-page.ts +20 -11
- package/src/memory/jobs-worker.ts +6 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
- package/src/memory/message-content.ts +1 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/inference.ts +1 -3
- package/src/memory/schema/infrastructure.ts +12 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/injection.test.ts +98 -8
- package/src/memory/v2/__tests__/migration.test.ts +87 -0
- package/src/memory/v2/__tests__/page-index.test.ts +83 -0
- package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
- package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
- package/src/memory/v2/__tests__/router.test.ts +15 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
- package/src/memory/v2/injection.ts +32 -6
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +35 -5
- package/src/memory/v2/prompts/router.ts +11 -8
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +135 -7
- package/src/memory/v2/router.ts +9 -8
- package/src/memory/v2/skill-store.ts +120 -35
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/decision-engine.ts +2 -1
- package/src/notifications/emit-signal.ts +20 -1
- package/src/notifications/home-feed-side-effect.ts +54 -0
- package/src/notifications/signal.ts +3 -1
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.ts +6 -2
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +34 -1
- package/src/plugin-api/types.ts +104 -22
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +36 -3
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +105 -32
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +31 -26
- package/src/plugins/user-loader.ts +21 -2
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +37 -5
- package/src/prompts/__tests__/system-prompt.test.ts +12 -0
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -166
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +173 -0
- package/src/providers/__tests__/inference.test.ts +22 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/connection-resolution.ts +7 -0
- package/src/providers/inference/adapter-factory.ts +41 -4
- package/src/providers/inference/connections.ts +74 -29
- package/src/providers/inference/resolve-auth.ts +12 -4
- package/src/providers/model-catalog.ts +294 -12
- package/src/providers/openai/chat-completions-provider.ts +10 -2
- package/src/providers/openrouter/client.ts +7 -0
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +36 -0
- package/src/providers/registry.ts +22 -14
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +42 -14
- package/src/runtime/auth/route-policy.ts +8 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-types.ts +19 -0
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +5 -3
- package/src/runtime/routes/auth-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +5 -3
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +121 -0
- package/src/runtime/routes/conversation-cli-routes.ts +44 -3
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +17 -42
- package/src/runtime/routes/conversation-query-routes.ts +40 -35
- package/src/runtime/routes/conversation-routes.ts +90 -11
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
- package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
- package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +6 -13
- package/src/runtime/routes/notification-routes.ts +1 -1
- package/src/runtime/routes/oauth-commands-routes.ts +105 -15
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +4 -7
- package/src/runtime/routes/subagents-routes.ts +57 -18
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +27 -2
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +93 -1
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +9 -1
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +15 -7
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/browser-execution.ts +15 -11
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +72 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/watcher/__tests__/engine.test.ts +22 -0
- package/src/watcher/engine.ts +6 -2
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +39 -9
- package/src/workspace/migrations/types.ts +4 -0
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-window-manager.test.ts +0 -2481
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
|
@@ -24,13 +24,18 @@ mock.module("../../../../security/secure-keys.js", () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
// OAuth helpers are exercised only when no bot_token is cached. The adapter
|
|
27
|
-
// imports them at module load
|
|
28
|
-
// OAuth fallback with a distinctive error so tests can assert on it.
|
|
27
|
+
// imports them at module load, so route them through a configurable stub.
|
|
29
28
|
const OAUTH_FALLBACK_SENTINEL = "OAUTH_FALLBACK_NOT_STUBBED";
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const resolveOAuthConnectionMock = mock(
|
|
30
|
+
async (
|
|
31
|
+
_provider: string,
|
|
32
|
+
_opts?: { account?: string },
|
|
33
|
+
): Promise<OAuthConnection> => {
|
|
32
34
|
throw new Error(OAUTH_FALLBACK_SENTINEL);
|
|
33
35
|
},
|
|
36
|
+
);
|
|
37
|
+
mock.module("../../../../oauth/connection-resolver.js", () => ({
|
|
38
|
+
resolveOAuthConnection: resolveOAuthConnectionMock,
|
|
34
39
|
}));
|
|
35
40
|
mock.module("../../../../oauth/oauth-store.js", () => ({
|
|
36
41
|
isProviderConnected: async () => false,
|
|
@@ -44,7 +49,7 @@ mock.module("../../../../contacts/contacts-write.js", () => ({
|
|
|
44
49
|
upsertContactChannel: () => {},
|
|
45
50
|
}));
|
|
46
51
|
|
|
47
|
-
import { slackProvider } from "../adapter.js";
|
|
52
|
+
import { slackProvider, withSlackBotToken } from "../adapter.js";
|
|
48
53
|
|
|
49
54
|
// ── fetch capture ───────────────────────────────────────────────────────────
|
|
50
55
|
|
|
@@ -108,9 +113,23 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
|
|
|
108
113
|
const BOT_TOKEN = "xoxb-BOT";
|
|
109
114
|
const USER_TOKEN = "xoxp-USER";
|
|
110
115
|
|
|
116
|
+
function makeOAuthConnection(account: string, token: string): OAuthConnection {
|
|
117
|
+
return {
|
|
118
|
+
id: `conn-${account}`,
|
|
119
|
+
provider: "slack",
|
|
120
|
+
accountInfo: account,
|
|
121
|
+
request: async () => ({ status: 200, headers: {}, body: { ok: true } }),
|
|
122
|
+
withToken: async <T>(fn: (rawToken: string) => Promise<T>) => fn(token),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
describe("Slack adapter token routing", () => {
|
|
112
127
|
beforeEach(() => {
|
|
113
128
|
captured.length = 0;
|
|
129
|
+
resolveOAuthConnectionMock.mockReset();
|
|
130
|
+
resolveOAuthConnectionMock.mockImplementation(async () => {
|
|
131
|
+
throw new Error(OAUTH_FALLBACK_SENTINEL);
|
|
132
|
+
});
|
|
114
133
|
getSecureKeyAsyncMock.mockReset();
|
|
115
134
|
getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
|
|
116
135
|
if (key === credentialKey("slack_channel", "bot_token")) return BOT_TOKEN;
|
|
@@ -279,4 +298,25 @@ describe("Slack adapter token routing", () => {
|
|
|
279
298
|
OAUTH_FALLBACK_SENTINEL,
|
|
280
299
|
);
|
|
281
300
|
});
|
|
301
|
+
|
|
302
|
+
test("raw bot token helper resolves the requested OAuth account even when cache is warm", async () => {
|
|
303
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
304
|
+
resolveOAuthConnectionMock.mockImplementation(
|
|
305
|
+
async (_provider: string, opts?: { account?: string }) => {
|
|
306
|
+
const account = opts?.account ?? "default";
|
|
307
|
+
return makeOAuthConnection(account, `token-${account}`);
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
await slackProvider.resolveConnection!("workspace-a");
|
|
312
|
+
|
|
313
|
+
const result = await withSlackBotToken("workspace-b", async (token) => {
|
|
314
|
+
return token;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(result).toBe("token-workspace-b");
|
|
318
|
+
expect(resolveOAuthConnectionMock).toHaveBeenCalledWith("slack", {
|
|
319
|
+
account: "workspace-b",
|
|
320
|
+
});
|
|
321
|
+
});
|
|
282
322
|
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the assistant-side Slack file downloader used by the
|
|
3
|
+
* thread-backfill image-hydration path.
|
|
4
|
+
*
|
|
5
|
+
* The downloader has three contract-level behaviors worth pinning:
|
|
6
|
+
* 1. URL selection — `url_private_download` is preferred over `url_private`.
|
|
7
|
+
* 2. Bearer auth — the bot token MUST be sent on the initial request.
|
|
8
|
+
* 3. Manual cross-origin redirect handling — the CDN URL is signed and the
|
|
9
|
+
* Authorization header MUST NOT be re-sent on the second hop (Slack
|
|
10
|
+
* rejects the signed URL when an unexpected Authorization is present).
|
|
11
|
+
* 4. Returns null when no usable URL is present.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
17
|
+
getLogger: () =>
|
|
18
|
+
new Proxy({} as Record<string, unknown>, {
|
|
19
|
+
get: () => () => {},
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { downloadSlackFile } from "../download.js";
|
|
24
|
+
|
|
25
|
+
interface CapturedFetchCall {
|
|
26
|
+
url: string;
|
|
27
|
+
init: RequestInit | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let calls: CapturedFetchCall[];
|
|
31
|
+
let responses: Response[];
|
|
32
|
+
let originalFetch: typeof fetch;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
calls = [];
|
|
36
|
+
responses = [];
|
|
37
|
+
originalFetch = globalThis.fetch;
|
|
38
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
39
|
+
calls.push({
|
|
40
|
+
url: typeof input === "string" ? input : input.toString(),
|
|
41
|
+
init,
|
|
42
|
+
});
|
|
43
|
+
const next = responses.shift();
|
|
44
|
+
if (!next) {
|
|
45
|
+
throw new Error("downloadSlackFile test: no canned response available");
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
}) as typeof fetch;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
globalThis.fetch = originalFetch;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("downloadSlackFile", () => {
|
|
56
|
+
test("returns null when neither url_private_download nor url_private is present", async () => {
|
|
57
|
+
const result = await downloadSlackFile(
|
|
58
|
+
{ name: "screenshot.png", mimetype: "image/png" },
|
|
59
|
+
"xoxb-test",
|
|
60
|
+
);
|
|
61
|
+
expect(result).toBeNull();
|
|
62
|
+
expect(calls.length).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("prefers url_private_download over url_private", async () => {
|
|
66
|
+
responses.push(
|
|
67
|
+
new Response(new Uint8Array([1, 2, 3]).buffer, {
|
|
68
|
+
status: 200,
|
|
69
|
+
headers: { "Content-Type": "image/png" },
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
await downloadSlackFile(
|
|
73
|
+
{
|
|
74
|
+
id: "F1",
|
|
75
|
+
name: "shot.png",
|
|
76
|
+
mimetype: "image/png",
|
|
77
|
+
urlPrivateDownload: "https://files.slack.com/files-pri/T/F1/download",
|
|
78
|
+
urlPrivate: "https://files.slack.com/files-pri/T/F1/inline",
|
|
79
|
+
},
|
|
80
|
+
"xoxb-test",
|
|
81
|
+
);
|
|
82
|
+
expect(calls.length).toBe(1);
|
|
83
|
+
expect(calls[0].url).toBe(
|
|
84
|
+
"https://files.slack.com/files-pri/T/F1/download",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("sends bot token as Bearer on the initial request and base64-encodes the body", async () => {
|
|
89
|
+
responses.push(
|
|
90
|
+
new Response(new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer, {
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: { "Content-Type": "image/png" },
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
const result = await downloadSlackFile(
|
|
96
|
+
{
|
|
97
|
+
id: "F1",
|
|
98
|
+
name: "shot.png",
|
|
99
|
+
mimetype: "image/png",
|
|
100
|
+
urlPrivate: "https://files.slack.com/files-pri/T/F1/inline",
|
|
101
|
+
},
|
|
102
|
+
"xoxb-test-token",
|
|
103
|
+
);
|
|
104
|
+
expect(calls.length).toBe(1);
|
|
105
|
+
const auth = (calls[0].init?.headers as Record<string, string>)
|
|
106
|
+
?.Authorization;
|
|
107
|
+
expect(auth).toBe("Bearer xoxb-test-token");
|
|
108
|
+
expect(calls[0].init?.redirect).toBe("manual");
|
|
109
|
+
expect(result).not.toBeNull();
|
|
110
|
+
expect(result?.filename).toBe("shot.png");
|
|
111
|
+
expect(result?.mimeType).toBe("image/png");
|
|
112
|
+
// 0xdeadbeef → "3q2+7w==" in base64.
|
|
113
|
+
expect(result?.data).toBe("3q2+7w==");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("follows a 302 to the signed CDN URL without re-sending the bearer token", async () => {
|
|
117
|
+
responses.push(
|
|
118
|
+
new Response(null, {
|
|
119
|
+
status: 302,
|
|
120
|
+
headers: {
|
|
121
|
+
Location:
|
|
122
|
+
"https://files-edge.slack.com/files-tmb/T-F1-abc/cdn-signed?t=1700000000",
|
|
123
|
+
},
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
responses.push(
|
|
127
|
+
new Response(new Uint8Array([1, 2]).buffer, {
|
|
128
|
+
status: 200,
|
|
129
|
+
headers: { "Content-Type": "image/jpeg" },
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
const result = await downloadSlackFile(
|
|
133
|
+
{
|
|
134
|
+
id: "F1",
|
|
135
|
+
name: "photo.jpg",
|
|
136
|
+
mimetype: "image/jpeg",
|
|
137
|
+
urlPrivateDownload: "https://files.slack.com/files-pri/T/F1/download",
|
|
138
|
+
},
|
|
139
|
+
"xoxb-test",
|
|
140
|
+
);
|
|
141
|
+
expect(calls.length).toBe(2);
|
|
142
|
+
expect(calls[0].init?.redirect).toBe("manual");
|
|
143
|
+
const secondAuth = (calls[1].init?.headers as Record<string, string>)
|
|
144
|
+
?.Authorization;
|
|
145
|
+
expect(secondAuth).toBeUndefined();
|
|
146
|
+
expect(calls[1].url).toBe(
|
|
147
|
+
"https://files-edge.slack.com/files-tmb/T-F1-abc/cdn-signed?t=1700000000",
|
|
148
|
+
);
|
|
149
|
+
expect(result?.data).toBe(Buffer.from([1, 2]).toString("base64"));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("resolves a relative Location header against the original URL", async () => {
|
|
153
|
+
responses.push(
|
|
154
|
+
new Response(null, {
|
|
155
|
+
status: 302,
|
|
156
|
+
headers: { Location: "/files-tmb/cdn-signed?t=1700" },
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
responses.push(
|
|
160
|
+
new Response(new Uint8Array([9]).buffer, {
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: { "Content-Type": "image/png" },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
await downloadSlackFile(
|
|
166
|
+
{
|
|
167
|
+
id: "F1",
|
|
168
|
+
name: "x.png",
|
|
169
|
+
urlPrivateDownload: "https://files.slack.com/a/b/download",
|
|
170
|
+
},
|
|
171
|
+
"xoxb-test",
|
|
172
|
+
);
|
|
173
|
+
expect(calls[1].url).toBe(
|
|
174
|
+
"https://files.slack.com/files-tmb/cdn-signed?t=1700",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("throws when the second hop responds non-2xx", async () => {
|
|
179
|
+
responses.push(
|
|
180
|
+
new Response(null, {
|
|
181
|
+
status: 302,
|
|
182
|
+
headers: { Location: "https://files-edge.slack.com/cdn?t=1" },
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
responses.push(
|
|
186
|
+
new Response(null, { status: 403, statusText: "Forbidden" }),
|
|
187
|
+
);
|
|
188
|
+
await expect(
|
|
189
|
+
downloadSlackFile(
|
|
190
|
+
{
|
|
191
|
+
id: "F1",
|
|
192
|
+
name: "x.png",
|
|
193
|
+
urlPrivateDownload: "https://files.slack.com/a/b/download",
|
|
194
|
+
},
|
|
195
|
+
"xoxb-test",
|
|
196
|
+
),
|
|
197
|
+
).rejects.toThrow(/403/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("throws when a redirect has no Location header", async () => {
|
|
201
|
+
responses.push(new Response(null, { status: 302 }));
|
|
202
|
+
await expect(
|
|
203
|
+
downloadSlackFile(
|
|
204
|
+
{
|
|
205
|
+
id: "F1",
|
|
206
|
+
name: "x.png",
|
|
207
|
+
urlPrivateDownload: "https://files.slack.com/a/b/download",
|
|
208
|
+
},
|
|
209
|
+
"xoxb-test",
|
|
210
|
+
),
|
|
211
|
+
).rejects.toThrow(/no Location header/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("falls back to response Content-Type when file.mimetype is absent", async () => {
|
|
215
|
+
responses.push(
|
|
216
|
+
new Response(new Uint8Array([1]).buffer, {
|
|
217
|
+
status: 200,
|
|
218
|
+
headers: { "Content-Type": "image/webp; charset=binary" },
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
const result = await downloadSlackFile(
|
|
222
|
+
{
|
|
223
|
+
id: "F1",
|
|
224
|
+
name: "photo.webp",
|
|
225
|
+
urlPrivate: "https://files.slack.com/files-pri/T/F1/inline",
|
|
226
|
+
},
|
|
227
|
+
"xoxb-test",
|
|
228
|
+
);
|
|
229
|
+
expect(result?.mimeType).toBe("image/webp");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -99,6 +99,30 @@ function getWriteAuth(connection?: OAuthConnection): OAuthConnection | string {
|
|
|
99
99
|
return getSlackAuth(connection);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the bot token (raw string) and pass it to `fn`. Returns the
|
|
104
|
+
* callback's result, or `null` when no Slack auth is available.
|
|
105
|
+
*
|
|
106
|
+
* Bridges the Socket Mode case (cached string token) and the OAuth case
|
|
107
|
+
* (`OAuthConnection.withToken`) for callers that need a raw token to hand
|
|
108
|
+
* to a non-Slack-client API call — currently `downloadSlackFile` for inline
|
|
109
|
+
* file/image fetches. Slack-client method calls should keep going through
|
|
110
|
+
* `getReadAuth` / `getWriteAuth` and pass the union through.
|
|
111
|
+
*/
|
|
112
|
+
export async function withSlackBotToken<T>(
|
|
113
|
+
account: string | undefined,
|
|
114
|
+
fn: (token: string) => Promise<T>,
|
|
115
|
+
): Promise<T | null> {
|
|
116
|
+
// Resolve for this call's account even when the process cache is warm.
|
|
117
|
+
// Multi-workspace backfills can interleave, so use the returned connection
|
|
118
|
+
// directly instead of accepting any previously cached workspace token.
|
|
119
|
+
const resolvedAuth = await slackProvider.resolveConnection?.(account);
|
|
120
|
+
const auth = resolvedAuth ?? _cachedSlackWriteAuth;
|
|
121
|
+
if (!auth) return null;
|
|
122
|
+
if (typeof auth === "string") return fn(auth);
|
|
123
|
+
return auth.withToken(fn);
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
/**
|
|
103
127
|
* Run a read-path Slack call, falling back to the bot token if the cached
|
|
104
128
|
* user token is rejected with an auth error. On fallback, the read cache is
|
|
@@ -192,15 +216,31 @@ function mapConversation(conv: SlackConversation): Conversation {
|
|
|
192
216
|
};
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
function mapSlackFiles(
|
|
196
|
-
|
|
197
|
-
|
|
219
|
+
function mapSlackFiles(files: SlackMessage["files"]):
|
|
220
|
+
| Array<{
|
|
221
|
+
id?: string;
|
|
222
|
+
name: string;
|
|
223
|
+
mimetype?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Transient — only present on the in-flight `ProviderMessage.metadata`.
|
|
226
|
+
* The persisted `slackFiles` shape carries `{ id, name, mimetype }` only
|
|
227
|
+
* (see `slackFileMetadataSchema`). Callers that hydrate image attachments
|
|
228
|
+
* during backfill rely on this URL; persistence strips it before write.
|
|
229
|
+
*/
|
|
230
|
+
urlPrivateDownload?: string;
|
|
231
|
+
urlPrivate?: string;
|
|
232
|
+
}>
|
|
233
|
+
| undefined {
|
|
198
234
|
if (!files || files.length === 0) return undefined;
|
|
199
235
|
const mapped = files
|
|
200
236
|
.map((file) => ({
|
|
201
237
|
...(file.id ? { id: file.id } : {}),
|
|
202
238
|
name: file.name,
|
|
203
239
|
...(file.mimetype ? { mimetype: file.mimetype } : {}),
|
|
240
|
+
...(file.url_private_download
|
|
241
|
+
? { urlPrivateDownload: file.url_private_download }
|
|
242
|
+
: {}),
|
|
243
|
+
...(file.url_private ? { urlPrivate: file.url_private } : {}),
|
|
204
244
|
}))
|
|
205
245
|
.filter((file) => file.name.length > 0);
|
|
206
246
|
return mapped.length > 0 ? mapped : undefined;
|
|
@@ -419,8 +459,6 @@ export const slackProvider: MessagingProvider = {
|
|
|
419
459
|
if (conv.type === "dm" && conv.metadata?.dmUserId) {
|
|
420
460
|
const dmUserId = conv.metadata.dmUserId as string;
|
|
421
461
|
conv.name = await resolveUserName(auth, dmUserId);
|
|
422
|
-
|
|
423
|
-
|
|
424
462
|
}
|
|
425
463
|
}
|
|
426
464
|
|
|
@@ -20,8 +20,10 @@ import type {
|
|
|
20
20
|
SlackConversationsListResponse,
|
|
21
21
|
SlackConversationsOpenResponse,
|
|
22
22
|
SlackPostMessageResponse,
|
|
23
|
+
SlackReactionsAddResponse,
|
|
23
24
|
SlackSearchMessagesResponse,
|
|
24
25
|
SlackUserInfoResponse,
|
|
26
|
+
SlackUsersListResponse,
|
|
25
27
|
} from "./types.js";
|
|
26
28
|
|
|
27
29
|
const SLACK_API_BASE = "https://slack.com/api";
|
|
@@ -432,3 +434,28 @@ export async function searchMessages(
|
|
|
432
434
|
},
|
|
433
435
|
);
|
|
434
436
|
}
|
|
437
|
+
|
|
438
|
+
export async function addReaction(
|
|
439
|
+
connectionOrToken: OAuthConnection | string,
|
|
440
|
+
channel: string,
|
|
441
|
+
timestamp: string,
|
|
442
|
+
name: string,
|
|
443
|
+
): Promise<SlackReactionsAddResponse> {
|
|
444
|
+
return request<SlackReactionsAddResponse>(
|
|
445
|
+
connectionOrToken,
|
|
446
|
+
"reactions.add",
|
|
447
|
+
undefined,
|
|
448
|
+
{ channel, timestamp, name },
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function listUsers(
|
|
453
|
+
connectionOrToken: OAuthConnection | string,
|
|
454
|
+
limit = 200,
|
|
455
|
+
cursor?: string,
|
|
456
|
+
): Promise<SlackUsersListResponse> {
|
|
457
|
+
return request<SlackUsersListResponse>(connectionOrToken, "users.list", {
|
|
458
|
+
limit: String(limit),
|
|
459
|
+
cursor,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface SlackMessageDeepLinks {
|
|
2
|
+
appUrl?: string;
|
|
3
|
+
webUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function formatSlackPermalinkTimestamp(ts: string): string {
|
|
7
|
+
return ts.replace(".", "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildSlackAppMessageUrl(params: {
|
|
11
|
+
teamId?: string | null;
|
|
12
|
+
channelId: string;
|
|
13
|
+
messageTs: string;
|
|
14
|
+
}): string | undefined {
|
|
15
|
+
const teamId = params.teamId?.trim();
|
|
16
|
+
if (!teamId) return undefined;
|
|
17
|
+
|
|
18
|
+
const search = new URLSearchParams({
|
|
19
|
+
team: teamId,
|
|
20
|
+
id: params.channelId,
|
|
21
|
+
message: params.messageTs,
|
|
22
|
+
});
|
|
23
|
+
return `slack://channel?${search.toString()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeSlackTeamUrl(teamUrl?: string | null): string | undefined {
|
|
27
|
+
const trimmed = teamUrl?.trim();
|
|
28
|
+
if (!trimmed) return undefined;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(trimmed);
|
|
32
|
+
if (parsed.protocol !== "https:") return undefined;
|
|
33
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildSlackWebMessageUrl(params: {
|
|
40
|
+
teamUrl?: string | null;
|
|
41
|
+
channelId: string;
|
|
42
|
+
messageTs: string;
|
|
43
|
+
}): string | undefined {
|
|
44
|
+
const teamUrl = normalizeSlackTeamUrl(params.teamUrl);
|
|
45
|
+
if (!teamUrl) return undefined;
|
|
46
|
+
|
|
47
|
+
return `${teamUrl}/archives/${encodeURIComponent(
|
|
48
|
+
params.channelId,
|
|
49
|
+
)}/p${formatSlackPermalinkTimestamp(params.messageTs)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildSlackMessageDeepLinks(params: {
|
|
53
|
+
teamId?: string | null;
|
|
54
|
+
teamUrl?: string | null;
|
|
55
|
+
channelId: string;
|
|
56
|
+
messageTs: string;
|
|
57
|
+
}): SlackMessageDeepLinks | undefined {
|
|
58
|
+
const appUrl = buildSlackAppMessageUrl(params);
|
|
59
|
+
const webUrl = buildSlackWebMessageUrl(params);
|
|
60
|
+
if (!appUrl && !webUrl) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
...(appUrl ? { appUrl } : {}),
|
|
63
|
+
...(webUrl ? { webUrl } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack file download for the assistant-side backfill path.
|
|
3
|
+
*
|
|
4
|
+
* The gateway runs the live inbound path and downloads files via its own
|
|
5
|
+
* `gateway/src/slack/download.ts`. The assistant cannot import that module
|
|
6
|
+
* (different package, different fetch infra), so the thread-backfill path
|
|
7
|
+
* has its own minimal downloader here.
|
|
8
|
+
*
|
|
9
|
+
* Both implementations target the same Slack contract:
|
|
10
|
+
* - `url_private_download` (preferred) / `url_private` (fallback) are
|
|
11
|
+
* Slack-hosted URLs requiring bot-token auth.
|
|
12
|
+
* - Slack typically redirects to a CDN host (e.g. `files-edge.slack.com`)
|
|
13
|
+
* where the signed redirect URL is self-authenticating; the WHATWG fetch
|
|
14
|
+
* spec strips `Authorization` on cross-origin redirects, so we manually
|
|
15
|
+
* follow the redirect without re-sending the bot token.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getLogger } from "../../../util/logger.js";
|
|
19
|
+
|
|
20
|
+
const log = getLogger("slack-download");
|
|
21
|
+
|
|
22
|
+
export interface DownloadedSlackFile {
|
|
23
|
+
filename: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
/** Base64-encoded file bytes. */
|
|
26
|
+
data: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SlackFileDownloadInput {
|
|
30
|
+
id?: string;
|
|
31
|
+
name: string;
|
|
32
|
+
mimetype?: string;
|
|
33
|
+
urlPrivateDownload?: string;
|
|
34
|
+
urlPrivate?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Download a Slack file using a raw bot token for authentication.
|
|
41
|
+
*
|
|
42
|
+
* The caller is responsible for resolving the token from the slack adapter
|
|
43
|
+
* (`withSlackBotToken`); this module stays decoupled from the auth-resolution
|
|
44
|
+
* dispatch so it remains trivially mockable in tests.
|
|
45
|
+
*
|
|
46
|
+
* Returns `null` when no usable URL is present on the file metadata — callers
|
|
47
|
+
* commonly pass file shapes that have already been sanitized for persistence
|
|
48
|
+
* (`{ id, name, mimetype }`) and have no way to download. This is treated as
|
|
49
|
+
* an expected branch rather than an error.
|
|
50
|
+
*
|
|
51
|
+
* Throws on transport / HTTP errors so the caller can decide whether to log
|
|
52
|
+
* and skip or fail the surrounding operation. The thread-backfill caller
|
|
53
|
+
* logs and proceeds with the text-only message rather than failing the whole
|
|
54
|
+
* backfill.
|
|
55
|
+
*/
|
|
56
|
+
export async function downloadSlackFile(
|
|
57
|
+
file: SlackFileDownloadInput,
|
|
58
|
+
token: string,
|
|
59
|
+
): Promise<DownloadedSlackFile | null> {
|
|
60
|
+
const url = file.urlPrivateDownload ?? file.urlPrivate;
|
|
61
|
+
if (!url) {
|
|
62
|
+
log.debug(
|
|
63
|
+
{ fileId: file.id, name: file.name },
|
|
64
|
+
"Slack file has no download URL; skipping",
|
|
65
|
+
);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let response = await fetch(url, {
|
|
70
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
71
|
+
redirect: "manual",
|
|
72
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (response.status >= 300 && response.status < 400) {
|
|
76
|
+
const location = response.headers.get("Location");
|
|
77
|
+
if (!location) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Slack file ${file.id ?? file.name} returned ${response.status} redirect with no Location header`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
// CDN redirect URLs are signed; no Authorization needed. Resolve
|
|
83
|
+
// relative locations against the original URL.
|
|
84
|
+
const resolvedLocation = new URL(location, url).href;
|
|
85
|
+
response = await fetch(resolvedLocation, {
|
|
86
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Failed to download Slack file ${file.id ?? file.name}: ${response.status} ${response.statusText}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const buffer = await response.arrayBuffer();
|
|
97
|
+
const mimeType =
|
|
98
|
+
file.mimetype ||
|
|
99
|
+
response.headers.get("Content-Type")?.split(";")[0]?.trim() ||
|
|
100
|
+
"application/octet-stream";
|
|
101
|
+
const filename = file.name || `slack_file_${file.id ?? "unknown"}`;
|
|
102
|
+
const data = Buffer.from(buffer).toString("base64");
|
|
103
|
+
return { filename, mimeType, data };
|
|
104
|
+
}
|
|
@@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import {
|
|
4
4
|
mergeSlackMetadata,
|
|
5
5
|
readSlackMetadata,
|
|
6
|
+
readSlackMetadataFromMessageMetadata,
|
|
6
7
|
type SlackMessageMetadata,
|
|
7
8
|
writeSlackMetadata,
|
|
8
9
|
} from "./message-metadata.js";
|
|
@@ -113,6 +114,7 @@ describe("readSlackMetadata", () => {
|
|
|
113
114
|
channelTs: "1700000000.000100",
|
|
114
115
|
threadTs: "1699999999.000000",
|
|
115
116
|
displayName: "Alice",
|
|
117
|
+
actorExternalUserId: "U_ALICE",
|
|
116
118
|
eventKind: "message",
|
|
117
119
|
editedAt: 1700000123,
|
|
118
120
|
};
|
|
@@ -138,6 +140,35 @@ describe("readSlackMetadata", () => {
|
|
|
138
140
|
});
|
|
139
141
|
});
|
|
140
142
|
|
|
143
|
+
describe("readSlackMetadataFromMessageMetadata", () => {
|
|
144
|
+
const meta: SlackMessageMetadata = {
|
|
145
|
+
source: "slack",
|
|
146
|
+
channelId: "C123",
|
|
147
|
+
channelTs: "1700000000.000100",
|
|
148
|
+
threadTs: "1699999999.000000",
|
|
149
|
+
eventKind: "message",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
test("reads nested slackMeta from a message metadata envelope", () => {
|
|
153
|
+
expect(
|
|
154
|
+
readSlackMetadataFromMessageMetadata(
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
userMessageChannel: "slack",
|
|
157
|
+
slackMeta: writeSlackMetadata(meta),
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
).toEqual(meta);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("can read flat legacy Slack metadata when explicitly allowed", () => {
|
|
164
|
+
const raw = writeSlackMetadata(meta);
|
|
165
|
+
expect(readSlackMetadataFromMessageMetadata(raw)).toBeNull();
|
|
166
|
+
expect(
|
|
167
|
+
readSlackMetadataFromMessageMetadata(raw, { allowFlatLegacy: true }),
|
|
168
|
+
).toEqual(meta);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
141
172
|
describe("writeSlackMetadata", () => {
|
|
142
173
|
test("round-trips through readSlackMetadata", () => {
|
|
143
174
|
const meta: SlackMessageMetadata = {
|
|
@@ -146,6 +177,7 @@ describe("writeSlackMetadata", () => {
|
|
|
146
177
|
channelTs: "1700000000.000100",
|
|
147
178
|
threadTs: "1699999999.000000",
|
|
148
179
|
displayName: "Alice",
|
|
180
|
+
actorExternalUserId: "U_ALICE",
|
|
149
181
|
eventKind: "message",
|
|
150
182
|
};
|
|
151
183
|
const raw = writeSlackMetadata(meta);
|
|
@@ -38,6 +38,7 @@ export const slackMessageMetadataSchema = z.object({
|
|
|
38
38
|
channelTs: z.string(),
|
|
39
39
|
threadTs: z.string().optional(),
|
|
40
40
|
displayName: z.string().optional(),
|
|
41
|
+
actorExternalUserId: z.string().optional(),
|
|
41
42
|
eventKind: z.enum(["message", "reaction"]),
|
|
42
43
|
reaction: slackReactionMetadataSchema.optional(),
|
|
43
44
|
editedAt: z.number().optional(),
|
|
@@ -75,6 +76,32 @@ export function readSlackMetadata(
|
|
|
75
76
|
return result.success ? result.data : null;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
export function readSlackMetadataFromMessageMetadata(
|
|
80
|
+
metadata: string | null | undefined,
|
|
81
|
+
opts?: { allowFlatLegacy?: boolean },
|
|
82
|
+
): SlackMessageMetadata | null {
|
|
83
|
+
if (!metadata) return null;
|
|
84
|
+
|
|
85
|
+
let parent: Record<string, unknown> | null = null;
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(metadata) as unknown;
|
|
88
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
89
|
+
parent = parsed as Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!parent) return null;
|
|
95
|
+
|
|
96
|
+
const nested = parent.slackMeta;
|
|
97
|
+
if (typeof nested === "string") {
|
|
98
|
+
const parsedNested = readSlackMetadata(nested);
|
|
99
|
+
if (parsedNested) return parsedNested;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return opts?.allowFlatLegacy ? readSlackMetadata(metadata) : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/**
|
|
79
106
|
* Serialize `SlackMessageMetadata` to a JSON string suitable for a fresh
|
|
80
107
|
* write to the `messages.metadata` column. Use `mergeSlackMetadata` when an
|