@vellumai/assistant 0.11.8 → 0.11.9-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/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +12 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +15 -1
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +12 -7
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +31 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
package/src/approvals/AGENTS.md
CHANGED
|
@@ -7,7 +7,7 @@ The end-to-end lifecycle of interactive guardian requests (approvals, questions)
|
|
|
7
7
|
- **Rich delivery failures must degrade gracefully.** If delivering a rich approval prompt (e.g., Telegram inline buttons) fails, fall back to plain text with instructions (e.g., `Reply "yes" to approve`) — never auto-deny.
|
|
8
8
|
- **Non-rich channels** (http-api) receive plain-text approval prompts. The conversational approval engine handles free-text responses.
|
|
9
9
|
- **Race conditions:** Always check whether a decision has already been resolved before delivering the engine's optimistic reply. If `handleChannelDecision` returns `applied: false`, deliver an "already resolved" notice and return `stale_ignored`.
|
|
10
|
-
- **Unified guardian decision primitive:** All guardian decision paths (callback buttons, conversational engine, channel
|
|
10
|
+
- **Unified guardian decision primitive:** All guardian decision paths (callback buttons, conversational engine, channel text) must route through `applyGuardianDecision()` in `assistant/src/approvals/guardian-decision-primitive.ts`. Do not inline decision logic (CAS resolution, resolver dispatch, grant minting) at individual callsites.
|
|
11
11
|
|
|
12
12
|
## Single-Guardian Invariant
|
|
13
13
|
|
|
@@ -327,21 +327,6 @@ describe("request lookups", () => {
|
|
|
327
327
|
});
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
test("getPendingRequestByDestinationMessageOrNull maps the destination triple", async () => {
|
|
331
|
-
ipcResponse = null;
|
|
332
|
-
expect(
|
|
333
|
-
await client.getPendingRequestByDestinationMessageOrNull(
|
|
334
|
-
"telegram",
|
|
335
|
-
"chat-456",
|
|
336
|
-
"msg-1",
|
|
337
|
-
),
|
|
338
|
-
).toBeNull();
|
|
339
|
-
expect(ipcCalls[0]).toEqual({
|
|
340
|
-
method: "guardian_requests_get_by_destination_message",
|
|
341
|
-
params: { channel: "telegram", chatId: "chat-456", messageId: "msg-1" },
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
|
|
345
330
|
test("getPendingRequestByCallSession and getRequestByPendingQuestionOrNull map params", async () => {
|
|
346
331
|
const request = makeWireRequest({ callSessionId: "call-1" });
|
|
347
332
|
ipcResponse = request;
|
|
@@ -378,18 +363,11 @@ describe("request lookups", () => {
|
|
|
378
363
|
|
|
379
364
|
ipcError = new Error("gateway unavailable");
|
|
380
365
|
expect(await client.getGuardianRequestByCodeOrNull("AB12")).toBeNull();
|
|
381
|
-
expect(
|
|
382
|
-
await client.getPendingRequestByDestinationMessageOrNull(
|
|
383
|
-
"telegram",
|
|
384
|
-
"chat-456",
|
|
385
|
-
"msg-1",
|
|
386
|
-
),
|
|
387
|
-
).toBeNull();
|
|
388
366
|
expect(
|
|
389
367
|
await client.getPendingRequestByCallSessionOrNull("call-1"),
|
|
390
368
|
).toBeNull();
|
|
391
369
|
expect(await client.getRequestByPendingQuestionOrNull("pq-1")).toBeNull();
|
|
392
|
-
expect(warnCalls).toHaveLength(
|
|
370
|
+
expect(warnCalls).toHaveLength(4);
|
|
393
371
|
});
|
|
394
372
|
|
|
395
373
|
test("OrNull variants pass successful reads through untouched", async () => {
|
|
@@ -21,6 +21,7 @@ describe("INTERFACE_IDS", () => {
|
|
|
21
21
|
for (const id of [
|
|
22
22
|
"macos",
|
|
23
23
|
"windows",
|
|
24
|
+
"linux",
|
|
24
25
|
"ios",
|
|
25
26
|
"cli",
|
|
26
27
|
"telegram",
|
|
@@ -45,6 +46,7 @@ describe("INTERACTIVE_INTERFACES", () => {
|
|
|
45
46
|
test("includes desktop clients", () => {
|
|
46
47
|
expect(INTERACTIVE_INTERFACES.has("macos")).toBe(true);
|
|
47
48
|
expect(INTERACTIVE_INTERFACES.has("windows")).toBe(true);
|
|
49
|
+
expect(INTERACTIVE_INTERFACES.has("linux")).toBe(true);
|
|
48
50
|
});
|
|
49
51
|
});
|
|
50
52
|
|
|
@@ -56,6 +58,7 @@ describe("isInterfaceId", () => {
|
|
|
56
58
|
test("returns true for desktop interfaces", () => {
|
|
57
59
|
expect(isInterfaceId("macos")).toBe(true);
|
|
58
60
|
expect(isInterfaceId("windows")).toBe(true);
|
|
61
|
+
expect(isInterfaceId("linux")).toBe(true);
|
|
59
62
|
});
|
|
60
63
|
|
|
61
64
|
test("returns false for unknown interface", () => {
|
|
@@ -72,6 +75,7 @@ describe("parseInterfaceId", () => {
|
|
|
72
75
|
expect(parseInterfaceId("web")).toBe("web");
|
|
73
76
|
expect(parseInterfaceId("macos")).toBe("macos");
|
|
74
77
|
expect(parseInterfaceId("windows")).toBe("windows");
|
|
78
|
+
expect(parseInterfaceId("linux")).toBe("linux");
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
test("normalizes legacy 'vellum' alias to 'web'", () => {
|
|
@@ -117,6 +121,16 @@ describe("supportsHostProxy", () => {
|
|
|
117
121
|
expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
|
|
118
122
|
});
|
|
119
123
|
|
|
124
|
+
test("linux exposes the same host capabilities as windows", () => {
|
|
125
|
+
expect(supportsHostProxy("linux")).toBe(true);
|
|
126
|
+
expect(supportsHostProxy("linux", "host_bash")).toBe(true);
|
|
127
|
+
expect(supportsHostProxy("linux", "host_file")).toBe(true);
|
|
128
|
+
expect(supportsHostProxy("linux", "host_cu")).toBe(true);
|
|
129
|
+
expect(supportsHostProxy("linux", "host_browser")).toBe(true);
|
|
130
|
+
expect(supportsHostProxy("linux", "host_ui_snapshot")).toBe(true);
|
|
131
|
+
expect(supportsHostProxy("linux", "host_app_control")).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
120
134
|
// ── chrome-extension: only host_browser. ──
|
|
121
135
|
test("chrome-extension returns false (no capability)", () => {
|
|
122
136
|
// Chrome extension does not support "any host proxy at all" — it only
|
|
@@ -185,13 +199,20 @@ describe("parseClientOs", () => {
|
|
|
185
199
|
);
|
|
186
200
|
});
|
|
187
201
|
|
|
202
|
+
test("includes linux (Electron desktop client)", () => {
|
|
203
|
+
expect(parseClientOs("linux")).toBe("linux");
|
|
204
|
+
expect((CLIENT_OS_VALUES as readonly string[]).includes("linux")).toBe(
|
|
205
|
+
true,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
188
209
|
test("rejects transport-only interface ids and unknown values", () => {
|
|
189
210
|
// `clientOs` is an OS surface, not a transport interface — values like
|
|
190
211
|
// "cli"/"telegram" (real interfaces) are not valid OS surfaces.
|
|
191
212
|
expect(parseClientOs("cli")).toBeNull();
|
|
192
213
|
expect(parseClientOs("telegram")).toBeNull();
|
|
193
214
|
expect(parseClientOs("vellum")).toBeNull();
|
|
194
|
-
expect(parseClientOs("
|
|
215
|
+
expect(parseClientOs("plan9")).toBeNull();
|
|
195
216
|
expect(parseClientOs(undefined)).toBeNull();
|
|
196
217
|
expect(parseClientOs(42)).toBeNull();
|
|
197
218
|
});
|
|
@@ -301,28 +301,6 @@ export async function listGuardianRequestDeliveriesByChat(
|
|
|
301
301
|
);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
/**
|
|
305
|
-
* Reaction routing: the pending request whose delivered card is the
|
|
306
|
-
* reacted-to message. Throws on transport failure.
|
|
307
|
-
*/
|
|
308
|
-
async function getPendingRequestByDestinationMessage(
|
|
309
|
-
channel: string,
|
|
310
|
-
chatId: string,
|
|
311
|
-
messageId: string,
|
|
312
|
-
): Promise<GuardianRequestWire | null> {
|
|
313
|
-
return callGateway(
|
|
314
|
-
GUARDIAN_REQUESTS_IPC_METHODS.getByDestinationMessage,
|
|
315
|
-
{ channel, chatId, messageId },
|
|
316
|
-
GuardianRequestLookupIpcResponseSchema,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** `getPendingRequestByDestinationMessage` deny-path variant: degrades to null. */
|
|
321
|
-
export const getPendingRequestByDestinationMessageOrNull = degradeOnFailure(
|
|
322
|
-
getPendingRequestByDestinationMessage,
|
|
323
|
-
null,
|
|
324
|
-
);
|
|
325
|
-
|
|
326
304
|
/**
|
|
327
305
|
* Reply routing: pending requests delivered to a destination conversation
|
|
328
306
|
* (`conversationId`, optionally narrowed by `channel`) or chat
|
package/src/channels/types.ts
CHANGED
|
@@ -187,6 +187,7 @@ export const CHANNEL_METADATA: Partial<Record<ChannelId, ChannelInfo>> = {
|
|
|
187
187
|
export const INTERFACE_IDS = [
|
|
188
188
|
"macos",
|
|
189
189
|
"windows",
|
|
190
|
+
"linux",
|
|
190
191
|
"ios",
|
|
191
192
|
"cli",
|
|
192
193
|
"telegram",
|
|
@@ -263,6 +264,7 @@ export const CLIENT_OS_VALUES = [
|
|
|
263
264
|
"ios",
|
|
264
265
|
"macos",
|
|
265
266
|
"windows",
|
|
267
|
+
"linux",
|
|
266
268
|
"android",
|
|
267
269
|
] as const;
|
|
268
270
|
|
|
@@ -286,6 +288,7 @@ export function parseClientOs(value: unknown): ClientOs | null {
|
|
|
286
288
|
export const INTERACTIVE_INTERFACES: ReadonlySet<InterfaceId> = new Set([
|
|
287
289
|
"macos",
|
|
288
290
|
"windows",
|
|
291
|
+
"linux",
|
|
289
292
|
"ios",
|
|
290
293
|
"cli",
|
|
291
294
|
"web",
|
|
@@ -297,8 +300,8 @@ export function isInteractiveInterface(id: InterfaceId): boolean {
|
|
|
297
300
|
|
|
298
301
|
/**
|
|
299
302
|
* Host proxy capabilities that an interface can support. macOS supports all
|
|
300
|
-
* of them, Windows
|
|
301
|
-
* host_browser through the Chrome DevTools Protocol proxy.
|
|
303
|
+
* of them, Windows and Linux withhold app control, and chrome-extension
|
|
304
|
+
* supports only host_browser through the Chrome DevTools Protocol proxy.
|
|
302
305
|
*/
|
|
303
306
|
export const HOST_PROXY_CAPABILITIES = [
|
|
304
307
|
"host_bash",
|
|
@@ -317,10 +320,9 @@ export type HostProxyCapability = (typeof HOST_PROXY_CAPABILITIES)[number];
|
|
|
317
320
|
* `supportsHostProxy(id)` type predicate.
|
|
318
321
|
*
|
|
319
322
|
* Extend this literal type AND the `supportsHostProxy` implementation
|
|
320
|
-
* below in lock-step when adding a new host-capable client
|
|
321
|
-
* Linux.
|
|
323
|
+
* below in lock-step when adding a new host-capable client.
|
|
322
324
|
*/
|
|
323
|
-
export type HostProxyInterfaceId = "macos" | "windows";
|
|
325
|
+
export type HostProxyInterfaceId = "macos" | "windows" | "linux";
|
|
324
326
|
|
|
325
327
|
/**
|
|
326
328
|
* Whether the interface supports a host proxy capability.
|
|
@@ -352,7 +354,7 @@ export function supportsHostProxy(
|
|
|
352
354
|
if (id === "macos") {
|
|
353
355
|
return true;
|
|
354
356
|
}
|
|
355
|
-
if (id === "windows") {
|
|
357
|
+
if (id === "windows" || id === "linux") {
|
|
356
358
|
return capability == null || capability !== "host_app_control";
|
|
357
359
|
}
|
|
358
360
|
if (id === "chrome-extension" && capability === "host_browser") {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
10
10
|
|
|
11
11
|
import { buildCliCommandHelpContent } from "../../plugins/defaults/memory/substrate/cli-command-content.js";
|
|
12
|
+
import { channelsHelp } from "../commands/channels/index.help.js";
|
|
12
13
|
import { pluginsHelp } from "../commands/plugins.help.js";
|
|
13
14
|
import { skillsHelp } from "../commands/skills.help.js";
|
|
14
15
|
|
|
@@ -33,6 +34,8 @@ describe("catalog search help for setup-intent retrieval", () => {
|
|
|
33
34
|
expect(indexed.toLowerCase()).toContain("before searching the web");
|
|
34
35
|
expect(indexed).toContain("assistant plugins search");
|
|
35
36
|
expect(indexed).toContain("assistant skills search");
|
|
37
|
+
expect(indexed.toLowerCase()).toContain("channels");
|
|
38
|
+
expect(pluginsHelp.description).toContain("channels");
|
|
36
39
|
expect(indexed.toLowerCase()).not.toContain("empty query");
|
|
37
40
|
});
|
|
38
41
|
|
|
@@ -47,4 +50,13 @@ describe("catalog search help for setup-intent retrieval", () => {
|
|
|
47
50
|
expect(indexed).toContain("assistant skills search");
|
|
48
51
|
expect(indexed).toContain("try web search");
|
|
49
52
|
});
|
|
53
|
+
|
|
54
|
+
test("channels help points missing channels at plugin search", () => {
|
|
55
|
+
const indexed = buildCliCommandHelpContent(channelsHelp);
|
|
56
|
+
const list = channelsHelp.subcommands?.find((sub) => sub.name === "list");
|
|
57
|
+
|
|
58
|
+
expect(list?.helpText).toBeDefined();
|
|
59
|
+
expect(indexed).toContain("assistant plugins search <name>");
|
|
60
|
+
expect(indexed).toContain("not listed");
|
|
61
|
+
});
|
|
50
62
|
});
|
|
@@ -89,6 +89,32 @@ describe("assistant channels", () => {
|
|
|
89
89
|
queryParams: { includeRemote: "true" },
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
test("human list output points missing channels at plugin search", async () => {
|
|
94
|
+
mockResponses = [
|
|
95
|
+
{
|
|
96
|
+
ok: true,
|
|
97
|
+
result: { success: true, snapshots: [emptySnapshot("slack")] },
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
const out = await runCli("channels", "list");
|
|
101
|
+
expect(out).toContain("assistant plugins search <name>");
|
|
102
|
+
expect(out).toContain("not listed");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("json list output omits the plugin-search hint", async () => {
|
|
106
|
+
mockResponses = [
|
|
107
|
+
{
|
|
108
|
+
ok: true,
|
|
109
|
+
result: { success: true, snapshots: [emptySnapshot("slack")] },
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const out = await runCli("channels", "list", "--json");
|
|
113
|
+
expect(out).not.toContain("assistant plugins search");
|
|
114
|
+
expect(JSON.parse(out)).toEqual({
|
|
115
|
+
snapshots: [emptySnapshot("slack")],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
92
118
|
});
|
|
93
119
|
|
|
94
120
|
describe("two-axis rendering", () => {
|
|
@@ -14,6 +14,9 @@ export const KNOWN_CHANNELS = [
|
|
|
14
14
|
"a2a",
|
|
15
15
|
] as const;
|
|
16
16
|
|
|
17
|
+
export const CHANNELS_PLUGIN_SEARCH_HINT =
|
|
18
|
+
"If the channel you are looking for is not listed, search the plugin marketplace with 'assistant plugins search <name>'.";
|
|
19
|
+
|
|
17
20
|
export const channelsHelp: CliCommandHelp = {
|
|
18
21
|
name: "channels",
|
|
19
22
|
description:
|
|
@@ -23,6 +26,9 @@ Channels are the messaging surfaces the assistant talks over — slack,
|
|
|
23
26
|
telegram, whatsapp, email, phone, vellum, platform, a2a. Each channel
|
|
24
27
|
has a probe that reports whether it's configured and reachable.
|
|
25
28
|
|
|
29
|
+
${CHANNELS_PLUGIN_SEARCH_HINT} Plugins can bundle additional channels
|
|
30
|
+
from other Vellum users.
|
|
31
|
+
|
|
26
32
|
list Overview of every channel + ready state
|
|
27
33
|
get <channel> Live snapshot of one channel (always re-probes)
|
|
28
34
|
|
|
@@ -45,6 +51,14 @@ Examples:
|
|
|
45
51
|
defaultValue: false,
|
|
46
52
|
},
|
|
47
53
|
],
|
|
54
|
+
helpText: `
|
|
55
|
+
Shows readiness for every built-in channel.
|
|
56
|
+
|
|
57
|
+
${CHANNELS_PLUGIN_SEARCH_HINT}
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
$ assistant channels list
|
|
61
|
+
$ assistant channels list --json`,
|
|
48
62
|
},
|
|
49
63
|
{
|
|
50
64
|
name: "get",
|
|
@@ -53,7 +67,7 @@ Examples:
|
|
|
53
67
|
arguments: [
|
|
54
68
|
{
|
|
55
69
|
name: "<channel>",
|
|
56
|
-
description: `Channel id: ${KNOWN_CHANNELS.join(", ")}
|
|
70
|
+
description: `Channel id: ${KNOWN_CHANNELS.join(", ")}. If the channel is not in this list, run 'assistant plugins search <name>'.`,
|
|
57
71
|
},
|
|
58
72
|
],
|
|
59
73
|
options: [
|
|
@@ -20,7 +20,7 @@ import { applyCommandHelp, subcommand } from "../../lib/cli-command-help.js";
|
|
|
20
20
|
import { registerCommand } from "../../lib/register-command.js";
|
|
21
21
|
import { log } from "../../logger.js";
|
|
22
22
|
import { shouldOutputJson, writeOutput } from "../../output.js";
|
|
23
|
-
import { channelsHelp } from "./index.help.js";
|
|
23
|
+
import { CHANNELS_PLUGIN_SEARCH_HINT, channelsHelp } from "./index.help.js";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
// Snapshot shape
|
|
@@ -95,6 +95,8 @@ function renderList(snapshots: ChannelSnapshot[]): void {
|
|
|
95
95
|
for (const s of sorted) {
|
|
96
96
|
log.info(`${statusGlyph(s)} ${s.channel.padEnd(12)} ${statusState(s)}`);
|
|
97
97
|
}
|
|
98
|
+
log.info("");
|
|
99
|
+
log.info(CHANNELS_PLUGIN_SEARCH_HINT);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
function renderSnapshot(s: ChannelSnapshot): void {
|
|
@@ -185,7 +187,9 @@ export function registerChannelsCommand(program: Command): void {
|
|
|
185
187
|
(s) => s.channel === channel,
|
|
186
188
|
);
|
|
187
189
|
if (!snapshot) {
|
|
188
|
-
log.error(
|
|
190
|
+
log.error(
|
|
191
|
+
`No readiness probe registered for channel: ${channel}. If you are looking for a community channel, run 'assistant plugins search ${channel}'.`,
|
|
192
|
+
);
|
|
189
193
|
process.exitCode = 1;
|
|
190
194
|
return;
|
|
191
195
|
}
|
|
@@ -169,6 +169,8 @@ describe("assistant db status — happy path", () => {
|
|
|
169
169
|
// (memory_checkpoints has 3 rows.)
|
|
170
170
|
expect(stdout).toContain("Largest tables");
|
|
171
171
|
expect(stdout.indexOf("messages")).toBeLessThan(stdout.indexOf("small"));
|
|
172
|
+
expect(stdout).toContain("Schema contract");
|
|
173
|
+
expect(stdout).toContain("missing");
|
|
172
174
|
});
|
|
173
175
|
|
|
174
176
|
test("reports the latest 'completed' migration, ignoring 'started'", async () => {
|
|
@@ -212,6 +214,26 @@ describe("assistant db status — happy path", () => {
|
|
|
212
214
|
expect(parsed.db.largest.length).toBeGreaterThan(0);
|
|
213
215
|
// Either 'bytes' (if dbstat is available) or 'rows' fallback.
|
|
214
216
|
expect(["bytes", "rows"]).toContain(parsed.db.sizingMethod);
|
|
217
|
+
expect(Array.isArray(parsed.schemaContract.missingTables)).toBe(true);
|
|
218
|
+
expect(Array.isArray(parsed.schemaContract.missingColumns)).toBe(true);
|
|
219
|
+
// The seed is a 3-table stub, so the drizzle catalog reports misses.
|
|
220
|
+
expect(parsed.schemaContract.missingTables.length).toBeGreaterThan(0);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("reports conversations.fork_strategy when the table exists without that column", async () => {
|
|
224
|
+
seedDb();
|
|
225
|
+
const db = new Database(dbPath);
|
|
226
|
+
try {
|
|
227
|
+
db.exec(`CREATE TABLE conversations (id TEXT PRIMARY KEY, title TEXT)`);
|
|
228
|
+
} finally {
|
|
229
|
+
db.close();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { stdout, exitCode } = await runStatus(["status"]);
|
|
233
|
+
|
|
234
|
+
expect(exitCode).toBe(0);
|
|
235
|
+
expect(stdout).toContain("Schema contract");
|
|
236
|
+
expect(stdout).toContain("conversations.fork_strategy");
|
|
215
237
|
});
|
|
216
238
|
});
|
|
217
239
|
|
|
@@ -12,7 +12,7 @@ export const dbHelp: CliCommandHelp = {
|
|
|
12
12
|
{
|
|
13
13
|
name: "status",
|
|
14
14
|
description:
|
|
15
|
-
"Show database path, size, key pragmas, and
|
|
15
|
+
"Show database path, size, key pragmas, largest tables, and drizzle schema misses",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "repair",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* callers will see the row-count path today)
|
|
17
17
|
* - latest applied workspace migration (proxy for schema version, read
|
|
18
18
|
* from the `memory_checkpoints` rows the migration runner writes)
|
|
19
|
+
* - drizzle-vs-SQLite schema contract (columns the current binary
|
|
20
|
+
* selects that no live database file has)
|
|
19
21
|
* - file mtime + age, for "is this DB live?" triage at a glance
|
|
20
22
|
*
|
|
21
23
|
* If the DB file is missing, exits with code 1 after a loud error. If the file
|
|
@@ -28,6 +30,7 @@ import { Database } from "bun:sqlite";
|
|
|
28
30
|
|
|
29
31
|
import type { Command } from "commander";
|
|
30
32
|
|
|
33
|
+
import type { SchemaContractReport } from "../../../persistence/schema-contract.js";
|
|
31
34
|
import { getLogsDbPath } from "../../../util/logs-db-path.js";
|
|
32
35
|
import { getMemoryDbPath } from "../../../util/memory-db-path.js";
|
|
33
36
|
import { getDbPath } from "../../../util/platform.js";
|
|
@@ -110,6 +113,12 @@ interface StatusReport {
|
|
|
110
113
|
*/
|
|
111
114
|
telemetryFile?: FileFacts;
|
|
112
115
|
telemetryDb?: DbFacts | null;
|
|
116
|
+
/**
|
|
117
|
+
* Drizzle source catalog vs the union of columns in every live SQLite
|
|
118
|
+
* file (main, logs, memory, telemetry). Extra SQLite-only columns are
|
|
119
|
+
* ignored. Omitted when the main file could not be opened.
|
|
120
|
+
*/
|
|
121
|
+
schemaContract?: SchemaContractReport;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
interface MigrationSummary {
|
|
@@ -260,6 +269,83 @@ function readDbFacts(path: string): DbFacts {
|
|
|
260
269
|
}
|
|
261
270
|
}
|
|
262
271
|
|
|
272
|
+
/** Quote a SQLite identifier that came from sqlite_master (never user input). */
|
|
273
|
+
function quoteIdent(name: string): string {
|
|
274
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Read table → column names from one SQLite file. Skips `sqlite_*`
|
|
279
|
+
* internals and tables whose `PRAGMA table_info` fails.
|
|
280
|
+
*/
|
|
281
|
+
function readSqliteColumns(path: string): Map<string, Set<string>> {
|
|
282
|
+
const db = new Database(path, { readonly: true });
|
|
283
|
+
try {
|
|
284
|
+
const tables = db
|
|
285
|
+
.query<{ name: string }, []>(
|
|
286
|
+
`SELECT name FROM sqlite_master
|
|
287
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'`,
|
|
288
|
+
)
|
|
289
|
+
.all();
|
|
290
|
+
const out = new Map<string, Set<string>>();
|
|
291
|
+
for (const table of tables) {
|
|
292
|
+
try {
|
|
293
|
+
const columns = db
|
|
294
|
+
.query<{ name: string }, []>(
|
|
295
|
+
`PRAGMA table_info(${quoteIdent(table.name)})`,
|
|
296
|
+
)
|
|
297
|
+
.all();
|
|
298
|
+
out.set(table.name, new Set(columns.map((column) => column.name)));
|
|
299
|
+
} catch {
|
|
300
|
+
// Virtual or unreadable table. Skip so one failure does not hide
|
|
301
|
+
// the rest of the contract.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
} finally {
|
|
306
|
+
db.close();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Union column sets from several files. Same table name keeps every column. */
|
|
311
|
+
function mergeSqliteColumns(
|
|
312
|
+
files: Array<Map<string, Set<string>>>,
|
|
313
|
+
): Map<string, Set<string>> {
|
|
314
|
+
const merged = new Map<string, Set<string>>();
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
for (const [table, columns] of file) {
|
|
317
|
+
const existing = merged.get(table);
|
|
318
|
+
if (!existing) {
|
|
319
|
+
merged.set(table, new Set(columns));
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
for (const column of columns) {
|
|
323
|
+
existing.add(column);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return merged;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function readSchemaContract(
|
|
331
|
+
paths: string[],
|
|
332
|
+
): Promise<SchemaContractReport> {
|
|
333
|
+
// Lazy so the drizzle catalog stays out of the CLI's static graph
|
|
334
|
+
// (cli/no-daemon-internals).
|
|
335
|
+
const { diffDrizzleSchemaContract } =
|
|
336
|
+
await import("../../../persistence/schema-contract.js");
|
|
337
|
+
const live: Array<Map<string, Set<string>>> = [];
|
|
338
|
+
for (const path of paths) {
|
|
339
|
+
try {
|
|
340
|
+
live.push(readSqliteColumns(path));
|
|
341
|
+
} catch {
|
|
342
|
+
// File vanished or is unreadable. The main-file open already
|
|
343
|
+
// succeeded; skip this sidecar rather than fail the report.
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return diffDrizzleSchemaContract(mergeSqliteColumns(live));
|
|
347
|
+
}
|
|
348
|
+
|
|
263
349
|
// ---------------------------------------------------------------------------
|
|
264
350
|
// Rendering
|
|
265
351
|
// ---------------------------------------------------------------------------
|
|
@@ -350,6 +436,7 @@ function renderHuman(report: StatusReport): string {
|
|
|
350
436
|
// Cross-database migration summary — the checkpoint ledger lives only in
|
|
351
437
|
// the main DB, so this is a single block at the end covering all three.
|
|
352
438
|
out += renderSummary(report.migration);
|
|
439
|
+
out += renderSchemaContract(report.schemaContract);
|
|
353
440
|
|
|
354
441
|
return out;
|
|
355
442
|
}
|
|
@@ -469,6 +556,72 @@ function renderSummary(migration: MigrationSummary | undefined): string {
|
|
|
469
556
|
return out;
|
|
470
557
|
}
|
|
471
558
|
|
|
559
|
+
const HUMAN_COLUMN_MISS_LIMIT = 40;
|
|
560
|
+
const HUMAN_TABLE_MISS_LIMIT = 15;
|
|
561
|
+
|
|
562
|
+
function schemaContractIsClean(report: SchemaContractReport): boolean {
|
|
563
|
+
return (
|
|
564
|
+
report.missingTables.length === 0 && report.missingColumns.length === 0
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function renderMissList(labels: string[], limit: number): string {
|
|
569
|
+
let out = "";
|
|
570
|
+
const shown = labels.slice(0, limit);
|
|
571
|
+
for (const label of shown) {
|
|
572
|
+
out += ` ${label}\n`;
|
|
573
|
+
}
|
|
574
|
+
if (labels.length > limit) {
|
|
575
|
+
out += ` … and ${formatCount(labels.length - limit)} more\n`;
|
|
576
|
+
}
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function renderSchemaContract(
|
|
581
|
+
contract: SchemaContractReport | undefined,
|
|
582
|
+
): string {
|
|
583
|
+
if (!contract) {
|
|
584
|
+
return "";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let out = "\nSchema contract (drizzle vs SQLite)\n";
|
|
588
|
+
if (schemaContractIsClean(contract)) {
|
|
589
|
+
out += row("Status", "ok");
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const parts: string[] = [];
|
|
594
|
+
if (contract.missingColumns.length > 0) {
|
|
595
|
+
parts.push(
|
|
596
|
+
`${formatCount(contract.missingColumns.length)} missing column${
|
|
597
|
+
contract.missingColumns.length === 1 ? "" : "s"
|
|
598
|
+
}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
if (contract.missingTables.length > 0) {
|
|
602
|
+
parts.push(
|
|
603
|
+
`${formatCount(contract.missingTables.length)} missing table${
|
|
604
|
+
contract.missingTables.length === 1 ? "" : "s"
|
|
605
|
+
}`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
out += row("Status", red(parts.join(", ")));
|
|
609
|
+
|
|
610
|
+
if (contract.missingColumns.length > 0) {
|
|
611
|
+
out += "Missing columns\n";
|
|
612
|
+
out += renderMissList(
|
|
613
|
+
contract.missingColumns.map((miss) => `${miss.table}.${miss.column}`),
|
|
614
|
+
HUMAN_COLUMN_MISS_LIMIT,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (contract.missingTables.length > 0) {
|
|
618
|
+
out += "Missing tables\n";
|
|
619
|
+
out += renderMissList(contract.missingTables, HUMAN_TABLE_MISS_LIMIT);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
|
|
472
625
|
function renderMissing(path: string): string {
|
|
473
626
|
return (
|
|
474
627
|
`${red("ERROR")} Database not found at ${path}\n\n` +
|
|
@@ -487,7 +640,7 @@ function renderMissing(path: string): string {
|
|
|
487
640
|
// ---------------------------------------------------------------------------
|
|
488
641
|
|
|
489
642
|
export function registerDbStatus(parent: Command): void {
|
|
490
|
-
subcommand(parent, "status").action(function (this: Command) {
|
|
643
|
+
subcommand(parent, "status").action(async function (this: Command) {
|
|
491
644
|
const file = readFileFacts(getDbPath());
|
|
492
645
|
|
|
493
646
|
if (!file.exists) {
|
|
@@ -561,10 +714,28 @@ export function registerDbStatus(parent: Command): void {
|
|
|
561
714
|
migration = null;
|
|
562
715
|
}
|
|
563
716
|
|
|
717
|
+
const schemaPaths = [file.path];
|
|
718
|
+
if (logsFile.exists) {
|
|
719
|
+
schemaPaths.push(logsFile.path);
|
|
720
|
+
}
|
|
721
|
+
if (memoryFile.exists) {
|
|
722
|
+
schemaPaths.push(memoryFile.path);
|
|
723
|
+
}
|
|
724
|
+
if (telemetryFile.exists) {
|
|
725
|
+
schemaPaths.push(telemetryFile.path);
|
|
726
|
+
}
|
|
727
|
+
let schemaContract: SchemaContractReport | undefined;
|
|
728
|
+
try {
|
|
729
|
+
schemaContract = await readSchemaContract(schemaPaths);
|
|
730
|
+
} catch {
|
|
731
|
+
schemaContract = undefined;
|
|
732
|
+
}
|
|
733
|
+
|
|
564
734
|
const report: StatusReport = {
|
|
565
735
|
file,
|
|
566
736
|
db,
|
|
567
737
|
migration: migration ?? undefined,
|
|
738
|
+
schemaContract,
|
|
568
739
|
logsFile,
|
|
569
740
|
logsDb,
|
|
570
741
|
memoryFile,
|
|
@@ -69,6 +69,35 @@ describe("assistant platform connect", () => {
|
|
|
69
69
|
expect(parsed.showPlatformLogin).toBe(true);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
// A stored key the platform rejects is a recovery, not a first setup: the
|
|
73
|
+
// outcome has to reach the caller in the response, since the daemon's login
|
|
74
|
+
// signal has no shipped consumer.
|
|
75
|
+
test("a rejected stored credential is reported as such", async () => {
|
|
76
|
+
mockResponse = {
|
|
77
|
+
ok: true,
|
|
78
|
+
result: { showPlatformLogin: true, credentialRejected: true },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const program = buildProgram();
|
|
82
|
+
const stdoutChunks: string[] = [];
|
|
83
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
84
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
85
|
+
stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
86
|
+
return true;
|
|
87
|
+
}) as typeof process.stdout.write;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await program.parseAsync(["node", "assistant", "connect", "--json"]);
|
|
91
|
+
} finally {
|
|
92
|
+
process.stdout.write = origWrite;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parsed = JSON.parse(stdoutChunks.join(""));
|
|
96
|
+
expect(parsed.ok).toBe(true);
|
|
97
|
+
expect(parsed.credentialRejected).toBe(true);
|
|
98
|
+
expect(parsed.alreadyConnected).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
72
101
|
test("already connected returns success with existing base URL", async () => {
|
|
73
102
|
mockResponse = {
|
|
74
103
|
ok: true,
|