@vellumai/assistant 0.12.0 → 0.12.1-staging.1
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 +5 -1
- package/ARCHITECTURE.md +11 -12
- package/docs/architecture/turn-actor.md +15 -8
- package/openapi.yaml +292 -2
- package/package.json +1 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +17 -5
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +212 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/cu-unified-flow.test.ts +8 -2
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +94 -9
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/index.ts +6 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +53 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +59 -46
- package/src/calls/voice-triage-escalate.ts +175 -56
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +117 -0
- package/src/config/__tests__/platform-rehydration.test.ts +93 -0
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +8 -12
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +144 -0
- package/src/config/platform-rehydration.ts +75 -21
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +10 -8
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +119 -0
- package/src/desktop/desktop-chrome-policy.test.ts +71 -0
- package/src/desktop/desktop-chrome-policy.ts +42 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +306 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +627 -0
- package/src/desktop/desktop-session-manager.ts +814 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/live-voice-session.ts +213 -693
- package/src/live-voice/protocol.ts +0 -2
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +2 -0
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +6 -1
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +192 -78
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/runtime/AGENTS.md +2 -0
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +73 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/credential-routes.ts +19 -10
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +55 -3
- package/src/runtime/routes/index.ts +4 -0
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +8 -21
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +4 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
- package/src/tools/terminal/safe-env.ts +23 -3
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +5 -54
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
package/src/cli/commands/apps.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
cliIpcCall,
|
|
5
|
+
exitCodeFromIpcResult,
|
|
6
|
+
} from "../../ipc/cli-client.js";
|
|
3
7
|
import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
|
|
4
8
|
import { registerCommand } from "../lib/register-command.js";
|
|
5
9
|
import { log } from "../logger.js";
|
|
10
|
+
import { shouldOutputJson, writeError, writeOutput } from "../output.js";
|
|
6
11
|
import { appsHelp } from "./apps.help.js";
|
|
7
12
|
|
|
8
13
|
interface AppListEntry {
|
|
@@ -10,6 +15,36 @@ interface AppListEntry {
|
|
|
10
15
|
source: string;
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
interface AppsRefreshResponse {
|
|
19
|
+
ok: true;
|
|
20
|
+
appId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
compiled: boolean;
|
|
23
|
+
compile_duration_ms: number;
|
|
24
|
+
compile_errors?: Array<{
|
|
25
|
+
text: string;
|
|
26
|
+
location?: { file: string; line: number; column: number };
|
|
27
|
+
}>;
|
|
28
|
+
compile_warnings?: Array<{
|
|
29
|
+
text: string;
|
|
30
|
+
location?: { file: string; line: number; column: number };
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function refreshHint(appId: string): string {
|
|
35
|
+
return `Run 'assistant apps refresh ${appId}' to compile.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatDiagnostic(diag: {
|
|
39
|
+
text: string;
|
|
40
|
+
location?: { file: string; line: number; column: number };
|
|
41
|
+
}): string {
|
|
42
|
+
if (!diag.location) {
|
|
43
|
+
return diag.text;
|
|
44
|
+
}
|
|
45
|
+
return `${diag.location.file}:${diag.location.line}: ${diag.text}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
13
48
|
export function registerAppsCommand(program: Command): void {
|
|
14
49
|
registerCommand(program, {
|
|
15
50
|
name: appsHelp.name,
|
|
@@ -46,6 +81,154 @@ export function registerAppsCommand(program: Command): void {
|
|
|
46
81
|
log.info(` ${e.name.padEnd(nameWidth)} ${e.source}`);
|
|
47
82
|
}
|
|
48
83
|
});
|
|
84
|
+
|
|
85
|
+
subcommand(apps, "inspect").action(
|
|
86
|
+
async (appName: string, opts: { json?: boolean }, cmd: Command) => {
|
|
87
|
+
const { resolveAppQuery, appNotFoundMessage, appAmbiguousMessage } =
|
|
88
|
+
await import("../../apps/resolve-app.js");
|
|
89
|
+
const { inspectAppSource } = await import(
|
|
90
|
+
"../../apps/source-fingerprint.js"
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const resolved = resolveAppQuery(appName);
|
|
94
|
+
if (!resolved.ok) {
|
|
95
|
+
const message =
|
|
96
|
+
resolved.reason === "ambiguous"
|
|
97
|
+
? appAmbiguousMessage(resolved.query, resolved.matches)
|
|
98
|
+
: appNotFoundMessage(resolved.query);
|
|
99
|
+
writeError(cmd, message);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { app } = resolved;
|
|
105
|
+
const inspect = inspectAppSource(app.sourcePath);
|
|
106
|
+
const origin =
|
|
107
|
+
app.origin.kind === "plugin"
|
|
108
|
+
? { kind: "plugin" as const, plugin: app.origin.pluginName }
|
|
109
|
+
: { kind: "workspace" as const };
|
|
110
|
+
|
|
111
|
+
if (opts.json || shouldOutputJson(cmd)) {
|
|
112
|
+
writeOutput(cmd, {
|
|
113
|
+
ok: true,
|
|
114
|
+
app: {
|
|
115
|
+
id: app.id,
|
|
116
|
+
name: app.name,
|
|
117
|
+
source: app.sourcePath,
|
|
118
|
+
origin,
|
|
119
|
+
},
|
|
120
|
+
compile: inspect,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const originLabel =
|
|
126
|
+
origin.kind === "plugin"
|
|
127
|
+
? `plugin ${origin.plugin}`
|
|
128
|
+
: "workspace";
|
|
129
|
+
log.info(`App: ${app.name} (${originLabel})`);
|
|
130
|
+
log.info(`Id: ${app.id}`);
|
|
131
|
+
log.info(`Source: ${app.sourcePath}`);
|
|
132
|
+
|
|
133
|
+
if (inspect.status === "clean") {
|
|
134
|
+
const when = inspect.compiledAt
|
|
135
|
+
? ` (compiled ${inspect.compiledAt})`
|
|
136
|
+
: "";
|
|
137
|
+
log.info(`Compile: up to date${when}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (inspect.status === "never_compiled") {
|
|
141
|
+
log.info("Compile: never compiled");
|
|
142
|
+
log.info(refreshHint(app.id));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (inspect.status === "unknown_baseline") {
|
|
146
|
+
log.info(
|
|
147
|
+
"Compile: compiled, but no source fingerprint is recorded.",
|
|
148
|
+
);
|
|
149
|
+
log.info(
|
|
150
|
+
`Run 'assistant apps refresh ${app.id}' to record one and rebuild.`,
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const when = inspect.compiledAt
|
|
156
|
+
? ` (last compiled ${inspect.compiledAt})`
|
|
157
|
+
: "";
|
|
158
|
+
log.info(`Compile: source changed since last compile${when}`);
|
|
159
|
+
if (inspect.modified.length > 0) {
|
|
160
|
+
log.info("Modified:");
|
|
161
|
+
for (const path of inspect.modified) {
|
|
162
|
+
log.info(` ${path}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (inspect.added.length > 0) {
|
|
166
|
+
log.info("Added:");
|
|
167
|
+
for (const path of inspect.added) {
|
|
168
|
+
log.info(` ${path}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (inspect.removed.length > 0) {
|
|
172
|
+
log.info("Removed:");
|
|
173
|
+
for (const path of inspect.removed) {
|
|
174
|
+
log.info(` ${path}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
log.info(refreshHint(app.id));
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
subcommand(apps, "refresh").action(
|
|
182
|
+
async (appName: string, opts: { json?: boolean }, cmd: Command) => {
|
|
183
|
+
const { resolveAppQuery, appNotFoundMessage, appAmbiguousMessage } =
|
|
184
|
+
await import("../../apps/resolve-app.js");
|
|
185
|
+
|
|
186
|
+
const resolved = resolveAppQuery(appName);
|
|
187
|
+
if (!resolved.ok) {
|
|
188
|
+
const message =
|
|
189
|
+
resolved.reason === "ambiguous"
|
|
190
|
+
? appAmbiguousMessage(resolved.query, resolved.matches)
|
|
191
|
+
: appNotFoundMessage(resolved.query);
|
|
192
|
+
writeError(cmd, message);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = await cliIpcCall<AppsRefreshResponse>("apps_refresh", {
|
|
198
|
+
pathParams: { id: resolved.app.id },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!result.ok || !result.result) {
|
|
202
|
+
writeError(cmd, result.error ?? "Refresh failed");
|
|
203
|
+
process.exitCode = exitCodeFromIpcResult(result);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const payload = result.result;
|
|
208
|
+
if (opts.json || shouldOutputJson(cmd)) {
|
|
209
|
+
writeOutput(cmd, payload);
|
|
210
|
+
if (!payload.compiled) {
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!payload.compiled) {
|
|
217
|
+
log.error(
|
|
218
|
+
`Compile failed for "${payload.name}" (${payload.compile_duration_ms}ms).`,
|
|
219
|
+
);
|
|
220
|
+
for (const diag of payload.compile_errors ?? []) {
|
|
221
|
+
log.error(` ${formatDiagnostic(diag)}`);
|
|
222
|
+
}
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
log.info(
|
|
228
|
+
`Compiled "${payload.name}" in ${payload.compile_duration_ms}ms.`,
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
49
232
|
},
|
|
50
233
|
});
|
|
51
234
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// The command is a door onto the daemon's one send implementation, so what
|
|
4
|
+
// matters is the request it hands the route and what it does with the
|
|
5
|
+
// answer. IPC is stubbed to capture that request; the exit-code helper
|
|
6
|
+
// stays real, so the tests assert the mapping that ships.
|
|
7
|
+
|
|
8
|
+
const ipcCalls: Array<{
|
|
9
|
+
method: string;
|
|
10
|
+
params: Record<string, unknown>;
|
|
11
|
+
opts?: Record<string, unknown>;
|
|
12
|
+
}> = [];
|
|
13
|
+
let ipcResult: Record<string, unknown> = {
|
|
14
|
+
ok: true,
|
|
15
|
+
result: {
|
|
16
|
+
channel: "slack",
|
|
17
|
+
chatId: "C0123456789",
|
|
18
|
+
messageIds: ["1700000000.000100"],
|
|
19
|
+
lastMessageId: "1700000000.000100",
|
|
20
|
+
recordedIn: "conv-home",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const actualCliClient = await import("../../../../ipc/cli-client.js");
|
|
25
|
+
mock.module("../../../../ipc/cli-client.js", () => ({
|
|
26
|
+
...actualCliClient,
|
|
27
|
+
cliIpcCall: async (
|
|
28
|
+
method: string,
|
|
29
|
+
params: Record<string, unknown>,
|
|
30
|
+
opts?: Record<string, unknown>,
|
|
31
|
+
) => {
|
|
32
|
+
ipcCalls.push({ method, params, ...(opts ? { opts } : {}) });
|
|
33
|
+
return ipcResult;
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Imported after the mock is installed, so the command module under test
|
|
38
|
+
// binds the stubbed IPC call rather than the real one.
|
|
39
|
+
const { runCliCommand } = await import("../../__tests__/cli-test-harness.js");
|
|
40
|
+
const { registerChannelsCommand } = await import("../index.js");
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
ipcCalls.length = 0;
|
|
44
|
+
ipcResult = {
|
|
45
|
+
ok: true,
|
|
46
|
+
result: {
|
|
47
|
+
channel: "slack",
|
|
48
|
+
chatId: "C0123456789",
|
|
49
|
+
messageIds: ["1700000000.000100"],
|
|
50
|
+
lastMessageId: "1700000000.000100",
|
|
51
|
+
recordedIn: "conv-home",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// The shipped registration as-is: `send` declares its own `--json`, which is
|
|
57
|
+
// why the program's global leaf-option pass skips it.
|
|
58
|
+
function runSend(args: string[]) {
|
|
59
|
+
return runCliCommand(registerChannelsCommand, ["channels", "send", ...args]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The body the command handed the route on its one call. */
|
|
63
|
+
function sentBody(): Record<string, unknown> {
|
|
64
|
+
expect(ipcCalls).toHaveLength(1);
|
|
65
|
+
expect(ipcCalls[0]!.method).toBe("channels_send_post");
|
|
66
|
+
return ipcCalls[0]!.params.body as Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("the request the command makes", () => {
|
|
70
|
+
test("names the chat and asks for the channel's rich rendering", async () => {
|
|
71
|
+
const r = await runSend([
|
|
72
|
+
"slack",
|
|
73
|
+
"C0123456789",
|
|
74
|
+
"--text",
|
|
75
|
+
"deploy is green",
|
|
76
|
+
]);
|
|
77
|
+
expect(r.exitCode).toBe(0);
|
|
78
|
+
expect(sentBody()).toEqual({
|
|
79
|
+
channel: "slack",
|
|
80
|
+
target: { kind: "chat", chatId: "C0123456789" },
|
|
81
|
+
text: "deploy is green",
|
|
82
|
+
renderRichly: true,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("carries a named thread as the target's thread", async () => {
|
|
87
|
+
await runSend([
|
|
88
|
+
"slack",
|
|
89
|
+
"C0123456789",
|
|
90
|
+
"--thread",
|
|
91
|
+
"1700000000.000001",
|
|
92
|
+
"--text",
|
|
93
|
+
"and the smoke tests passed",
|
|
94
|
+
]);
|
|
95
|
+
expect(sentBody().target).toEqual({
|
|
96
|
+
kind: "chat",
|
|
97
|
+
chatId: "C0123456789",
|
|
98
|
+
threadId: "1700000000.000001",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("--plain asks for the text verbatim", async () => {
|
|
103
|
+
await runSend(["telegram", "123456789", "--text", "morning", "--plain"]);
|
|
104
|
+
const body = sentBody();
|
|
105
|
+
expect(body.renderRichly).toBeUndefined();
|
|
106
|
+
expect(body.channel).toBe("telegram");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("sends the text as the caller wrote it, whitespace included", async () => {
|
|
110
|
+
await runSend([
|
|
111
|
+
"slack",
|
|
112
|
+
"C0123456789",
|
|
113
|
+
"--text",
|
|
114
|
+
" indented ",
|
|
115
|
+
"--plain",
|
|
116
|
+
]);
|
|
117
|
+
expect(sentBody().text).toBe(" indented ");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("waits longer than the IPC default, since the send itself retries", async () => {
|
|
121
|
+
await runSend(["slack", "C0123456789", "--text", "hi"]);
|
|
122
|
+
expect(ipcCalls[0]!.opts).toMatchObject({ timeoutMs: 120_000 });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("refuses an empty text before it sends anything", async () => {
|
|
126
|
+
for (const args of [
|
|
127
|
+
["slack", "C0123456789"],
|
|
128
|
+
["slack", "C0123456789", "--text", " "],
|
|
129
|
+
]) {
|
|
130
|
+
ipcCalls.length = 0;
|
|
131
|
+
const r = await runSend(args);
|
|
132
|
+
expect(r.exitCode).toBe(1);
|
|
133
|
+
expect(r.stderr).toContain("--text is required");
|
|
134
|
+
expect(ipcCalls).toEqual([]);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("what the command reports", () => {
|
|
140
|
+
test("names where the post landed and the conversation it was recorded in", async () => {
|
|
141
|
+
const r = await runSend(["slack", "C0123456789", "--text", "hello"]);
|
|
142
|
+
expect(r.stdout).toContain("C0123456789");
|
|
143
|
+
expect(r.stdout).toContain("1700000000.000100");
|
|
144
|
+
expect(r.stdout).toContain("conv-home");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("says so when the channel split the text", async () => {
|
|
148
|
+
ipcResult = {
|
|
149
|
+
ok: true,
|
|
150
|
+
result: {
|
|
151
|
+
channel: "telegram",
|
|
152
|
+
chatId: "123456789",
|
|
153
|
+
messageIds: ["10", "11", "12"],
|
|
154
|
+
lastMessageId: "12",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const r = await runSend(["telegram", "123456789", "--text", "a long one"]);
|
|
158
|
+
expect(r.stdout).toContain("3 posts");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("--json answers with the route's own result", async () => {
|
|
162
|
+
const r = await runSend([
|
|
163
|
+
"slack",
|
|
164
|
+
"C0123456789",
|
|
165
|
+
"--text",
|
|
166
|
+
"hello",
|
|
167
|
+
"--json",
|
|
168
|
+
]);
|
|
169
|
+
expect(JSON.parse(r.stdout)).toEqual({
|
|
170
|
+
channel: "slack",
|
|
171
|
+
chatId: "C0123456789",
|
|
172
|
+
messageIds: ["1700000000.000100"],
|
|
173
|
+
lastMessageId: "1700000000.000100",
|
|
174
|
+
recordedIn: "conv-home",
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a refused send reports the route's reason and its exit code", async () => {
|
|
179
|
+
ipcResult = {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: 'Channel "email" is not addressable',
|
|
182
|
+
statusCode: 400,
|
|
183
|
+
};
|
|
184
|
+
const r = await runSend(["email", "someone", "--text", "hello"]);
|
|
185
|
+
expect(r.exitCode).toBe(
|
|
186
|
+
actualCliClient.exitCodeFromIpcResult({ statusCode: 400 }),
|
|
187
|
+
);
|
|
188
|
+
expect(r.stderr).toContain("not addressable");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a timeout is reported as an unknown outcome, not a failure to retry", async () => {
|
|
192
|
+
ipcResult = { ok: false, error: "Request timed out", timedOut: true };
|
|
193
|
+
const r = await runSend(["slack", "C0123456789", "--text", "hello"]);
|
|
194
|
+
expect(r.stderr).toContain("may still be in flight");
|
|
195
|
+
expect(r.stderr).toContain("before sending again");
|
|
196
|
+
expect(r.stderr).not.toContain("The send failed");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a refusal in --json mode is a JSON error envelope", async () => {
|
|
200
|
+
ipcResult = {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: 'Channel "slack" did not acknowledge the send',
|
|
203
|
+
statusCode: 502,
|
|
204
|
+
};
|
|
205
|
+
const r = await runSend([
|
|
206
|
+
"slack",
|
|
207
|
+
"C0123456789",
|
|
208
|
+
"--text",
|
|
209
|
+
"hello",
|
|
210
|
+
"--json",
|
|
211
|
+
]);
|
|
212
|
+
expect(JSON.parse(r.stdout)).toEqual({
|
|
213
|
+
ok: false,
|
|
214
|
+
error: 'Channel "slack" did not acknowledge the send',
|
|
215
|
+
});
|
|
216
|
+
expect(r.exitCode).toBe(3);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -36,9 +36,14 @@ assistant's own bot on that channel (${BOT_CHANNELS}), resolving the bot
|
|
|
36
36
|
credential from the channel id so no token is handled. Acting as a person
|
|
37
37
|
through their OAuth integration is 'assistant oauth request'.
|
|
38
38
|
|
|
39
|
+
To say something in a chat, use 'send' rather than 'request': it goes
|
|
40
|
+
through the channel's transport, so the post is threaded and rendered like
|
|
41
|
+
every other message the assistant sends there, and it is recorded.
|
|
42
|
+
|
|
39
43
|
Examples:
|
|
40
44
|
$ assistant channels list
|
|
41
45
|
$ assistant channels get slack
|
|
46
|
+
$ assistant channels send slack C0123456789 --text 'deploy is green'
|
|
42
47
|
$ assistant channels request slack /auth.test --json
|
|
43
48
|
$ assistant channels request slack -X POST \\
|
|
44
49
|
-d '{"channel":"D0123456789","limit":20}' /conversations.history --json`,
|
|
@@ -65,8 +70,9 @@ This command can do anything the bot's API allows, including sending,
|
|
|
65
70
|
editing, deleting, uploading, and reacting, so it is classified high risk
|
|
66
71
|
and asks for approval like any action with irreversible effects. Reads are
|
|
67
72
|
not distinguished from writes: the effect is the endpoint's. To send a
|
|
68
|
-
message, use
|
|
69
|
-
through this command
|
|
73
|
+
message, use 'assistant channels send', which posts through the channel's
|
|
74
|
+
transport and records what it sent; a message posted through this command
|
|
75
|
+
reaches the platform directly and leaves no record of what was said.
|
|
70
76
|
|
|
71
77
|
Arguments:
|
|
72
78
|
<channel> One of: ${BOT_CHANNELS}. A channel with no bot credential of
|
|
@@ -86,6 +92,68 @@ Examples:
|
|
|
86
92
|
$ assistant channels request slack -X POST \\
|
|
87
93
|
-d '{"channel":"D0123456789","limit":100}' /conversations.history --json
|
|
88
94
|
$ assistant channels request discord /users/@me --json`,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "send",
|
|
98
|
+
description:
|
|
99
|
+
"Post text to a chat on the channel, as the assistant's bot, recorded",
|
|
100
|
+
arguments: [
|
|
101
|
+
{
|
|
102
|
+
name: "<channel>",
|
|
103
|
+
description: `Channel to post on. One with a transport that can be addressed from a named chat; anything else is refused before the send.`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "<chat-id>",
|
|
107
|
+
description:
|
|
108
|
+
"The chat in the channel's own id space (a Slack channel or DM id, a Telegram chat id, a Discord channel id, a WhatsApp number).",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
options: [
|
|
112
|
+
{
|
|
113
|
+
flags: "--text <text>",
|
|
114
|
+
description: "The message text. Required.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
flags: "--thread <id>",
|
|
118
|
+
description:
|
|
119
|
+
"Post inside this thread or topic, in the channel's own id space. Omit to post to the chat itself.",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
flags: "--plain",
|
|
123
|
+
description:
|
|
124
|
+
"Send the text verbatim instead of the channel's rich rendering.",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
flags: "--json",
|
|
128
|
+
description: "Machine-readable compact JSON output",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
helpText: `
|
|
132
|
+
Posts one message through the channel's own transport, the same path a
|
|
133
|
+
reply takes. Once the channel acknowledges it, the daemon records it in
|
|
134
|
+
the chat's conversation, which is what lets the assistant see later what
|
|
135
|
+
it said here and lets a reaction or an edit naming that post resolve back
|
|
136
|
+
to it. Recording is best effort and never fails a send that already went
|
|
137
|
+
out, so the result names the conversation only when the record was
|
|
138
|
+
written.
|
|
139
|
+
|
|
140
|
+
This sends a message people will read, so it is classified high risk and
|
|
141
|
+
asks for approval like any other action with effects nobody can take back.
|
|
142
|
+
|
|
143
|
+
The send fails, and records nothing, when the channel does not acknowledge
|
|
144
|
+
it or names no message id for it. A channel the assistant cannot address
|
|
145
|
+
from a named chat is refused before anything is sent. If the daemon does
|
|
146
|
+
not answer in time, the message may still go out: that is reported as an
|
|
147
|
+
unknown outcome rather than a failure, so check the chat before sending
|
|
148
|
+
again.
|
|
149
|
+
|
|
150
|
+
Attachments are not sent from here.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
$ assistant channels send slack C0123456789 --text 'deploy is green'
|
|
154
|
+
$ assistant channels send slack C0123456789 --thread 1700000000.000100 \
|
|
155
|
+
--text 'and the smoke tests passed'
|
|
156
|
+
$ assistant channels send telegram 123456789 --text 'morning' --json`,
|
|
89
157
|
},
|
|
90
158
|
{
|
|
91
159
|
name: "list",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `assistant channels`: inspect messaging channels, and act as their bots.
|
|
3
3
|
*
|
|
4
|
-
* list
|
|
5
|
-
* get <channel>
|
|
6
|
-
* request <channel> <url>
|
|
4
|
+
* list : overview of every channel + ready state
|
|
5
|
+
* get <channel> : detailed live snapshot of a single channel
|
|
6
|
+
* request <channel> <url> : call the channel's platform API as its bot
|
|
7
|
+
* send <channel> <chat-id> : post text to a chat, recorded
|
|
7
8
|
*
|
|
8
9
|
* `get` always re-runs remote probes (it invalidates the readiness cache
|
|
9
10
|
* before reading), so the CLI answer matches the live source-of-truth.
|
|
@@ -12,6 +13,11 @@
|
|
|
12
13
|
* channel's bot credential and makes the same authenticated request
|
|
13
14
|
* `oauth request` makes, without the caller naming a provider.
|
|
14
15
|
*
|
|
16
|
+
* `send` is the send door (see ./send.ts): it runs the daemon's one send
|
|
17
|
+
* implementation for the channel, so the post is threaded, rendered, and
|
|
18
|
+
* recorded like every other message the assistant sends there. Posting
|
|
19
|
+
* through `request` instead reaches the platform API and leaves no record.
|
|
20
|
+
*
|
|
15
21
|
* A mutating `refresh` verb (for reconnecting channels, e.g. supplying
|
|
16
22
|
* fresh Slack tokens) is intentionally not part of this group.
|
|
17
23
|
*/
|
|
@@ -26,6 +32,7 @@ import { log } from "../../logger.js";
|
|
|
26
32
|
import { shouldOutputJson, writeOutput } from "../../output.js";
|
|
27
33
|
import { CHANNELS_PLUGIN_SEARCH_HINT, channelsHelp } from "./index.help.js";
|
|
28
34
|
import { registerChannelsRequestCommand } from "./request.js";
|
|
35
|
+
import { registerChannelsSendCommand } from "./send.js";
|
|
29
36
|
|
|
30
37
|
// ---------------------------------------------------------------------------
|
|
31
38
|
// Snapshot shape
|
|
@@ -211,6 +218,12 @@ export function registerChannelsCommand(program: Command): void {
|
|
|
211
218
|
// -----------------------------------------------------------------------
|
|
212
219
|
|
|
213
220
|
registerChannelsRequestCommand(channels);
|
|
221
|
+
|
|
222
|
+
// -----------------------------------------------------------------------
|
|
223
|
+
// send: post text to a chat, through the channel's transport
|
|
224
|
+
// -----------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
registerChannelsSendCommand(channels);
|
|
214
227
|
},
|
|
215
228
|
});
|
|
216
229
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `assistant channels send`: post text to a chat on a channel, as the
|
|
3
|
+
* assistant's own bot.
|
|
4
|
+
*
|
|
5
|
+
* The send door. It runs the daemon function the messaging tool runs
|
|
6
|
+
* (`sendChannelText`, over `POST /v1/channels/send`), so a send from the
|
|
7
|
+
* command line reaches the chat through the channel's transport and leaves
|
|
8
|
+
* the same record: threaded where a thread is named, rendered the way the
|
|
9
|
+
* channel renders a reply, and written to the chat's conversation only once
|
|
10
|
+
* the channel has acknowledged it.
|
|
11
|
+
*
|
|
12
|
+
* Posting the same text through `channels request` reaches the platform API
|
|
13
|
+
* directly and leaves no record of what was said, which is why this command
|
|
14
|
+
* exists beside it. Both act as the bot and both carry the same rating: the
|
|
15
|
+
* effect is a message either way.
|
|
16
|
+
*
|
|
17
|
+
* A channel whose transport cannot be addressed from a named chat is
|
|
18
|
+
* refused by the route before anything is sent.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Command } from "commander";
|
|
22
|
+
|
|
23
|
+
import { cliIpcCall, exitCodeFromIpcResult } from "../../../ipc/cli-client.js";
|
|
24
|
+
import { subcommand } from "../../lib/cli-command-help.js";
|
|
25
|
+
import { log } from "../../logger.js";
|
|
26
|
+
import { shouldOutputJson, writeError, writeOutput } from "../../output.js";
|
|
27
|
+
|
|
28
|
+
/** What the channel acknowledged, and where the post was recorded. */
|
|
29
|
+
interface ChannelSendResponse {
|
|
30
|
+
channel: string;
|
|
31
|
+
chatId: string;
|
|
32
|
+
threadId?: string;
|
|
33
|
+
messageIds: string[];
|
|
34
|
+
lastMessageId: string;
|
|
35
|
+
recordedIn?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ChannelSendOptions {
|
|
39
|
+
text?: string;
|
|
40
|
+
thread?: string;
|
|
41
|
+
plain?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How long to wait for the daemon before giving up on an answer.
|
|
46
|
+
*
|
|
47
|
+
* Longer than the IPC default because the work behind this call is a
|
|
48
|
+
* platform request that retries: a rate-limited Slack send sleeps for the
|
|
49
|
+
* `Retry-After` Slack names and tries again up to three times
|
|
50
|
+
* (`MAX_RATE_LIMIT_RETRIES`, providers/slack/web-api-transport.ts), and the
|
|
51
|
+
* other channels back off similarly. Waiting past that is better than
|
|
52
|
+
* abandoning a send that is still being made, because giving up tells the
|
|
53
|
+
* caller nothing about whether the message went out.
|
|
54
|
+
*/
|
|
55
|
+
const SEND_CALL_TIMEOUT_MS = 120_000;
|
|
56
|
+
|
|
57
|
+
export function registerChannelsSendCommand(channels: Command): void {
|
|
58
|
+
subcommand(channels, "send").action(
|
|
59
|
+
async (
|
|
60
|
+
channel: string,
|
|
61
|
+
chatId: string,
|
|
62
|
+
opts: ChannelSendOptions,
|
|
63
|
+
cmd: Command,
|
|
64
|
+
) => {
|
|
65
|
+
// Trimmed only to reject a blank message: the text goes out as the
|
|
66
|
+
// caller wrote it, which `--plain` in particular promises.
|
|
67
|
+
const text = opts.text;
|
|
68
|
+
if (!text?.trim()) {
|
|
69
|
+
writeError(cmd, "--text is required and cannot be empty.");
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const threadId = opts.thread?.trim();
|
|
75
|
+
const r = await cliIpcCall<ChannelSendResponse>(
|
|
76
|
+
"channels_send_post",
|
|
77
|
+
{
|
|
78
|
+
body: {
|
|
79
|
+
channel,
|
|
80
|
+
target: {
|
|
81
|
+
kind: "chat",
|
|
82
|
+
chatId,
|
|
83
|
+
...(threadId ? { threadId } : {}),
|
|
84
|
+
},
|
|
85
|
+
text,
|
|
86
|
+
// Rich unless asked otherwise, so a send from here reads like
|
|
87
|
+
// the assistant's other posts in that chat rather than like a
|
|
88
|
+
// different sender.
|
|
89
|
+
...(opts.plain ? {} : { renderRichly: true }),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{ timeoutMs: SEND_CALL_TIMEOUT_MS },
|
|
93
|
+
);
|
|
94
|
+
if (!r.ok) {
|
|
95
|
+
// A timeout is not a failure: closing the client socket does not
|
|
96
|
+
// stop the daemon, so the message may still go out. Saying so is
|
|
97
|
+
// what keeps a retry from posting it twice.
|
|
98
|
+
const message = r.timedOut
|
|
99
|
+
? `The daemon did not answer within ${Math.round(SEND_CALL_TIMEOUT_MS / 1000)}s. The send may still be in flight, so it may or may not have gone out. Check the chat before sending again.`
|
|
100
|
+
: (r.error ?? "The send failed.");
|
|
101
|
+
// Reported rather than exited, so `--json` gets an error envelope
|
|
102
|
+
// like every other answer this command gives.
|
|
103
|
+
writeError(cmd, message);
|
|
104
|
+
process.exitCode = exitCodeFromIpcResult({
|
|
105
|
+
statusCode: r.statusCode,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sent = r.result!;
|
|
111
|
+
if (shouldOutputJson(cmd)) {
|
|
112
|
+
writeOutput(cmd, sent);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const where = sent.threadId
|
|
117
|
+
? `${sent.chatId}, thread ${sent.threadId}`
|
|
118
|
+
: sent.chatId;
|
|
119
|
+
log.info(`Sent to ${channel} ${where} (id: ${sent.lastMessageId})`);
|
|
120
|
+
if (sent.messageIds.length > 1) {
|
|
121
|
+
log.info(`The channel split it into ${sent.messageIds.length} posts`);
|
|
122
|
+
}
|
|
123
|
+
if (sent.recordedIn) {
|
|
124
|
+
log.info(`Recorded in conversation ${sent.recordedIn}`);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
}
|