@vellumai/assistant 0.6.5 → 0.6.6
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 +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
__resetClientRegistryForTests,
|
|
5
|
+
ClientRegistry,
|
|
6
|
+
getClientRegistry,
|
|
7
|
+
} from "../client-registry.js";
|
|
8
|
+
|
|
9
|
+
describe("ClientRegistry", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
__resetClientRegistryForTests();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// ── register ──────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
test("register creates a new entry with derived capabilities", () => {
|
|
17
|
+
const registry = new ClientRegistry();
|
|
18
|
+
const entry = registry.register({
|
|
19
|
+
clientId: "mac-1",
|
|
20
|
+
interfaceId: "macos",
|
|
21
|
+
hostHomeDir: "/Users/alice",
|
|
22
|
+
hostUsername: "alice",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(entry.clientId).toBe("mac-1");
|
|
26
|
+
expect(entry.interfaceId).toBe("macos");
|
|
27
|
+
expect(entry.capabilities).toContain("host_bash");
|
|
28
|
+
expect(entry.capabilities).toContain("host_file");
|
|
29
|
+
expect(entry.capabilities).toContain("host_cu");
|
|
30
|
+
expect(entry.capabilities).toContain("host_browser");
|
|
31
|
+
expect(entry.hostHomeDir).toBe("/Users/alice");
|
|
32
|
+
expect(entry.hostUsername).toBe("alice");
|
|
33
|
+
expect(entry.connectedAt).toBeGreaterThan(0);
|
|
34
|
+
expect(entry.lastActiveAt).toBeGreaterThanOrEqual(entry.connectedAt);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("register derives empty capabilities for web interface", () => {
|
|
38
|
+
const registry = new ClientRegistry();
|
|
39
|
+
const entry = registry.register({
|
|
40
|
+
clientId: "web-1",
|
|
41
|
+
interfaceId: "cli",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(entry.capabilities).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("register derives host_browser capability for chrome-extension", () => {
|
|
48
|
+
const registry = new ClientRegistry();
|
|
49
|
+
const entry = registry.register({
|
|
50
|
+
clientId: "ext-1",
|
|
51
|
+
interfaceId: "chrome-extension",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(entry.capabilities).toEqual(["host_browser"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("register refreshes lastActiveAt on re-register with same clientId", () => {
|
|
58
|
+
const registry = new ClientRegistry();
|
|
59
|
+
const first = registry.register({
|
|
60
|
+
clientId: "mac-1",
|
|
61
|
+
interfaceId: "macos",
|
|
62
|
+
});
|
|
63
|
+
const firstActive = first.lastActiveAt;
|
|
64
|
+
|
|
65
|
+
// Advance time slightly
|
|
66
|
+
const second = registry.register({
|
|
67
|
+
clientId: "mac-1",
|
|
68
|
+
interfaceId: "macos",
|
|
69
|
+
hostHomeDir: "/Users/bob",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Same object reference — refreshed in place
|
|
73
|
+
expect(second).toBe(first);
|
|
74
|
+
expect(second.lastActiveAt).toBeGreaterThanOrEqual(firstActive);
|
|
75
|
+
expect(second.hostHomeDir).toBe("/Users/bob");
|
|
76
|
+
// connectedAt should NOT change on refresh
|
|
77
|
+
expect(second.connectedAt).toBe(first.connectedAt);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("register does not increase size on re-register", () => {
|
|
81
|
+
const registry = new ClientRegistry();
|
|
82
|
+
registry.register({ clientId: "mac-1", interfaceId: "macos" });
|
|
83
|
+
registry.register({ clientId: "mac-1", interfaceId: "macos" });
|
|
84
|
+
expect(registry.size).toBe(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── unregister ────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
test("unregister removes the entry", () => {
|
|
90
|
+
const registry = new ClientRegistry();
|
|
91
|
+
registry.register({ clientId: "mac-1", interfaceId: "macos" });
|
|
92
|
+
expect(registry.size).toBe(1);
|
|
93
|
+
|
|
94
|
+
registry.unregister("mac-1");
|
|
95
|
+
expect(registry.size).toBe(0);
|
|
96
|
+
expect(registry.get("mac-1")).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("unregister is a no-op for unknown clientId", () => {
|
|
100
|
+
const registry = new ClientRegistry();
|
|
101
|
+
registry.register({ clientId: "mac-1", interfaceId: "macos" });
|
|
102
|
+
registry.unregister("unknown");
|
|
103
|
+
expect(registry.size).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── touch ─────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
test("touch updates lastActiveAt", () => {
|
|
109
|
+
const registry = new ClientRegistry();
|
|
110
|
+
const entry = registry.register({
|
|
111
|
+
clientId: "mac-1",
|
|
112
|
+
interfaceId: "macos",
|
|
113
|
+
});
|
|
114
|
+
const before = entry.lastActiveAt;
|
|
115
|
+
registry.touch("mac-1");
|
|
116
|
+
expect(entry.lastActiveAt).toBeGreaterThanOrEqual(before);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("touch is a no-op for unknown clientId", () => {
|
|
120
|
+
const registry = new ClientRegistry();
|
|
121
|
+
// Should not throw
|
|
122
|
+
registry.touch("unknown");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── get ───────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
test("get returns the entry for a registered clientId", () => {
|
|
128
|
+
const registry = new ClientRegistry();
|
|
129
|
+
const entry = registry.register({
|
|
130
|
+
clientId: "mac-1",
|
|
131
|
+
interfaceId: "macos",
|
|
132
|
+
});
|
|
133
|
+
expect(registry.get("mac-1")).toBe(entry);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("get returns undefined for unknown clientId", () => {
|
|
137
|
+
const registry = new ClientRegistry();
|
|
138
|
+
expect(registry.get("unknown")).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── listAll ───────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test("listAll returns entries sorted by lastActiveAt descending", () => {
|
|
144
|
+
const registry = new ClientRegistry();
|
|
145
|
+
registry.register({ clientId: "old", interfaceId: "cli" });
|
|
146
|
+
// Touch the second one to make it most recent
|
|
147
|
+
registry.register({ clientId: "new", interfaceId: "macos" });
|
|
148
|
+
|
|
149
|
+
const all = registry.listAll();
|
|
150
|
+
expect(all.length).toBe(2);
|
|
151
|
+
expect(all[0].lastActiveAt).toBeGreaterThanOrEqual(all[1].lastActiveAt);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("listAll returns empty array when no clients registered", () => {
|
|
155
|
+
const registry = new ClientRegistry();
|
|
156
|
+
expect(registry.listAll()).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── listByCapability ──────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
test("listByCapability filters to clients with matching capability", () => {
|
|
162
|
+
const registry = new ClientRegistry();
|
|
163
|
+
registry.register({ clientId: "mac-1", interfaceId: "macos" });
|
|
164
|
+
registry.register({ clientId: "web-1", interfaceId: "cli" });
|
|
165
|
+
registry.register({ clientId: "ext-1", interfaceId: "chrome-extension" });
|
|
166
|
+
|
|
167
|
+
const bashCapable = registry.listByCapability("host_bash");
|
|
168
|
+
expect(bashCapable.length).toBe(1);
|
|
169
|
+
expect(bashCapable[0].clientId).toBe("mac-1");
|
|
170
|
+
|
|
171
|
+
const browserCapable = registry.listByCapability("host_browser");
|
|
172
|
+
expect(browserCapable.length).toBe(2);
|
|
173
|
+
// macOS now supports host_browser too
|
|
174
|
+
const ids = browserCapable.map((e) => e.clientId).sort();
|
|
175
|
+
expect(ids).toEqual(["ext-1", "mac-1"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("listByCapability returns empty when no clients match", () => {
|
|
179
|
+
const registry = new ClientRegistry();
|
|
180
|
+
registry.register({ clientId: "web-1", interfaceId: "cli" });
|
|
181
|
+
expect(registry.listByCapability("host_bash")).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── getMostRecentByCapability ─────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
test("getMostRecentByCapability returns the most recently active client", () => {
|
|
187
|
+
const registry = new ClientRegistry();
|
|
188
|
+
const mac1 = registry.register({ clientId: "mac-1", interfaceId: "macos" });
|
|
189
|
+
const mac2 = registry.register({ clientId: "mac-2", interfaceId: "macos" });
|
|
190
|
+
|
|
191
|
+
// Ensure deterministic ordering — both may register within the same ms
|
|
192
|
+
mac1.lastActiveAt = Date.now() - 5000;
|
|
193
|
+
mac2.lastActiveAt = Date.now();
|
|
194
|
+
|
|
195
|
+
const best = registry.getMostRecentByCapability("host_bash");
|
|
196
|
+
expect(best).toBeDefined();
|
|
197
|
+
expect(best!.clientId).toBe("mac-2");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("getMostRecentByCapability returns undefined when no clients match", () => {
|
|
201
|
+
const registry = new ClientRegistry();
|
|
202
|
+
registry.register({ clientId: "web-1", interfaceId: "cli" });
|
|
203
|
+
expect(registry.getMostRecentByCapability("host_bash")).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── toJSON ────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
test("toJSON serializes with ISO timestamps", () => {
|
|
209
|
+
const registry = new ClientRegistry();
|
|
210
|
+
const entry = registry.register({
|
|
211
|
+
clientId: "mac-1",
|
|
212
|
+
interfaceId: "macos",
|
|
213
|
+
hostHomeDir: "/Users/alice",
|
|
214
|
+
hostUsername: "alice",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const json = ClientRegistry.toJSON(entry);
|
|
218
|
+
expect(json.clientId).toBe("mac-1");
|
|
219
|
+
expect(json.interfaceId).toBe("macos");
|
|
220
|
+
expect(json.capabilities).toContain("host_bash");
|
|
221
|
+
expect(json.connectedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
222
|
+
expect(json.lastActiveAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
223
|
+
expect(json.hostHomeDir).toBe("/Users/alice");
|
|
224
|
+
expect(json.hostUsername).toBe("alice");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("toJSON omits host fields when not present", () => {
|
|
228
|
+
const registry = new ClientRegistry();
|
|
229
|
+
const entry = registry.register({
|
|
230
|
+
clientId: "web-1",
|
|
231
|
+
interfaceId: "cli",
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const json = ClientRegistry.toJSON(entry);
|
|
235
|
+
expect(json.hostHomeDir).toBeUndefined();
|
|
236
|
+
expect(json.hostUsername).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ── evictStale ─────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
test("evictStale removes entries older than maxAgeMs", () => {
|
|
242
|
+
const registry = new ClientRegistry();
|
|
243
|
+
const entry = registry.register({
|
|
244
|
+
clientId: "old-mac",
|
|
245
|
+
interfaceId: "macos",
|
|
246
|
+
});
|
|
247
|
+
// Backdate lastActiveAt by 1 hour
|
|
248
|
+
entry.lastActiveAt = Date.now() - 60 * 60 * 1000;
|
|
249
|
+
|
|
250
|
+
const evicted = registry.evictStale(30 * 60 * 1000); // 30 min threshold
|
|
251
|
+
expect(evicted).toBe(1);
|
|
252
|
+
expect(registry.size).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("evictStale keeps fresh entries", () => {
|
|
256
|
+
const registry = new ClientRegistry();
|
|
257
|
+
registry.register({ clientId: "fresh", interfaceId: "macos" });
|
|
258
|
+
|
|
259
|
+
const evicted = registry.evictStale(30 * 60 * 1000);
|
|
260
|
+
expect(evicted).toBe(0);
|
|
261
|
+
expect(registry.size).toBe(1);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("listAll triggers lazy eviction", () => {
|
|
265
|
+
const registry = new ClientRegistry();
|
|
266
|
+
const entry = registry.register({
|
|
267
|
+
clientId: "stale-cli",
|
|
268
|
+
interfaceId: "cli",
|
|
269
|
+
});
|
|
270
|
+
entry.lastActiveAt = Date.now() - 60 * 60 * 1000;
|
|
271
|
+
|
|
272
|
+
registry.register({ clientId: "fresh-mac", interfaceId: "macos" });
|
|
273
|
+
|
|
274
|
+
const all = registry.listAll();
|
|
275
|
+
expect(all.length).toBe(1);
|
|
276
|
+
expect(all[0].clientId).toBe("fresh-mac");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ── singleton ─────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
test("getClientRegistry returns a singleton", () => {
|
|
282
|
+
const a = getClientRegistry();
|
|
283
|
+
const b = getClientRegistry();
|
|
284
|
+
expect(a).toBe(b);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("__resetClientRegistryForTests clears the singleton", () => {
|
|
288
|
+
const a = getClientRegistry();
|
|
289
|
+
__resetClientRegistryForTests();
|
|
290
|
+
const b = getClientRegistry();
|
|
291
|
+
expect(a).not.toBe(b);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified registry of active client connections.
|
|
3
|
+
*
|
|
4
|
+
* Tracks all clients currently connected to the assistant — macOS desktop
|
|
5
|
+
* (SSE), iOS (SSE), web (SSE), chrome-extension (WebSocket), CLI, and
|
|
6
|
+
* channel interfaces. Each entry records the interface type, derived
|
|
7
|
+
* capabilities, connection timestamps, and optional host environment fields.
|
|
8
|
+
*
|
|
9
|
+
* The registry is populated by:
|
|
10
|
+
* - `handleSendMessage` in conversation-routes.ts (registers/refreshes on
|
|
11
|
+
* every inbound message with the interface and transport metadata)
|
|
12
|
+
*
|
|
13
|
+
* Future enhancements:
|
|
14
|
+
* - Deregister on SSE disconnect (events-routes.ts abort signal)
|
|
15
|
+
* - Surface ChromeExtensionRegistry entries through `listAll()`
|
|
16
|
+
* - Stale-client eviction sweep
|
|
17
|
+
*
|
|
18
|
+
* Consumers:
|
|
19
|
+
* - `assistant clients list` CLI command (via `list_clients` IPC route)
|
|
20
|
+
* - Future: deferred host tool routing (Phase 2)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { HostProxyCapability, InterfaceId } from "../channels/types.js";
|
|
24
|
+
import { supportsHostProxy } from "../channels/types.js";
|
|
25
|
+
import { getLogger } from "../util/logger.js";
|
|
26
|
+
|
|
27
|
+
const log = getLogger("client-registry");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default staleness threshold: entries not refreshed within this window are
|
|
31
|
+
* evicted on the next read. 30 minutes is generous — messages refresh the
|
|
32
|
+
* entry on every turn, so any actively used client stays well within this.
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_STALE_AGE_MS = 30 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Types
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/** All host-proxy capabilities checked against each interface on register. */
|
|
41
|
+
const ALL_CAPABILITIES: HostProxyCapability[] = [
|
|
42
|
+
"host_bash",
|
|
43
|
+
"host_file",
|
|
44
|
+
"host_cu",
|
|
45
|
+
"host_browser",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export interface ClientEntry {
|
|
49
|
+
/** Stable identifier for this client connection. */
|
|
50
|
+
clientId: string;
|
|
51
|
+
/** Interface type (e.g. "macos", "ios", "web", "chrome-extension"). */
|
|
52
|
+
interfaceId: InterfaceId;
|
|
53
|
+
/** Host-proxy capabilities this client supports. */
|
|
54
|
+
capabilities: HostProxyCapability[];
|
|
55
|
+
/** Wall-clock timestamp (ms) when the client first connected. */
|
|
56
|
+
connectedAt: number;
|
|
57
|
+
/** Wall-clock timestamp (ms) of the most recent activity. */
|
|
58
|
+
lastActiveAt: number;
|
|
59
|
+
/** Home directory on the host machine (only for host-proxy interfaces). */
|
|
60
|
+
hostHomeDir?: string;
|
|
61
|
+
/** Username on the host machine (only for host-proxy interfaces). */
|
|
62
|
+
hostUsername?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Serialized form returned by the IPC route / CLI command. */
|
|
66
|
+
export interface ClientEntryJSON {
|
|
67
|
+
clientId: string;
|
|
68
|
+
interfaceId: InterfaceId;
|
|
69
|
+
capabilities: HostProxyCapability[];
|
|
70
|
+
connectedAt: string;
|
|
71
|
+
lastActiveAt: string;
|
|
72
|
+
hostHomeDir?: string;
|
|
73
|
+
hostUsername?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Registry
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export class ClientRegistry {
|
|
81
|
+
private clients = new Map<string, ClientEntry>();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register or refresh a client connection.
|
|
85
|
+
*
|
|
86
|
+
* If a client with the same `clientId` already exists, its `lastActiveAt`
|
|
87
|
+
* and host environment fields are updated. Otherwise a new entry is created.
|
|
88
|
+
*/
|
|
89
|
+
register(opts: {
|
|
90
|
+
clientId: string;
|
|
91
|
+
interfaceId: InterfaceId;
|
|
92
|
+
hostHomeDir?: string;
|
|
93
|
+
hostUsername?: string;
|
|
94
|
+
}): ClientEntry {
|
|
95
|
+
const existing = this.clients.get(opts.clientId);
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
|
|
98
|
+
if (existing) {
|
|
99
|
+
existing.lastActiveAt = now;
|
|
100
|
+
if (opts.hostHomeDir !== undefined) {
|
|
101
|
+
existing.hostHomeDir = opts.hostHomeDir;
|
|
102
|
+
}
|
|
103
|
+
if (opts.hostUsername !== undefined) {
|
|
104
|
+
existing.hostUsername = opts.hostUsername;
|
|
105
|
+
}
|
|
106
|
+
log.debug(
|
|
107
|
+
{ clientId: opts.clientId, interfaceId: opts.interfaceId },
|
|
108
|
+
"client refreshed",
|
|
109
|
+
);
|
|
110
|
+
return existing;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const capabilities = ALL_CAPABILITIES.filter((cap) =>
|
|
114
|
+
supportsHostProxy(opts.interfaceId, cap),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const entry: ClientEntry = {
|
|
118
|
+
clientId: opts.clientId,
|
|
119
|
+
interfaceId: opts.interfaceId,
|
|
120
|
+
capabilities,
|
|
121
|
+
connectedAt: now,
|
|
122
|
+
lastActiveAt: now,
|
|
123
|
+
hostHomeDir: opts.hostHomeDir,
|
|
124
|
+
hostUsername: opts.hostUsername,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this.clients.set(opts.clientId, entry);
|
|
128
|
+
log.info(
|
|
129
|
+
{
|
|
130
|
+
clientId: opts.clientId,
|
|
131
|
+
interfaceId: opts.interfaceId,
|
|
132
|
+
capabilities,
|
|
133
|
+
},
|
|
134
|
+
"client registered",
|
|
135
|
+
);
|
|
136
|
+
return entry;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Remove a client connection. No-op if the clientId is not registered.
|
|
141
|
+
*/
|
|
142
|
+
unregister(clientId: string): void {
|
|
143
|
+
const entry = this.clients.get(clientId);
|
|
144
|
+
if (!entry) return;
|
|
145
|
+
this.clients.delete(clientId);
|
|
146
|
+
log.info(
|
|
147
|
+
{ clientId, interfaceId: entry.interfaceId },
|
|
148
|
+
"client unregistered",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Update `lastActiveAt` for a client without changing other fields.
|
|
154
|
+
* No-op if the clientId is not registered.
|
|
155
|
+
*/
|
|
156
|
+
touch(clientId: string): void {
|
|
157
|
+
const entry = this.clients.get(clientId);
|
|
158
|
+
if (entry) {
|
|
159
|
+
entry.lastActiveAt = Date.now();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Return a specific client entry, or `undefined` if not registered.
|
|
165
|
+
*/
|
|
166
|
+
get(clientId: string): ClientEntry | undefined {
|
|
167
|
+
return this.clients.get(clientId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Return all registered clients, sorted by `lastActiveAt` descending
|
|
172
|
+
* (most recently active first). Lazily evicts stale entries before
|
|
173
|
+
* returning so disconnected clients don't linger indefinitely.
|
|
174
|
+
*/
|
|
175
|
+
listAll(): ClientEntry[] {
|
|
176
|
+
this.evictStale();
|
|
177
|
+
return Array.from(this.clients.values()).sort(
|
|
178
|
+
(a, b) => b.lastActiveAt - a.lastActiveAt,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Return all registered clients that support the given capability,
|
|
184
|
+
* sorted by `lastActiveAt` descending.
|
|
185
|
+
*/
|
|
186
|
+
listByCapability(capability: HostProxyCapability): ClientEntry[] {
|
|
187
|
+
return this.listAll().filter((e) => e.capabilities.includes(capability));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Return the most recently active client that supports the given
|
|
192
|
+
* capability, or `undefined` if none exists.
|
|
193
|
+
*/
|
|
194
|
+
getMostRecentByCapability(
|
|
195
|
+
capability: HostProxyCapability,
|
|
196
|
+
): ClientEntry | undefined {
|
|
197
|
+
return this.listByCapability(capability)[0];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Remove entries whose `lastActiveAt` is older than `maxAgeMs`.
|
|
202
|
+
* Called lazily before reads to prevent unbounded map growth from
|
|
203
|
+
* churning client IDs that are never explicitly unregistered.
|
|
204
|
+
*
|
|
205
|
+
* @returns Number of evicted entries.
|
|
206
|
+
*/
|
|
207
|
+
evictStale(maxAgeMs: number = DEFAULT_STALE_AGE_MS): number {
|
|
208
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
209
|
+
let evicted = 0;
|
|
210
|
+
for (const [id, entry] of this.clients) {
|
|
211
|
+
if (entry.lastActiveAt < cutoff) {
|
|
212
|
+
this.clients.delete(id);
|
|
213
|
+
evicted++;
|
|
214
|
+
log.debug(
|
|
215
|
+
{ clientId: id, interfaceId: entry.interfaceId },
|
|
216
|
+
"client evicted (stale)",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return evicted;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Number of currently registered clients.
|
|
225
|
+
*/
|
|
226
|
+
get size(): number {
|
|
227
|
+
return this.clients.size;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Serialize a client entry to JSON (ISO timestamps).
|
|
232
|
+
*/
|
|
233
|
+
static toJSON(entry: ClientEntry): ClientEntryJSON {
|
|
234
|
+
return {
|
|
235
|
+
clientId: entry.clientId,
|
|
236
|
+
interfaceId: entry.interfaceId,
|
|
237
|
+
capabilities: entry.capabilities,
|
|
238
|
+
connectedAt: new Date(entry.connectedAt).toISOString(),
|
|
239
|
+
lastActiveAt: new Date(entry.lastActiveAt).toISOString(),
|
|
240
|
+
...(entry.hostHomeDir ? { hostHomeDir: entry.hostHomeDir } : {}),
|
|
241
|
+
...(entry.hostUsername ? { hostUsername: entry.hostUsername } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Module-level singleton ────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
let instance: ClientRegistry | null = null;
|
|
249
|
+
|
|
250
|
+
export function getClientRegistry(): ClientRegistry {
|
|
251
|
+
if (!instance) instance = new ClientRegistry();
|
|
252
|
+
return instance;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Test helper: reset the module-level singleton so each test starts with a
|
|
257
|
+
* fresh registry.
|
|
258
|
+
*/
|
|
259
|
+
export function __resetClientRegistryForTests(): void {
|
|
260
|
+
instance = null;
|
|
261
|
+
}
|