@vellumai/assistant 0.11.4 → 0.11.5-staging.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/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { audienceForReader } from "../message-audience.js";
|
|
4
|
+
|
|
5
|
+
describe("audienceForReader", () => {
|
|
6
|
+
test("restricts a reply in a Slack channel to the named reader", () => {
|
|
7
|
+
expect(audienceForReader("slack", "C0ROOM", "U0READER")).toEqual({
|
|
8
|
+
kind: "oneReader",
|
|
9
|
+
userId: "U0READER",
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("leaves a Slack DM unrestricted", () => {
|
|
14
|
+
// Nobody else can read a DM, so restricting buys no privacy and costs
|
|
15
|
+
// durability: an ephemeral message does not survive a reload.
|
|
16
|
+
expect(audienceForReader("slack", "D0DIRECT", "U0READER")).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("restricts a private group, which several readers share", () => {
|
|
20
|
+
// A `G` id is a shared room like a `C` one. Testing for a DM rather than
|
|
21
|
+
// for a channel id is what keeps this from leaking into the group.
|
|
22
|
+
expect(audienceForReader("slack", "G0PRIVATE", "U0READER")).toEqual({
|
|
23
|
+
kind: "oneReader",
|
|
24
|
+
userId: "U0READER",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("leaves every other channel unrestricted", () => {
|
|
29
|
+
for (const channel of ["telegram", "discord", "whatsapp", "email"]) {
|
|
30
|
+
expect(
|
|
31
|
+
audienceForReader(channel, "C0ROOM", "U0READER"),
|
|
32
|
+
`${channel} has no way to show one reader a message in a shared room`,
|
|
33
|
+
).toBeUndefined();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("stays unrestricted when there is no reader to name", () => {
|
|
38
|
+
expect(audienceForReader("slack", "C0ROOM", undefined)).toBeUndefined();
|
|
39
|
+
expect(audienceForReader("slack", "C0ROOM", null)).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("tolerates an absent channel or room rather than throwing", () => {
|
|
43
|
+
// Several callers hold values that were never narrowed, so the answer for
|
|
44
|
+
// an unknown channel has to be the safe one rather than an exception.
|
|
45
|
+
expect(audienceForReader(undefined, "C0ROOM", "U0READER")).toBeUndefined();
|
|
46
|
+
expect(audienceForReader(null, "C0ROOM", "U0READER")).toBeUndefined();
|
|
47
|
+
expect(audienceForReader("slack", null, "U0READER")).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -17,9 +17,10 @@ describe("INTERFACE_IDS", () => {
|
|
|
17
17
|
).toBe(true);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
test("
|
|
20
|
+
test("includes desktop and existing interfaces", () => {
|
|
21
21
|
for (const id of [
|
|
22
22
|
"macos",
|
|
23
|
+
"windows",
|
|
23
24
|
"ios",
|
|
24
25
|
"cli",
|
|
25
26
|
"telegram",
|
|
@@ -41,8 +42,9 @@ describe("INTERACTIVE_INTERFACES", () => {
|
|
|
41
42
|
expect(INTERACTIVE_INTERFACES.has("chrome-extension" as never)).toBe(false);
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
test("
|
|
45
|
+
test("includes desktop clients", () => {
|
|
45
46
|
expect(INTERACTIVE_INTERFACES.has("macos")).toBe(true);
|
|
47
|
+
expect(INTERACTIVE_INTERFACES.has("windows")).toBe(true);
|
|
46
48
|
});
|
|
47
49
|
});
|
|
48
50
|
|
|
@@ -51,8 +53,9 @@ describe("isInterfaceId", () => {
|
|
|
51
53
|
expect(isInterfaceId("chrome-extension")).toBe(true);
|
|
52
54
|
});
|
|
53
55
|
|
|
54
|
-
test("returns true for
|
|
56
|
+
test("returns true for desktop interfaces", () => {
|
|
55
57
|
expect(isInterfaceId("macos")).toBe(true);
|
|
58
|
+
expect(isInterfaceId("windows")).toBe(true);
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
test("returns false for unknown interface", () => {
|
|
@@ -68,6 +71,7 @@ describe("parseInterfaceId", () => {
|
|
|
68
71
|
test("returns canonical ID for valid interface", () => {
|
|
69
72
|
expect(parseInterfaceId("web")).toBe("web");
|
|
70
73
|
expect(parseInterfaceId("macos")).toBe("macos");
|
|
74
|
+
expect(parseInterfaceId("windows")).toBe("windows");
|
|
71
75
|
});
|
|
72
76
|
|
|
73
77
|
test("normalizes legacy 'vellum' alias to 'web'", () => {
|
|
@@ -103,6 +107,16 @@ describe("supportsHostProxy", () => {
|
|
|
103
107
|
expect(supportsHostProxy("macos", "host_browser")).toBe(true);
|
|
104
108
|
});
|
|
105
109
|
|
|
110
|
+
test("windows exposes its implemented host capabilities", () => {
|
|
111
|
+
expect(supportsHostProxy("windows")).toBe(true);
|
|
112
|
+
expect(supportsHostProxy("windows", "host_bash")).toBe(true);
|
|
113
|
+
expect(supportsHostProxy("windows", "host_file")).toBe(true);
|
|
114
|
+
expect(supportsHostProxy("windows", "host_cu")).toBe(true);
|
|
115
|
+
expect(supportsHostProxy("windows", "host_browser")).toBe(true);
|
|
116
|
+
expect(supportsHostProxy("windows", "host_ui_snapshot")).toBe(true);
|
|
117
|
+
expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
106
120
|
// ── chrome-extension: only host_browser. ──
|
|
107
121
|
test("chrome-extension returns false (no capability)", () => {
|
|
108
122
|
// Chrome extension does not support "any host proxy at all" — it only
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who a reply should be visible to, decided once for every producer.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { MessageAudience } from "@vellumai/gateway-client";
|
|
6
|
+
|
|
7
|
+
import { isSlackDmConversation } from "../messaging/providers/slack/message-metadata.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The audience a reply addressed at `userId` should carry, or `undefined` when
|
|
11
|
+
* the whole room may see it.
|
|
12
|
+
*
|
|
13
|
+
* Restricting a message only means something where someone else could read it.
|
|
14
|
+
* A room with one reader gets no restriction: a Slack DM is already private,
|
|
15
|
+
* and marking it restricted there costs durability, because an ephemeral
|
|
16
|
+
* message does not survive a reload.
|
|
17
|
+
*
|
|
18
|
+
* Slack is the only channel that can show one reader a message in a room it
|
|
19
|
+
* shares with others. The test is for a DM rather than for a channel id, so
|
|
20
|
+
* every other room shape counts as shared: a private group holds several
|
|
21
|
+
* readers even though its id begins with `G` rather than `C`.
|
|
22
|
+
*
|
|
23
|
+
* `sourceChannel` and `chatId` are loosely typed because the only questions
|
|
24
|
+
* asked of them are whether the channel is Slack and whether the room is a
|
|
25
|
+
* DM, and several callers hold values that were never narrowed. An unknown
|
|
26
|
+
* channel answers with no restriction rather than throwing.
|
|
27
|
+
*/
|
|
28
|
+
export function audienceForReader(
|
|
29
|
+
sourceChannel: string | null | undefined,
|
|
30
|
+
chatId: string | null | undefined,
|
|
31
|
+
userId: string | null | undefined,
|
|
32
|
+
): MessageAudience | undefined {
|
|
33
|
+
if (sourceChannel !== "slack" || !userId || !chatId) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (isSlackDmConversation(chatId)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return { kind: "oneReader", userId };
|
|
40
|
+
}
|
package/src/channels/types.ts
CHANGED
|
@@ -166,6 +166,7 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
|
|
|
166
166
|
|
|
167
167
|
export const INTERFACE_IDS = [
|
|
168
168
|
"macos",
|
|
169
|
+
"windows",
|
|
169
170
|
"ios",
|
|
170
171
|
"cli",
|
|
171
172
|
"telegram",
|
|
@@ -264,6 +265,7 @@ export function parseClientOs(value: unknown): ClientOs | null {
|
|
|
264
265
|
*/
|
|
265
266
|
export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
|
|
266
267
|
"macos",
|
|
268
|
+
"windows",
|
|
267
269
|
"ios",
|
|
268
270
|
"cli",
|
|
269
271
|
"web",
|
|
@@ -274,9 +276,9 @@ export function isInteractiveInterface(id: InterfaceId): boolean {
|
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
/**
|
|
277
|
-
* Host proxy capabilities that an interface can support.
|
|
278
|
-
*
|
|
279
|
-
* host_browser
|
|
279
|
+
* Host proxy capabilities that an interface can support. macOS supports all
|
|
280
|
+
* of them, Windows withholds app control, and chrome-extension supports only
|
|
281
|
+
* host_browser through the Chrome DevTools Protocol proxy.
|
|
280
282
|
*/
|
|
281
283
|
export const HOST_PROXY_CAPABILITIES = [
|
|
282
284
|
"host_bash",
|
|
@@ -290,29 +292,28 @@ export const HOST_PROXY_CAPABILITIES = [
|
|
|
290
292
|
export type HostProxyCapability = (typeof HOST_PROXY_CAPABILITIES)[number];
|
|
291
293
|
|
|
292
294
|
/**
|
|
293
|
-
* Interfaces that support
|
|
294
|
-
*
|
|
295
|
-
* by the discriminated transport metadata union and by the
|
|
295
|
+
* Interfaces that support desktop host-proxy tools. This identity is used by
|
|
296
|
+
* the discriminated transport metadata union and by the
|
|
296
297
|
* `supportsHostProxy(id)` type predicate.
|
|
297
298
|
*
|
|
298
299
|
* Extend this literal type AND the `supportsHostProxy` implementation
|
|
299
|
-
* below in lock-step when adding a new host-capable client
|
|
300
|
-
* Linux
|
|
300
|
+
* below in lock-step when adding a new host-capable client such as native
|
|
301
|
+
* Linux.
|
|
301
302
|
*/
|
|
302
|
-
export type HostProxyInterfaceId = "macos";
|
|
303
|
+
export type HostProxyInterfaceId = "macos" | "windows";
|
|
303
304
|
|
|
304
305
|
/**
|
|
305
306
|
* Whether the interface supports a host proxy capability.
|
|
306
307
|
*
|
|
307
308
|
* The no-arg form `supportsHostProxy(id)` asks "is this interface a desktop
|
|
308
|
-
* host-proxy client?"
|
|
309
|
+
* host-proxy client?" It returns `true` for native desktop clients and is the
|
|
309
310
|
* predicate that narrows `InterfaceId` to `HostProxyInterfaceId`. It returns
|
|
310
311
|
* `false` for chrome-extension because chrome-extension only supports
|
|
311
312
|
* `host_browser`, and the no-arg form is the gate that legacy desktop-only
|
|
312
313
|
* call sites use (e.g. preactivating computer-use, restoring host proxies
|
|
313
|
-
* in the drain queue). Callers that want to check a single capability
|
|
314
|
+
* in the drain queue). Callers that want to check a single capability,
|
|
314
315
|
* for example, to decide whether to keep `hostBrowserProxy` available for
|
|
315
|
-
* chrome-extension
|
|
316
|
+
* chrome-extension, should pass the capability explicitly:
|
|
316
317
|
* `supportsHostProxy(id, "host_browser")`.
|
|
317
318
|
*/
|
|
318
319
|
export function supportsHostProxy(id: InterfaceId): id is HostProxyInterfaceId;
|
|
@@ -331,6 +332,9 @@ export function supportsHostProxy(
|
|
|
331
332
|
if (id === "macos") {
|
|
332
333
|
return true;
|
|
333
334
|
}
|
|
335
|
+
if (id === "windows") {
|
|
336
|
+
return capability == null || capability !== "host_app_control";
|
|
337
|
+
}
|
|
334
338
|
if (id === "chrome-extension" && capability === "host_browser") {
|
|
335
339
|
return true;
|
|
336
340
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory injection indexes CLI help via {@link buildCliCommandHelpContent}.
|
|
3
|
+
* Setup prompts like "Setup <app> for me" should retrieve `plugins search` /
|
|
4
|
+
* `skills search` before the model falls back to web search.
|
|
5
|
+
*
|
|
6
|
+
* These assertions lock the retrieval phrases in the indexed blob so a later
|
|
7
|
+
* wording change cannot silently drop them.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { buildCliCommandHelpContent } from "../../plugins/defaults/memory/substrate/cli-command-content.js";
|
|
12
|
+
import { pluginsHelp } from "../commands/plugins.help.js";
|
|
13
|
+
import { skillsHelp } from "../commands/skills.help.js";
|
|
14
|
+
|
|
15
|
+
function searchHelp(
|
|
16
|
+
help: { subcommands?: Array<{ name: string; helpText?: string }> },
|
|
17
|
+
): { name: string; helpText?: string } {
|
|
18
|
+
const search = help.subcommands?.find((sub) => sub.name === "search");
|
|
19
|
+
if (!search) {
|
|
20
|
+
throw new Error("expected a search subcommand");
|
|
21
|
+
}
|
|
22
|
+
return search;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("catalog search help for setup-intent retrieval", () => {
|
|
26
|
+
test("plugins help indexes setup, superpowers, and web-search fallback", () => {
|
|
27
|
+
const indexed = buildCliCommandHelpContent(pluginsHelp);
|
|
28
|
+
const search = searchHelp(pluginsHelp);
|
|
29
|
+
|
|
30
|
+
expect(search.helpText).toBeDefined();
|
|
31
|
+
expect(indexed.toLowerCase()).toContain("superpowers");
|
|
32
|
+
expect(indexed).toContain("Setup <app> for me");
|
|
33
|
+
expect(indexed.toLowerCase()).toContain("before searching the web");
|
|
34
|
+
expect(indexed).toContain("assistant plugins search");
|
|
35
|
+
expect(indexed).toContain("assistant skills search");
|
|
36
|
+
expect(indexed.toLowerCase()).not.toContain("empty query");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("skills help indexes setup as the second stop before web search", () => {
|
|
40
|
+
const indexed = buildCliCommandHelpContent(skillsHelp);
|
|
41
|
+
const search = searchHelp(skillsHelp);
|
|
42
|
+
|
|
43
|
+
expect(search.helpText).toBeDefined();
|
|
44
|
+
expect(indexed).toContain("Second stop after plugin search");
|
|
45
|
+
expect(indexed).toContain("Setup <app> for me");
|
|
46
|
+
expect(indexed.toLowerCase()).toContain("before searching the web");
|
|
47
|
+
expect(indexed).toContain("assistant skills search");
|
|
48
|
+
expect(indexed).toContain("try web search");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
* - `revoke` with no flags (channel: undefined)
|
|
14
14
|
* - `revoke --channel phone` sends channel param
|
|
15
15
|
* - `create --channel fax` → invalid channel, exits with code 1, IPC not called
|
|
16
|
+
* - the accepted `--channel` set is the canonical CHANNEL_IDS vocabulary
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
20
|
|
|
21
|
+
import { CHANNEL_IDS } from "@vellumai/service-contracts/channels";
|
|
20
22
|
import { Command } from "commander";
|
|
21
23
|
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
@@ -242,6 +244,35 @@ describe("channel-verification-sessions create", () => {
|
|
|
242
244
|
expect(lastIpcCall).toBeNull();
|
|
243
245
|
});
|
|
244
246
|
|
|
247
|
+
test("--channel discord reaches IPC (canonical vocabulary, not a local copy)", async () => {
|
|
248
|
+
const { exitCode } = await runCommand([
|
|
249
|
+
"channel-verification-sessions",
|
|
250
|
+
"create",
|
|
251
|
+
"--channel",
|
|
252
|
+
"discord",
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
expect(exitCode).toBe(0);
|
|
256
|
+
expect(lastIpcCall!.method).toBe("channel_verification_sessions_create");
|
|
257
|
+
expect(lastIpcCall!.params).toMatchObject({
|
|
258
|
+
body: { channel: "discord", purpose: "guardian" },
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("every canonical channel id is accepted", async () => {
|
|
263
|
+
for (const channel of CHANNEL_IDS) {
|
|
264
|
+
const { exitCode } = await runCommand([
|
|
265
|
+
"channel-verification-sessions",
|
|
266
|
+
"create",
|
|
267
|
+
"--channel",
|
|
268
|
+
channel,
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
expect(exitCode).toBe(0);
|
|
272
|
+
expect(lastIpcCall!.params).toMatchObject({ body: { channel } });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
245
276
|
test("IPC error results in exit code 1", async () => {
|
|
246
277
|
mockIpcResult = { ok: false, error: "Could not connect" };
|
|
247
278
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `assistant conversations search`.
|
|
3
|
+
*
|
|
4
|
+
* Validates IPC plumbing onto `conversations_search`, human vs `--json`
|
|
5
|
+
* output, empty-result copy, and validation of the search term / --limit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as nodeFs from "node:fs";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
|
|
13
|
+
let lastIpcCall: { method: string; params?: Record<string, unknown> } | null =
|
|
14
|
+
null;
|
|
15
|
+
|
|
16
|
+
let mockIpcResult: {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: string;
|
|
20
|
+
statusCode?: number;
|
|
21
|
+
} = { ok: true, result: { query: "flux", results: [] } };
|
|
22
|
+
|
|
23
|
+
mock.module("../../../ipc/cli-client.js", () => ({
|
|
24
|
+
cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
|
|
25
|
+
lastIpcCall = { method, params };
|
|
26
|
+
return mockIpcResult;
|
|
27
|
+
},
|
|
28
|
+
exitCodeFromIpcResult: (r: { statusCode?: number }) =>
|
|
29
|
+
r.statusCode === undefined
|
|
30
|
+
? 10
|
|
31
|
+
: r.statusCode >= 500
|
|
32
|
+
? 3
|
|
33
|
+
: r.statusCode >= 400
|
|
34
|
+
? 2
|
|
35
|
+
: 1,
|
|
36
|
+
exitFromIpcResult: () => {
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const fakeLogger = {
|
|
42
|
+
info: () => {},
|
|
43
|
+
warn: () => {},
|
|
44
|
+
error: () => {},
|
|
45
|
+
debug: () => {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
mock.module("../../../util/logger.js", () => ({
|
|
49
|
+
getLogger: () => fakeLogger,
|
|
50
|
+
getCliLogger: () => fakeLogger,
|
|
51
|
+
initLogger: () => {},
|
|
52
|
+
truncateForLog: (v: string) => v,
|
|
53
|
+
pruneOldLogFiles: () => 0,
|
|
54
|
+
LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
|
|
55
|
+
getCurrentLogFilePath: () => "/tmp/test-assistant.log",
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const realFs = { ...nodeFs };
|
|
59
|
+
mock.module("node:fs", () => ({ ...realFs }));
|
|
60
|
+
|
|
61
|
+
const { registerConversationsCommand } = await import("../conversations.js");
|
|
62
|
+
|
|
63
|
+
const THREE_HOURS_AGO = Date.now() - 3 * 60 * 60 * 1000;
|
|
64
|
+
|
|
65
|
+
const matchingResult = {
|
|
66
|
+
conversationId: "conv-xyz",
|
|
67
|
+
conversationTitle: "Quarterly metrics",
|
|
68
|
+
conversationUpdatedAt: THREE_HOURS_AGO,
|
|
69
|
+
matchingMessages: [
|
|
70
|
+
{
|
|
71
|
+
messageId: "msg-1",
|
|
72
|
+
role: "user",
|
|
73
|
+
excerpt: "let's review the flux capacitor numbers",
|
|
74
|
+
createdAt: THREE_HOURS_AGO,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
messageId: "msg-2",
|
|
78
|
+
role: "assistant",
|
|
79
|
+
excerpt: "the Q3 flux capacitor spend is on track",
|
|
80
|
+
createdAt: THREE_HOURS_AGO,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
async function runSearch(
|
|
86
|
+
args: string[],
|
|
87
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
88
|
+
const stdoutChunks: string[] = [];
|
|
89
|
+
const stderrChunks: string[] = [];
|
|
90
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
91
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
92
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
93
|
+
stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
94
|
+
return true;
|
|
95
|
+
}) as typeof process.stdout.write;
|
|
96
|
+
process.stderr.write = ((chunk: unknown) => {
|
|
97
|
+
stderrChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
98
|
+
return true;
|
|
99
|
+
}) as typeof process.stderr.write;
|
|
100
|
+
|
|
101
|
+
const program = new Command();
|
|
102
|
+
program.exitOverride();
|
|
103
|
+
program.configureOutput({
|
|
104
|
+
writeErr: (s) => {
|
|
105
|
+
stderrChunks.push(s);
|
|
106
|
+
},
|
|
107
|
+
writeOut: (s) => {
|
|
108
|
+
stdoutChunks.push(s);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
registerConversationsCommand(program);
|
|
112
|
+
try {
|
|
113
|
+
await program.parseAsync(["node", "assistant", "conversations", ...args]);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const isHelp =
|
|
116
|
+
typeof err === "object" &&
|
|
117
|
+
err !== null &&
|
|
118
|
+
"code" in err &&
|
|
119
|
+
(err as { code?: string }).code === "commander.helpDisplayed";
|
|
120
|
+
if (!isHelp && (process.exitCode === 0 || process.exitCode === undefined)) {
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
!isHelp &&
|
|
125
|
+
err instanceof Error &&
|
|
126
|
+
!stderrChunks.some((s) => s.includes(err.message))
|
|
127
|
+
) {
|
|
128
|
+
stderrChunks.push(err.message);
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
process.stdout.write = originalStdoutWrite;
|
|
132
|
+
process.stderr.write = originalStderrWrite;
|
|
133
|
+
}
|
|
134
|
+
const code = Number(process.exitCode ?? 0);
|
|
135
|
+
process.exitCode = 0;
|
|
136
|
+
return {
|
|
137
|
+
code,
|
|
138
|
+
stdout: stdoutChunks.join(""),
|
|
139
|
+
stderr: stderrChunks.join(""),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
lastIpcCall = null;
|
|
145
|
+
process.exitCode = 0;
|
|
146
|
+
mockIpcResult = { ok: true, result: { query: "flux", results: [] } };
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
process.exitCode = 0;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("conversations search", () => {
|
|
154
|
+
test("forwards the term to conversations_search as queryParams.q", async () => {
|
|
155
|
+
mockIpcResult = {
|
|
156
|
+
ok: true,
|
|
157
|
+
result: { query: "flux capacitor", results: [matchingResult] },
|
|
158
|
+
};
|
|
159
|
+
const { code } = await runSearch(["search", "flux capacitor"]);
|
|
160
|
+
expect(code).toBe(0);
|
|
161
|
+
expect(lastIpcCall?.method).toBe("conversations_search");
|
|
162
|
+
expect(lastIpcCall?.params).toEqual({
|
|
163
|
+
queryParams: { q: "flux capacitor" },
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("passes --limit as a query param string", async () => {
|
|
168
|
+
mockIpcResult = {
|
|
169
|
+
ok: true,
|
|
170
|
+
result: { query: "flux", results: [] },
|
|
171
|
+
};
|
|
172
|
+
const { code } = await runSearch(["search", "flux", "--limit", "5"]);
|
|
173
|
+
expect(code).toBe(0);
|
|
174
|
+
expect(lastIpcCall?.params).toEqual({
|
|
175
|
+
queryParams: { q: "flux", limit: "5" },
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("prints conversation rows and excerpts in human mode", async () => {
|
|
180
|
+
mockIpcResult = {
|
|
181
|
+
ok: true,
|
|
182
|
+
result: { query: "flux", results: [matchingResult] },
|
|
183
|
+
};
|
|
184
|
+
const { code, stdout } = await runSearch(["search", "flux"]);
|
|
185
|
+
expect(code).toBe(0);
|
|
186
|
+
expect(stdout).toContain("conv-xyz");
|
|
187
|
+
expect(stdout).toContain("Quarterly metrics");
|
|
188
|
+
expect(stdout).toContain("3h ago");
|
|
189
|
+
expect(stdout).toContain("user let's review the flux capacitor numbers");
|
|
190
|
+
expect(stdout).toContain(
|
|
191
|
+
"assistant the Q3 flux capacitor spend is on track",
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("prints Untitled when the conversation title is null", async () => {
|
|
196
|
+
mockIpcResult = {
|
|
197
|
+
ok: true,
|
|
198
|
+
result: {
|
|
199
|
+
query: "flux",
|
|
200
|
+
results: [
|
|
201
|
+
{
|
|
202
|
+
...matchingResult,
|
|
203
|
+
conversationTitle: null,
|
|
204
|
+
matchingMessages: [],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const { code, stdout } = await runSearch(["search", "flux"]);
|
|
210
|
+
expect(code).toBe(0);
|
|
211
|
+
expect(stdout).toContain("Untitled");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("prints a no-match line when the daemon returns no results", async () => {
|
|
215
|
+
mockIpcResult = {
|
|
216
|
+
ok: true,
|
|
217
|
+
result: { query: "no-such-term", results: [] },
|
|
218
|
+
};
|
|
219
|
+
const { code, stdout } = await runSearch(["search", "no-such-term"]);
|
|
220
|
+
expect(code).toBe(0);
|
|
221
|
+
expect(lastIpcCall?.method).toBe("conversations_search");
|
|
222
|
+
expect(stdout).toBe('No conversations matched "no-such-term"\n');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("--json emits the daemon payload", async () => {
|
|
226
|
+
mockIpcResult = {
|
|
227
|
+
ok: true,
|
|
228
|
+
result: { query: "flux", results: [matchingResult] },
|
|
229
|
+
};
|
|
230
|
+
const { code, stdout } = await runSearch(["search", "flux", "--json"]);
|
|
231
|
+
expect(code).toBe(0);
|
|
232
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
233
|
+
query: "flux",
|
|
234
|
+
results: [matchingResult],
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("--json reports IPC failure as { ok: false } and exits non-zero", async () => {
|
|
239
|
+
mockIpcResult = { ok: false, error: "assistant not running" };
|
|
240
|
+
const { code, stdout } = await runSearch(["search", "flux", "--json"]);
|
|
241
|
+
expect(code).toBe(10);
|
|
242
|
+
expect(JSON.parse(stdout)).toEqual({
|
|
243
|
+
ok: false,
|
|
244
|
+
error: "assistant not running",
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("rejects a whitespace-only term before IPC", async () => {
|
|
249
|
+
const { code, stderr } = await runSearch(["search", " "]);
|
|
250
|
+
expect(code).toBe(1);
|
|
251
|
+
expect(lastIpcCall).toBeNull();
|
|
252
|
+
expect(stderr).toContain("Search term must be a non-empty string");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("--json rejects a whitespace-only term as { ok: false }", async () => {
|
|
256
|
+
const { code, stdout } = await runSearch(["search", " ", "--json"]);
|
|
257
|
+
expect(code).toBe(1);
|
|
258
|
+
expect(lastIpcCall).toBeNull();
|
|
259
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
260
|
+
ok: false,
|
|
261
|
+
error: expect.stringContaining("Search term must be a non-empty string"),
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("rejects a non-numeric --limit before IPC", async () => {
|
|
266
|
+
const { code, stderr } = await runSearch([
|
|
267
|
+
"search",
|
|
268
|
+
"flux",
|
|
269
|
+
"--limit",
|
|
270
|
+
"all",
|
|
271
|
+
]);
|
|
272
|
+
expect(code).toBe(1);
|
|
273
|
+
expect(lastIpcCall).toBeNull();
|
|
274
|
+
expect(stderr).toContain("Invalid --limit value");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("search --help documents the term, --limit, and examples", async () => {
|
|
278
|
+
const { code, stdout } = await runSearch(["search", "--help"]);
|
|
279
|
+
expect(code).toBe(0);
|
|
280
|
+
expect(stdout).toContain(
|
|
281
|
+
"Search conversations by title and message content",
|
|
282
|
+
);
|
|
283
|
+
expect(stdout).toContain("--limit");
|
|
284
|
+
expect(stdout).toContain(
|
|
285
|
+
'assistant conversations search "project planning"',
|
|
286
|
+
);
|
|
287
|
+
expect(stdout).toContain("--json");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -98,6 +98,7 @@ mock.module("../../../ipc/gateway-client.js", () => ({
|
|
|
98
98
|
}));
|
|
99
99
|
|
|
100
100
|
mock.module("../../../messaging/providers/slack/send.js", () => ({
|
|
101
|
+
updateSlackMessage: async () => ({ ok: true, ts: "ts" }),
|
|
101
102
|
sendSlackReply: async (
|
|
102
103
|
chatId: string,
|
|
103
104
|
text: string,
|