@phuetz/code-buddy 2.0.0 → 2.1.0
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/README.fr.md +56 -0
- package/README.md +73 -16
- package/codebuddy-runtime.json +5 -5
- package/dist/agent/codebuddy-agent.d.ts +18 -0
- package/dist/agent/codebuddy-agent.js +45 -8
- package/dist/agent/execution/agent-executor.d.ts +0 -9
- package/dist/agent/execution/agent-executor.js +165 -12
- package/dist/agent/execution/context-pipeline.d.ts +9 -0
- package/dist/agent/execution/context-pipeline.js +64 -2
- package/dist/agent/execution/tool-loop-guard.d.ts +76 -0
- package/dist/agent/execution/tool-loop-guard.js +165 -0
- package/dist/agent/execution/tool-selection-strategy.js +19 -2
- package/dist/agent/facades/session-facade.d.ts +0 -6
- package/dist/agent/facades/session-facade.js +0 -45
- package/dist/agent/hermes-claw-migrate.d.ts +0 -21
- package/dist/agent/hermes-claw-migrate.js +9 -4
- package/dist/agent/hermes-protocol-gateways.js +8 -1
- package/dist/agent/repo-profiler.js +17 -4
- package/dist/agent/self-improvement/authored-tool-runtime.js +10 -2
- package/dist/agent/self-improvement/evolution/ast-novelty.d.ts +5 -0
- package/dist/agent/self-improvement/evolution/ast-novelty.js +58 -23
- package/dist/agent/self-improvement/evolution/code-variant-store.d.ts +3 -0
- package/dist/agent/self-improvement/evolution/evolution-engine.js +2 -0
- package/dist/agent/self-improvement/evolution/protected-paths.js +6 -0
- package/dist/agent/self-improvement/evolution/variant-fitness.d.ts +13 -2
- package/dist/agent/self-improvement/evolution/variant-fitness.js +184 -58
- package/dist/agent/self-improvement/evolution/worktree-scorer.d.ts +2 -0
- package/dist/agent/self-improvement/evolution/worktree-scorer.js +15 -0
- package/dist/agent/self-improvement/evolutionary-archive.d.ts +2 -1
- package/dist/agent/self-improvement/evolutionary-archive.js +34 -5
- package/dist/agent/self-improvement/proposal-store.d.ts +1 -0
- package/dist/agent/self-improvement/proposal-store.js +79 -17
- package/dist/agent/self-improvement/skill-apply-journal.d.ts +44 -0
- package/dist/agent/self-improvement/skill-apply-journal.js +178 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.d.ts +3 -0
- package/dist/agent/self-improvement/skill-behavior-benchmark.js +19 -0
- package/dist/agent/self-improvement/skill-behavior-gate.d.ts +34 -0
- package/dist/agent/self-improvement/skill-behavior-gate.js +96 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +22 -0
- package/dist/agent/self-improvement/skill-engine.js +385 -13
- package/dist/agent/self-improvement/skill-gate.d.ts +2 -0
- package/dist/agent/self-improvement/skill-gate.js +17 -2
- package/dist/agent/self-improvement/skill-mutator.d.ts +6 -1
- package/dist/agent/self-improvement/skill-mutator.js +51 -15
- package/dist/agent/self-improvement/skill-types.d.ts +2 -1
- package/dist/agent/self-improvement/types.d.ts +11 -0
- package/dist/agent/tool-handler.d.ts +1 -1
- package/dist/agent/tool-handler.js +28 -9
- package/dist/auth/profile-manager.js +1 -1
- package/dist/channels/companion-channel-turn.d.ts +53 -4
- package/dist/channels/companion-channel-turn.js +218 -16
- package/dist/channels/slash-parity.d.ts +4 -6
- package/dist/channels/slash-parity.js +9 -44
- package/dist/cli/session-commands.d.ts +9 -0
- package/dist/cli/session-commands.js +45 -5
- package/dist/cli/session-picker.d.ts +43 -0
- package/dist/cli/session-picker.js +128 -0
- package/dist/codebuddy/a2a-call-tool-defs.d.ts +26 -0
- package/dist/codebuddy/a2a-call-tool-defs.js +12 -0
- package/dist/codebuddy/fleet-tool-defs.d.ts +1 -0
- package/dist/codebuddy/fleet-tool-defs.js +24 -0
- package/dist/codebuddy/providers/chatgpt-headers.d.ts +18 -0
- package/dist/codebuddy/providers/chatgpt-headers.js +37 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +2 -1
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +10 -23
- package/dist/codebuddy/providers/provider-openai-compat.js +1 -1
- package/dist/codebuddy/ragchat-tool-defs.d.ts +31 -0
- package/dist/codebuddy/ragchat-tool-defs.js +19 -0
- package/dist/codebuddy/resource-catalog-tool-defs.d.ts +28 -0
- package/dist/codebuddy/resource-catalog-tool-defs.js +18 -0
- package/dist/codebuddy/tool-definitions/agent-tools.js +2 -2
- package/dist/codebuddy/tool-definitions/code-exec-tools.js +5 -1
- package/dist/codebuddy/tool-definitions/cron-tools.js +6 -0
- package/dist/codebuddy/tool-definitions/self-describe-tools.js +6 -1
- package/dist/codebuddy/tools.d.ts +0 -6
- package/dist/codebuddy/tools.js +25 -3
- package/dist/commands/cli/daemon-commands.js +3 -1
- package/dist/commands/cli/evolve-command.js +21 -5
- package/dist/commands/cli/fleet-collaboration-commands.d.ts +2 -0
- package/dist/commands/cli/fleet-collaboration-commands.js +57 -0
- package/dist/commands/cli/fleet-commands.js +29 -0
- package/dist/commands/cli/fleet-mission-commands.d.ts +3 -0
- package/dist/commands/cli/fleet-mission-commands.js +72 -0
- package/dist/commands/cli/fleet-rooms-commands.d.ts +10 -0
- package/dist/commands/cli/fleet-rooms-commands.js +289 -0
- package/dist/commands/cli/tools-commands.js +2 -1
- package/dist/commands/cli/triage-command.d.ts +8 -0
- package/dist/commands/cli/triage-command.js +72 -0
- package/dist/commands/cli/utility-commands.js +25 -1
- package/dist/commands/client-dispatcher.d.ts +7 -0
- package/dist/commands/client-dispatcher.js +73 -21
- package/dist/commands/cron-cli/index.d.ts +3 -1
- package/dist/commands/cron-cli/index.js +25 -0
- package/dist/commands/device-auth.d.ts +10 -0
- package/dist/commands/device-auth.js +91 -0
- package/dist/commands/enhanced-command-handler.d.ts +9 -0
- package/dist/commands/enhanced-command-handler.js +28 -6
- package/dist/commands/handlers/agent-handlers.d.ts +1 -1
- package/dist/commands/handlers/agent-handlers.js +4 -4
- package/dist/commands/handlers/channel-handlers.js +53 -2
- package/dist/commands/handlers/export-handlers.d.ts +3 -2
- package/dist/commands/handlers/export-handlers.js +55 -28
- package/dist/commands/handlers/extra-handlers.js +5 -1
- package/dist/commands/handlers/grill-me-handler.js +26 -18
- package/dist/commands/handlers/memory-handlers.d.ts +6 -2
- package/dist/commands/handlers/memory-handlers.js +35 -15
- package/dist/commands/handlers/missing-handlers.d.ts +2 -2
- package/dist/commands/handlers/missing-handlers.js +4 -4
- package/dist/commands/handlers/resources-handler.d.ts +12 -0
- package/dist/commands/handlers/resources-handler.js +52 -0
- package/dist/commands/handlers/starter-handlers.d.ts +1 -1
- package/dist/commands/handlers/starter-handlers.js +32 -2
- package/dist/commands/handlers/switch-handler.d.ts +1 -1
- package/dist/commands/handlers/switch-handler.js +15 -0
- package/dist/commands/handlers/test-handlers.js +4 -86
- package/dist/commands/handlers/ui-handlers.js +11 -5
- package/dist/commands/login-chatgpt.d.ts +8 -0
- package/dist/commands/login-chatgpt.js +17 -0
- package/dist/commands/mcp-import.d.ts +11 -0
- package/dist/commands/mcp-import.js +67 -0
- package/dist/commands/mcp-invoke.d.ts +2 -0
- package/dist/commands/mcp-invoke.js +68 -0
- package/dist/commands/mcp.js +4 -0
- package/dist/commands/pipeline.d.ts +7 -1
- package/dist/commands/pipeline.js +104 -7
- package/dist/commands/resources.d.ts +2 -0
- package/dist/commands/resources.js +64 -0
- package/dist/commands/skills-cli/index.js +35 -12
- package/dist/commands/slash/builtin-commands.js +16 -3
- package/dist/commands/slash/index.d.ts +3 -1
- package/dist/commands/slash/index.js +2 -0
- package/dist/commands/slash/surfaces.d.ts +53 -0
- package/dist/commands/slash/surfaces.js +177 -0
- package/dist/commands/slash/types.d.ts +17 -0
- package/dist/commands/try.d.ts +1 -1
- package/dist/commands/try.js +1 -1
- package/dist/companion/companion-identity.d.ts +43 -0
- package/dist/companion/companion-identity.js +136 -0
- package/dist/companion/companion-toolset.d.ts +66 -0
- package/dist/companion/companion-toolset.js +368 -0
- package/dist/companion/companion-turn.d.ts +10 -2
- package/dist/companion/companion-turn.js +38 -1
- package/dist/config/code-exec-policy.d.ts +25 -0
- package/dist/config/code-exec-policy.js +39 -0
- package/dist/config/model-tools.d.ts +9 -0
- package/dist/config/model-tools.js +4 -0
- package/dist/context/context-manager-v2.js +2 -2
- package/dist/context/lm-resizer-compressor.js +3 -2
- package/dist/context/lm-resizer-diagnostics.d.ts +9 -0
- package/dist/context/lm-resizer-diagnostics.js +30 -0
- package/dist/context/tool-output-masking.js +3 -4
- package/dist/curator/curator.d.ts +6 -0
- package/dist/curator/curator.js +53 -3
- package/dist/daemon/cron-agent-bridge.d.ts +6 -0
- package/dist/daemon/cron-agent-bridge.js +31 -4
- package/dist/doctor/index.d.ts +11 -2
- package/dist/doctor/index.js +84 -21
- package/dist/doctor/integrations.d.ts +179 -0
- package/dist/doctor/integrations.js +227 -0
- package/dist/doctor/triage.d.ts +108 -0
- package/dist/doctor/triage.js +304 -0
- package/dist/fleet/collaboration.d.ts +44 -0
- package/dist/fleet/collaboration.js +170 -0
- package/dist/fleet/fleet-listener.d.ts +1 -0
- package/dist/fleet/fleet-listener.js +87 -46
- package/dist/fleet/fleet-registry.d.ts +2 -0
- package/dist/fleet/resource-catalog.d.ts +245 -0
- package/dist/fleet/resource-catalog.js +211 -0
- package/dist/fleet/rooms/room-access.d.ts +138 -0
- package/dist/fleet/rooms/room-access.js +196 -0
- package/dist/fleet/rooms/room-client.d.ts +158 -0
- package/dist/fleet/rooms/room-client.js +685 -0
- package/dist/fleet/rooms/room-event.d.ts +139 -0
- package/dist/fleet/rooms/room-event.js +330 -0
- package/dist/fleet/rooms/room-filter.d.ts +48 -0
- package/dist/fleet/rooms/room-filter.js +152 -0
- package/dist/fleet/rooms/room-hub.d.ts +154 -0
- package/dist/fleet/rooms/room-hub.js +363 -0
- package/dist/fleet/rooms/room-identity.d.ts +26 -0
- package/dist/fleet/rooms/room-identity.js +82 -0
- package/dist/fleet/rooms/room-lock.d.ts +34 -0
- package/dist/fleet/rooms/room-lock.js +180 -0
- package/dist/fleet/rooms/room-observations.d.ts +7 -0
- package/dist/fleet/rooms/room-observations.js +61 -0
- package/dist/fleet/rooms/room-server.d.ts +28 -0
- package/dist/fleet/rooms/room-server.js +75 -0
- package/dist/fleet/rooms/room-store.d.ts +119 -0
- package/dist/fleet/rooms/room-store.js +448 -0
- package/dist/fleet/rooms/room-ws-bridge.d.ts +27 -0
- package/dist/fleet/rooms/room-ws-bridge.js +137 -0
- package/dist/fleet/sensory-mission-bridge.d.ts +42 -0
- package/dist/fleet/sensory-mission-bridge.js +136 -0
- package/dist/git/worktree-sessions.js +1 -0
- package/dist/harness/contract.d.ts +4 -4
- package/dist/harness/fleet-supervisor.d.ts +16 -0
- package/dist/harness/fleet-supervisor.js +64 -0
- package/dist/harness/index.d.ts +4 -0
- package/dist/harness/index.js +4 -0
- package/dist/harness/mission-runner.d.ts +3 -0
- package/dist/harness/mission-runner.js +53 -0
- package/dist/harness/mission-store.d.ts +93 -0
- package/dist/harness/mission-store.js +297 -0
- package/dist/harness/tool-harness.d.ts +56 -0
- package/dist/harness/tool-harness.js +153 -0
- package/dist/hooks/use-enhanced-input.js +48 -4
- package/dist/hooks/use-input-handler.d.ts +1 -0
- package/dist/hooks/use-input-handler.js +82 -22
- package/dist/hooks/use-input-history.js +1 -0
- package/dist/identity/lisa-introspection.js +5 -2
- package/dist/identity/operational-self-model.d.ts +2 -0
- package/dist/identity/operational-self-model.js +4 -0
- package/dist/index.js +52 -6
- package/dist/mcp/client.js +6 -0
- package/dist/mcp/config.d.ts +20 -0
- package/dist/mcp/config.js +121 -8
- package/dist/mcp/import-normalize.d.ts +12 -0
- package/dist/mcp/import-normalize.js +114 -0
- package/dist/mcp/transports.d.ts +10 -5
- package/dist/mcp/transports.js +61 -99
- package/dist/mcp/types.d.ts +5 -0
- package/dist/media/comfyui-recipe-contract.d.ts +7 -7
- package/dist/memory/facts-memory.d.ts +11 -1
- package/dist/memory/facts-memory.js +47 -8
- package/dist/memory/memory-candidate-queue.d.ts +2 -2
- package/dist/memory/memory-candidate-queue.js +6 -6
- package/dist/memory/persistent-memory.d.ts +13 -8
- package/dist/memory/persistent-memory.js +77 -30
- package/dist/nodes/device-node.js +5 -9
- package/dist/observability/mobile-supervision-gateway-contract.js +20 -7
- package/dist/observability/run-event-writer.d.ts +21 -0
- package/dist/observability/run-event-writer.js +81 -0
- package/dist/observability/run-store.d.ts +5 -0
- package/dist/observability/run-store.js +18 -13
- package/dist/observability/run-trajectory.js +1 -1
- package/dist/optimization/prompt-cache.d.ts +19 -2
- package/dist/optimization/prompt-cache.js +41 -6
- package/dist/orchestration/orchestrator.d.ts +21 -0
- package/dist/orchestration/orchestrator.js +122 -15
- package/dist/persistence/session-content.d.ts +8 -0
- package/dist/persistence/session-content.js +76 -0
- package/dist/persistence/session-handoff.d.ts +64 -0
- package/dist/persistence/session-handoff.js +80 -0
- package/dist/persistence/session-history.d.ts +4 -0
- package/dist/persistence/session-history.js +38 -0
- package/dist/persistence/session-store.d.ts +9 -0
- package/dist/persistence/session-store.js +48 -20
- package/dist/personas/persona-manager.js +17 -1
- package/dist/plugins/code-explorer/CodeExplorerManager.d.ts +10 -4
- package/dist/plugins/code-explorer/CodeExplorerManager.js +69 -16
- package/dist/plugins/plugin-manager.js +10 -1
- package/dist/prompts/prompt-manager.js +11 -4
- package/dist/prompts/system-base.js +6 -2
- package/dist/protocols/a2a/codebuddy-executor.d.ts +11 -2
- package/dist/protocols/a2a/codebuddy-executor.js +9 -8
- package/dist/protocols/a2a/index.d.ts +1 -1
- package/dist/protocols/a2a/jsonrpc-v1.d.ts +41 -0
- package/dist/protocols/a2a/jsonrpc-v1.js +145 -0
- package/dist/protocols/a2a/peer-config.d.ts +20 -0
- package/dist/protocols/a2a/peer-config.js +55 -0
- package/dist/providers/codex-oauth.js +30 -6
- package/dist/providers/model-provider-compat.js +2 -1
- package/dist/sandbox/os-sandbox.d.ts +1 -1
- package/dist/scheduler/cron-scheduler.d.ts +15 -2
- package/dist/scheduler/cron-scheduler.js +35 -4
- package/dist/scheduler/job-notepad.d.ts +69 -0
- package/dist/scheduler/job-notepad.js +339 -0
- package/dist/security/audit-logger.d.ts +1 -1
- package/dist/security/compute-confinement.d.ts +4 -0
- package/dist/security/compute-confinement.js +89 -0
- package/dist/security/session-encryption.d.ts +4 -0
- package/dist/security/session-encryption.js +31 -0
- package/dist/sensory/voice-loop.d.ts +17 -0
- package/dist/sensory/voice-loop.js +72 -15
- package/dist/server/auth/device-session-context.d.ts +9 -0
- package/dist/server/auth/device-session-context.js +13 -0
- package/dist/server/auth/device-store.d.ts +89 -0
- package/dist/server/auth/device-store.js +261 -0
- package/dist/server/auth/device-token.d.ts +3 -0
- package/dist/server/auth/device-token.js +6 -0
- package/dist/server/auth/jwt.js +4 -0
- package/dist/server/index.d.ts +0 -10
- package/dist/server/index.js +28 -0
- package/dist/server/middleware/auth.js +3 -2
- package/dist/server/routes/a2a-jsonrpc.d.ts +9 -0
- package/dist/server/routes/a2a-jsonrpc.js +66 -0
- package/dist/server/routes/a2a-protocol.d.ts +12 -0
- package/dist/server/routes/a2a-protocol.js +1 -1
- package/dist/server/routes/device-auth.d.ts +4 -0
- package/dist/server/routes/device-auth.js +40 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/websocket/handler.d.ts +13 -0
- package/dist/server/websocket/handler.js +102 -17
- package/dist/services/prompt-builder.d.ts +2 -0
- package/dist/services/prompt-builder.js +13 -5
- package/dist/services/runtime-settings-context.d.ts +73 -0
- package/dist/services/runtime-settings-context.js +69 -0
- package/dist/skills/executor.js +2 -0
- package/dist/skills/hub.js +16 -14
- package/dist/skills/local-inventory.d.ts +21 -0
- package/dist/skills/local-inventory.js +72 -0
- package/dist/skills/skill-loader.js +2 -2
- package/dist/skills/skill-usage-store.d.ts +49 -0
- package/dist/skills/skill-usage-store.js +121 -0
- package/dist/testing/ai-integration-tests.js +8 -5
- package/dist/themes/theme-manager.d.ts +12 -0
- package/dist/themes/theme-manager.js +29 -0
- package/dist/themes/theme-schema.d.ts +24 -24
- package/dist/tools/a2a-call-tool.d.ts +34 -0
- package/dist/tools/a2a-call-tool.js +75 -0
- package/dist/tools/bash/streaming-executor.d.ts +1 -0
- package/dist/tools/bash/streaming-executor.js +78 -90
- package/dist/tools/code-exec-preflight-runner.d.ts +41 -0
- package/dist/tools/code-exec-preflight-runner.js +254 -0
- package/dist/tools/code-exec-preflight.d.ts +44 -0
- package/dist/tools/code-exec-preflight.js +501 -0
- package/dist/tools/code-exec-tool.d.ts +48 -1
- package/dist/tools/code-exec-tool.js +191 -47
- package/dist/tools/comment-watcher.js +29 -12
- package/dist/tools/core-code-inspection.d.ts +56 -0
- package/dist/tools/core-code-inspection.js +86 -0
- package/dist/tools/create-skill-tool.d.ts +2 -3
- package/dist/tools/create-skill-tool.js +16 -75
- package/dist/tools/cronjob-tool.d.ts +1 -0
- package/dist/tools/cronjob-tool.js +1 -0
- package/dist/tools/env-doctor-tool.d.ts +2 -0
- package/dist/tools/env-doctor-tool.js +4 -3
- package/dist/tools/execute-code-rpc-invoker.d.ts +1 -1
- package/dist/tools/execute-code-rpc-invoker.js +35 -19
- package/dist/tools/execute-code-runner.d.ts +2 -0
- package/dist/tools/execute-code-runner.js +52 -17
- package/dist/tools/fleet-room-tool.d.ts +104 -0
- package/dist/tools/fleet-room-tool.js +124 -0
- package/dist/tools/integration-tool-hints.d.ts +4 -0
- package/dist/tools/integration-tool-hints.js +19 -0
- package/dist/tools/list-peers-tool.d.ts +3 -0
- package/dist/tools/list-peers-tool.js +11 -4
- package/dist/tools/media-generation-tool.d.ts +16 -1
- package/dist/tools/media-generation-tool.js +341 -9
- package/dist/tools/merge-conflict-tool.js +53 -25
- package/dist/tools/metadata.d.ts +1 -5
- package/dist/tools/metadata.js +21 -28
- package/dist/tools/ragchat-tool.d.ts +39 -0
- package/dist/tools/ragchat-tool.js +105 -0
- package/dist/tools/registry/fleet-tools.d.ts +9 -0
- package/dist/tools/registry/fleet-tools.js +22 -1
- package/dist/tools/registry/knowledge-tools.d.ts +2 -2
- package/dist/tools/registry/knowledge-tools.js +2 -2
- package/dist/tools/registry/lessons-tools.d.ts +7 -0
- package/dist/tools/registry/lessons-tools.js +43 -3
- package/dist/tools/registry/memory-tools.d.ts +1 -1
- package/dist/tools/registry/memory-tools.js +8 -8
- package/dist/tools/registry/research-tools.d.ts +1 -0
- package/dist/tools/registry/research-tools.js +4 -13
- package/dist/tools/registry/self-describe-tools.d.ts +1 -1
- package/dist/tools/registry/self-describe-tools.js +43 -30
- package/dist/tools/resource-catalog-tool.d.ts +38 -0
- package/dist/tools/resource-catalog-tool.js +46 -0
- package/dist/tools/skills-inspection-tool.js +30 -1
- package/dist/tools/stream-tool-output.d.ts +3 -0
- package/dist/tools/stream-tool-output.js +33 -0
- package/dist/tools/tool-call-scheduler.d.ts +10 -0
- package/dist/tools/tool-call-scheduler.js +58 -0
- package/dist/tools/tool-effect.d.ts +5 -0
- package/dist/tools/tool-effect.js +26 -0
- package/dist/tools/tool-search.d.ts +12 -11
- package/dist/tools/tool-search.js +67 -26
- package/dist/tools/types.d.ts +6 -0
- package/dist/ui/components/ChatHistory.d.ts +1 -1
- package/dist/ui/components/ChatHistory.js +2 -2
- package/dist/ui/components/ChatInput.d.ts +2 -1
- package/dist/ui/components/ChatInput.js +42 -88
- package/dist/ui/components/ChatInterface.d.ts +4 -2
- package/dist/ui/components/ChatInterface.js +27 -63
- package/dist/ui/components/CommandSuggestions.d.ts +0 -1
- package/dist/ui/components/CommandSuggestions.js +4 -6
- package/dist/ui/components/FileAutocomplete.js +1 -1
- package/dist/ui/components/KeyboardHelp.js +8 -6
- package/dist/ui/components/ModelSelection.js +6 -5
- package/dist/ui/components/StatusBar.d.ts +2 -1
- package/dist/ui/components/StatusBar.js +8 -7
- package/dist/ui/context/theme-context.js +3 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/ui/utils/markdown-renderer.js +4 -13
- package/dist/utils/atomic-write.d.ts +22 -0
- package/dist/utils/atomic-write.js +29 -0
- package/dist/utils/bounded-output.d.ts +16 -0
- package/dist/utils/bounded-output.js +63 -0
- package/dist/utils/device-store-file.d.ts +4 -0
- package/dist/utils/device-store-file.js +40 -0
- package/dist/utils/export-manager.d.ts +9 -0
- package/dist/utils/export-manager.js +19 -0
- package/dist/utils/first-use-hints.d.ts +25 -0
- package/dist/utils/first-use-hints.js +58 -0
- package/dist/utils/history-manager.js +1 -1
- package/dist/utils/update-notifier.js +16 -18
- package/dist/wizard/environment-detection.d.ts +5 -1
- package/dist/wizard/environment-detection.js +49 -7
- package/dist/wizard/onboarding.d.ts +5 -1
- package/dist/wizard/onboarding.js +90 -30
- package/dist/wizard/provider-onboarding.d.ts +2 -0
- package/dist/wizard/provider-onboarding.js +55 -17
- package/package.json +7 -3
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet rooms — member client.
|
|
3
|
+
*
|
|
4
|
+
* Reconnection follows the Buzz ACP harness (`crates/buzz-acp/src/relay.rs`):
|
|
5
|
+
* resubscribe after reconnect from a per-subscription cursor and drop events
|
|
6
|
+
* already seen with a two-generation id set (`TwoGenDedup`, 12 000 ids). The
|
|
7
|
+
* cursor is the hub `seq` (plus its `storeId`) instead of `created_at`, so
|
|
8
|
+
* member clock drift cannot open a hole.
|
|
9
|
+
*
|
|
10
|
+
* Everything received is untrusted data: signatures and room tags are checked
|
|
11
|
+
* again locally (the hub could be compromised), and `text` is the content
|
|
12
|
+
* passed through the fleet peer-text sanitizer. This client never runs a tool,
|
|
13
|
+
* a prompt or a command because of a message.
|
|
14
|
+
*
|
|
15
|
+
* @module fleet/rooms/room-client
|
|
16
|
+
*/
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
import WebSocket from 'ws';
|
|
19
|
+
import { sanitizePeerText } from '../peer-text-sanitizer.js';
|
|
20
|
+
import { buildRoomAuth, buildRoomMessage, canonicalRoomAudience, checkRoomEventShape, deriveRoomPublicKey, freezeRoomEvent, parseThreadMarkers, resolveThread, roomOf, signRoomEvent, tagValues, verifyRoomEvent, } from './room-event.js';
|
|
21
|
+
import { parseRoomFilters, roomFiltersMatch } from './room-filter.js';
|
|
22
|
+
const SEEN_ID_LIMIT = 12_000;
|
|
23
|
+
const DEFAULT_FETCH_MAX_MESSAGES = 500;
|
|
24
|
+
const MAX_FETCH_MESSAGES = 1_000;
|
|
25
|
+
const DEFAULT_FETCH_MAX_BYTES = 4 * 1024 * 1024;
|
|
26
|
+
const MAX_FETCH_BYTES = 4 * 1024 * 1024;
|
|
27
|
+
const STORE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
28
|
+
export class RoomClientError extends Error {
|
|
29
|
+
code;
|
|
30
|
+
constructor(message, code) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.name = 'RoomClientError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class TwoGenerationSet {
|
|
37
|
+
limit;
|
|
38
|
+
current = new Set();
|
|
39
|
+
previous = new Set();
|
|
40
|
+
constructor(limit) {
|
|
41
|
+
this.limit = limit;
|
|
42
|
+
}
|
|
43
|
+
/** Returns true when the id was not seen before. */
|
|
44
|
+
add(id) {
|
|
45
|
+
if (this.current.has(id) || this.previous.has(id))
|
|
46
|
+
return false;
|
|
47
|
+
this.current.add(id);
|
|
48
|
+
if (this.current.size >= this.limit / 2) {
|
|
49
|
+
this.previous = this.current;
|
|
50
|
+
this.current = new Set();
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function positive(name, value, fallback) {
|
|
56
|
+
const resolved = value ?? fallback;
|
|
57
|
+
if (!Number.isFinite(resolved) || resolved <= 0)
|
|
58
|
+
throw new RangeError(`${name} must be a finite number > 0`);
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
function boundedPositiveInteger(name, value, fallback, maximum) {
|
|
62
|
+
const resolved = value ?? fallback;
|
|
63
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > maximum) {
|
|
64
|
+
throw new RangeError(`${name} must be a positive safe integer <= ${maximum}`);
|
|
65
|
+
}
|
|
66
|
+
return resolved;
|
|
67
|
+
}
|
|
68
|
+
function isStoreId(value) {
|
|
69
|
+
return typeof value === 'string' && STORE_ID_PATTERN.test(value);
|
|
70
|
+
}
|
|
71
|
+
export class FleetRoomClient extends EventEmitter {
|
|
72
|
+
pubkey;
|
|
73
|
+
audience;
|
|
74
|
+
options;
|
|
75
|
+
requestTimeoutMs;
|
|
76
|
+
reconnectInitialDelayMs;
|
|
77
|
+
reconnectMaxDelayMs;
|
|
78
|
+
reconnectMaxAttempts;
|
|
79
|
+
ws = null;
|
|
80
|
+
ready = false;
|
|
81
|
+
closing = false;
|
|
82
|
+
everConnected = false;
|
|
83
|
+
requestCounter = 0;
|
|
84
|
+
pending = new Map();
|
|
85
|
+
subscriptions = new Map();
|
|
86
|
+
seen = new TwoGenerationSet(SEEN_ID_LIMIT);
|
|
87
|
+
reconnectTimer = null;
|
|
88
|
+
reconnectAttempts = 0;
|
|
89
|
+
readyWaiters = [];
|
|
90
|
+
opening = null;
|
|
91
|
+
connectionInfo = null;
|
|
92
|
+
constructor(options) {
|
|
93
|
+
super();
|
|
94
|
+
this.options = options;
|
|
95
|
+
this.pubkey = deriveRoomPublicKey(options.secretKey);
|
|
96
|
+
this.audience = canonicalRoomAudience(options.audience ?? options.url);
|
|
97
|
+
this.requestTimeoutMs = positive('requestTimeoutMs', options.requestTimeoutMs, 10_000);
|
|
98
|
+
this.reconnectInitialDelayMs = positive('reconnectInitialDelayMs', options.reconnectInitialDelayMs, 250);
|
|
99
|
+
this.reconnectMaxDelayMs = positive('reconnectMaxDelayMs', options.reconnectMaxDelayMs, 30_000);
|
|
100
|
+
this.reconnectMaxAttempts = positive('reconnectMaxAttempts', options.reconnectMaxAttempts, 20);
|
|
101
|
+
}
|
|
102
|
+
get isReady() {
|
|
103
|
+
return this.ready;
|
|
104
|
+
}
|
|
105
|
+
/** Connect, authenticate the WebSocket, then prove the member key. */
|
|
106
|
+
connect() {
|
|
107
|
+
this.closing = false;
|
|
108
|
+
if (this.ready && this.connectionInfo)
|
|
109
|
+
return Promise.resolve(this.connectionInfo);
|
|
110
|
+
return this.startOpen();
|
|
111
|
+
}
|
|
112
|
+
startOpen() {
|
|
113
|
+
if (this.opening)
|
|
114
|
+
return this.opening;
|
|
115
|
+
const opening = this.open();
|
|
116
|
+
this.opening = opening;
|
|
117
|
+
void opening.finally(() => {
|
|
118
|
+
if (this.opening === opening)
|
|
119
|
+
this.opening = null;
|
|
120
|
+
}).catch(() => undefined);
|
|
121
|
+
return opening;
|
|
122
|
+
}
|
|
123
|
+
open() {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
let settled = false;
|
|
126
|
+
let transportAuthRequested = false;
|
|
127
|
+
let roomProofStarted = false;
|
|
128
|
+
const fail = (error) => {
|
|
129
|
+
if (settled)
|
|
130
|
+
return;
|
|
131
|
+
settled = true;
|
|
132
|
+
reject(error);
|
|
133
|
+
try {
|
|
134
|
+
ws.terminate();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const ws = new WebSocket(this.options.url);
|
|
141
|
+
this.ws = ws;
|
|
142
|
+
const timer = setTimeout(() => fail(new RoomClientError('connection timed out', 'TIMEOUT')), this.requestTimeoutMs);
|
|
143
|
+
const startRoomProof = () => {
|
|
144
|
+
if (roomProofStarted || this.ws !== ws)
|
|
145
|
+
return;
|
|
146
|
+
roomProofStarted = true;
|
|
147
|
+
void this.proveIdentity(ws).then((info) => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
if (!settled) {
|
|
150
|
+
settled = true;
|
|
151
|
+
resolve(info);
|
|
152
|
+
}
|
|
153
|
+
}, (error) => { clearTimeout(timer); fail(error); });
|
|
154
|
+
};
|
|
155
|
+
ws.on('message', (data) => {
|
|
156
|
+
if (this.ws !== ws)
|
|
157
|
+
return;
|
|
158
|
+
let message;
|
|
159
|
+
try {
|
|
160
|
+
message = JSON.parse(data.toString());
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (message.type === 'connected') {
|
|
166
|
+
if (this.options.apiKey || this.options.jwt) {
|
|
167
|
+
if (transportAuthRequested)
|
|
168
|
+
return;
|
|
169
|
+
transportAuthRequested = true;
|
|
170
|
+
this.sendRaw({
|
|
171
|
+
type: 'authenticate',
|
|
172
|
+
payload: this.options.apiKey ? { apiKey: this.options.apiKey } : { token: this.options.jwt },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
startRoomProof();
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (message.type === 'authenticated') {
|
|
181
|
+
startRoomProof();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (message.type === 'error' && !message.requestId && !this.ready) {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
fail(new RoomClientError(message.error?.message ?? 'server error', message.error?.code ?? 'SERVER_ERROR'));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.handleMessage(message);
|
|
190
|
+
});
|
|
191
|
+
ws.on('error', (error) => {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
194
|
+
});
|
|
195
|
+
ws.on('close', () => {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
if (this.ws === ws)
|
|
198
|
+
this.onSocketClosed();
|
|
199
|
+
fail(new RoomClientError('connection closed before authentication', 'DISCONNECTED'));
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async proveIdentity(ws) {
|
|
204
|
+
const hello = await this.request('fleet.rooms.hello', {}, 'fleet.rooms.challenge');
|
|
205
|
+
const challenge = typeof hello.challenge === 'string' ? hello.challenge : '';
|
|
206
|
+
// The audience comes from the URL this client dialed, never from the hub.
|
|
207
|
+
const proof = signRoomEvent(buildRoomAuth(challenge, this.audience), this.options.secretKey);
|
|
208
|
+
const result = await this.request('fleet.rooms.auth', { event: proof }, 'fleet.rooms.auth');
|
|
209
|
+
if (result.ok !== true) {
|
|
210
|
+
throw new RoomClientError(String(result.message ?? 'room authentication refused'), 'ROOM_AUTH_REFUSED');
|
|
211
|
+
}
|
|
212
|
+
if (this.ws !== ws)
|
|
213
|
+
throw new RoomClientError('connection replaced during authentication', 'DISCONNECTED');
|
|
214
|
+
const info = {
|
|
215
|
+
pubkey: String(result.pubkey),
|
|
216
|
+
name: String(result.name),
|
|
217
|
+
rooms: Array.isArray(result.rooms) ? result.rooms : [],
|
|
218
|
+
};
|
|
219
|
+
if (info.pubkey !== this.pubkey) {
|
|
220
|
+
throw new RoomClientError('room hub returned another member identity', 'ROOM_AUTH_REFUSED');
|
|
221
|
+
}
|
|
222
|
+
this.ready = true;
|
|
223
|
+
this.everConnected = true;
|
|
224
|
+
this.connectionInfo = info;
|
|
225
|
+
this.reconnectAttempts = 0;
|
|
226
|
+
for (const waiter of this.readyWaiters.splice(0))
|
|
227
|
+
waiter.resolve();
|
|
228
|
+
for (const sub of this.subscriptions.values()) {
|
|
229
|
+
if (!sub.closed)
|
|
230
|
+
this.sendSubscribe(sub);
|
|
231
|
+
}
|
|
232
|
+
this.emit('ready');
|
|
233
|
+
return info;
|
|
234
|
+
}
|
|
235
|
+
sendRaw(message) {
|
|
236
|
+
const ws = this.ws;
|
|
237
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
238
|
+
return false;
|
|
239
|
+
ws.send(JSON.stringify(message));
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
request(type, payload, expect) {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const id = `room-${Date.now().toString(36)}-${(++this.requestCounter).toString(36)}`;
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
this.pending.delete(id);
|
|
247
|
+
reject(new RoomClientError(`${type} timed out`, 'TIMEOUT'));
|
|
248
|
+
}, this.requestTimeoutMs);
|
|
249
|
+
this.pending.set(id, { expect, resolve, reject, timer });
|
|
250
|
+
if (!this.sendRaw({ type, id, payload })) {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
this.pending.delete(id);
|
|
253
|
+
reject(new RoomClientError('not connected', 'DISCONNECTED'));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
handleMessage(message) {
|
|
258
|
+
const payload = message.payload && typeof message.payload === 'object'
|
|
259
|
+
? message.payload
|
|
260
|
+
: {};
|
|
261
|
+
if (message.requestId && this.pending.has(message.requestId)) {
|
|
262
|
+
const pending = this.pending.get(message.requestId);
|
|
263
|
+
this.pending.delete(message.requestId);
|
|
264
|
+
clearTimeout(pending.timer);
|
|
265
|
+
if (message.type === pending.expect)
|
|
266
|
+
pending.resolve(payload);
|
|
267
|
+
else
|
|
268
|
+
pending.reject(new RoomClientError(String(payload.message ?? `unexpected ${message.type}`), String(payload.code ?? 'ERROR')));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const subId = typeof payload.subId === 'string' ? payload.subId : undefined;
|
|
272
|
+
const sub = subId ? this.subscriptions.get(subId) : undefined;
|
|
273
|
+
if (!sub || sub.closed)
|
|
274
|
+
return;
|
|
275
|
+
if (message.type === 'fleet.rooms.event')
|
|
276
|
+
this.onEvent(sub, payload);
|
|
277
|
+
else if (message.type === 'fleet.rooms.eose')
|
|
278
|
+
this.onEose(sub, payload);
|
|
279
|
+
else if (message.type === 'fleet.rooms.closed')
|
|
280
|
+
this.onClosed(sub, payload);
|
|
281
|
+
}
|
|
282
|
+
onEvent(sub, payload) {
|
|
283
|
+
const seq = payload.seq;
|
|
284
|
+
if (!Number.isSafeInteger(seq) || seq <= 0)
|
|
285
|
+
return;
|
|
286
|
+
const shape = checkRoomEventShape(payload.event);
|
|
287
|
+
const room = shape.ok ? roomOf(shape.event) : undefined;
|
|
288
|
+
if (!shape.ok || !room || !verifyRoomEvent(shape.event).ok || !roomFiltersMatch(sub.filters, shape.event)) {
|
|
289
|
+
this.emit('invalid-event', { subId: sub.subId, seq });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
sub.maxSeenSeq = Math.max(sub.maxSeenSeq ?? 0, seq);
|
|
293
|
+
// While replaying, the old cursor is provisional until EOSE tells us which
|
|
294
|
+
// ledger generation produced these records.
|
|
295
|
+
if (sub.cursor && !sub.replaying) {
|
|
296
|
+
sub.cursor = { ...sub.cursor, throughSeq: Math.max(sub.cursor.throughSeq, seq) };
|
|
297
|
+
}
|
|
298
|
+
if (!this.seen.add(`${sub.subId.length}:${sub.subId}:${shape.event.id}`))
|
|
299
|
+
return;
|
|
300
|
+
const event = freezeRoomEvent(shape.event);
|
|
301
|
+
const thread = resolveThread(parseThreadMarkers(event.tags));
|
|
302
|
+
try {
|
|
303
|
+
sub.handlers.onMessage({
|
|
304
|
+
seq: seq,
|
|
305
|
+
room,
|
|
306
|
+
author: event.pubkey,
|
|
307
|
+
event,
|
|
308
|
+
mentions: tagValues(event, 'p'),
|
|
309
|
+
...(thread ? { thread } : {}),
|
|
310
|
+
text: sanitizePeerText(event.content),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
this.emit('handler-error', error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
onEose(sub, payload) {
|
|
318
|
+
const valid = isStoreId(payload.storeId) &&
|
|
319
|
+
Number.isSafeInteger(payload.throughSeq) && payload.throughSeq >= 0 &&
|
|
320
|
+
typeof payload.live === 'boolean' && typeof payload.truncated === 'boolean' &&
|
|
321
|
+
payload.truncated === !payload.live && typeof payload.gap === 'boolean' &&
|
|
322
|
+
typeof payload.epochChanged === 'boolean';
|
|
323
|
+
if (!valid) {
|
|
324
|
+
this.closeSubscriptionLocally(sub, 'invalid: malformed end-of-stored-events frame');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const storeId = payload.storeId;
|
|
328
|
+
const throughSeq = payload.throughSeq;
|
|
329
|
+
const replayCursor = sub.replayCursor;
|
|
330
|
+
const epochChanged = payload.epochChanged === true ||
|
|
331
|
+
(replayCursor !== undefined && replayCursor.storeId !== storeId);
|
|
332
|
+
if ((sub.maxSeenSeq ?? 0) > throughSeq) {
|
|
333
|
+
this.closeSubscriptionLocally(sub, 'invalid: EOSE cursor precedes a delivered event');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const replayFloor = replayCursor?.storeId === storeId ? replayCursor.throughSeq : 0;
|
|
337
|
+
if (payload.live === false && throughSeq <= replayFloor) {
|
|
338
|
+
this.closeSubscriptionLocally(sub, 'invalid: truncated replay made no cursor progress');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const previousThroughSeq = !epochChanged && replayCursor?.storeId === storeId
|
|
342
|
+
? replayCursor.throughSeq
|
|
343
|
+
: 0;
|
|
344
|
+
sub.cursor = { storeId, throughSeq: Math.max(previousThroughSeq, throughSeq, sub.maxSeenSeq ?? 0) };
|
|
345
|
+
sub.replaying = false;
|
|
346
|
+
sub.replayCursor = undefined;
|
|
347
|
+
sub.maxSeenSeq = sub.cursor.throughSeq;
|
|
348
|
+
sub.backpressureRetries = 0;
|
|
349
|
+
sub.replayGap ||= payload.gap;
|
|
350
|
+
sub.replayEpochChanged ||= epochChanged;
|
|
351
|
+
if (payload.live === false && sub.autoPage) {
|
|
352
|
+
// Truncated replay: page forward from the cursor.
|
|
353
|
+
this.sendSubscribe(sub, true);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.callHandler(() => sub.handlers.onEose?.({
|
|
357
|
+
storeId,
|
|
358
|
+
throughSeq: sub.cursor?.throughSeq ?? throughSeq,
|
|
359
|
+
gap: sub.replayGap,
|
|
360
|
+
epochChanged: sub.replayEpochChanged,
|
|
361
|
+
truncated: payload.truncated,
|
|
362
|
+
}));
|
|
363
|
+
sub.replayGap = false;
|
|
364
|
+
sub.replayEpochChanged = false;
|
|
365
|
+
}
|
|
366
|
+
onClosed(sub, payload) {
|
|
367
|
+
const message = typeof payload.message === 'string' ? payload.message : 'closed';
|
|
368
|
+
if (message.startsWith('error: backpressure') && sub.backpressureRetries < 5) {
|
|
369
|
+
sub.backpressureRetries++;
|
|
370
|
+
if (isStoreId(payload.storeId) && Number.isSafeInteger(payload.throughSeq) &&
|
|
371
|
+
payload.throughSeq >= 0) {
|
|
372
|
+
const sameGeneration = sub.cursor?.storeId === payload.storeId;
|
|
373
|
+
sub.cursor = {
|
|
374
|
+
storeId: payload.storeId,
|
|
375
|
+
throughSeq: sameGeneration
|
|
376
|
+
? Math.max(payload.throughSeq, sub.maxSeenSeq ?? 0)
|
|
377
|
+
: payload.throughSeq,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const retry = setTimeout(() => {
|
|
381
|
+
sub.retryTimers.delete(retry);
|
|
382
|
+
if (!sub.closed && this.ready)
|
|
383
|
+
this.sendSubscribe(sub);
|
|
384
|
+
}, 50 * sub.backpressureRetries);
|
|
385
|
+
sub.retryTimers.add(retry);
|
|
386
|
+
retry.unref?.();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.closeSubscriptionLocally(sub, message);
|
|
390
|
+
}
|
|
391
|
+
closeSubscriptionLocally(sub, message) {
|
|
392
|
+
if (sub.closed)
|
|
393
|
+
return;
|
|
394
|
+
sub.closed = true;
|
|
395
|
+
for (const timer of sub.retryTimers)
|
|
396
|
+
clearTimeout(timer);
|
|
397
|
+
sub.retryTimers.clear();
|
|
398
|
+
this.subscriptions.delete(sub.subId);
|
|
399
|
+
this.callHandler(() => sub.handlers.onClosed?.(message));
|
|
400
|
+
}
|
|
401
|
+
callHandler(callback) {
|
|
402
|
+
try {
|
|
403
|
+
callback();
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
this.emit('handler-error', error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
sendSubscribe(sub, continuingPage = false) {
|
|
410
|
+
if (!continuingPage) {
|
|
411
|
+
sub.replayGap = false;
|
|
412
|
+
sub.replayEpochChanged = false;
|
|
413
|
+
}
|
|
414
|
+
const cursor = sub.cursor;
|
|
415
|
+
sub.replayCursor = cursor ? { ...cursor } : undefined;
|
|
416
|
+
sub.replaying = true;
|
|
417
|
+
sub.maxSeenSeq = undefined;
|
|
418
|
+
this.sendRaw({
|
|
419
|
+
type: 'fleet.rooms.subscribe',
|
|
420
|
+
payload: {
|
|
421
|
+
subId: sub.subId,
|
|
422
|
+
filters: sub.filters,
|
|
423
|
+
...(cursor ? { afterSeq: cursor.throughSeq, storeId: cursor.storeId } : {}),
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
waitUntilReady(timeoutMs) {
|
|
428
|
+
if (this.ready)
|
|
429
|
+
return Promise.resolve();
|
|
430
|
+
if (this.closing)
|
|
431
|
+
return Promise.reject(new RoomClientError('client closed', 'CLOSED'));
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const waiter = {
|
|
434
|
+
resolve: () => { clearTimeout(timer); resolve(); },
|
|
435
|
+
reject: (error) => { clearTimeout(timer); reject(error); },
|
|
436
|
+
};
|
|
437
|
+
const timer = setTimeout(() => {
|
|
438
|
+
this.readyWaiters = this.readyWaiters.filter((entry) => entry !== waiter);
|
|
439
|
+
reject(new RoomClientError('not reconnected in time', 'DISCONNECTED'));
|
|
440
|
+
}, timeoutMs);
|
|
441
|
+
this.readyWaiters.push(waiter);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Sign and publish. Resolves once the hub has durably stored the message
|
|
446
|
+
* (or already had it). A connection lost before the acknowledgement is
|
|
447
|
+
* retried once with the SAME signed event, which the hub deduplicates.
|
|
448
|
+
*/
|
|
449
|
+
async publish(input) {
|
|
450
|
+
const event = signRoomEvent(buildRoomMessage(input, this.pubkey), this.options.secretKey);
|
|
451
|
+
return this.publishSigned(event);
|
|
452
|
+
}
|
|
453
|
+
async publishSigned(event) {
|
|
454
|
+
for (let attempt = 0;; attempt++) {
|
|
455
|
+
try {
|
|
456
|
+
if (!this.ready)
|
|
457
|
+
await this.waitUntilReady(this.requestTimeoutMs);
|
|
458
|
+
const ack = await this.request('fleet.rooms.publish', { event }, 'fleet.rooms.ok');
|
|
459
|
+
if (ack.accepted !== true || ack.id !== event.id ||
|
|
460
|
+
!Number.isSafeInteger(ack.seq) || ack.seq <= 0 || !isStoreId(ack.storeId)) {
|
|
461
|
+
throw new RoomClientError(String(ack.message || 'publication refused'), 'REJECTED');
|
|
462
|
+
}
|
|
463
|
+
const message = String(ack.message ?? '');
|
|
464
|
+
return {
|
|
465
|
+
id: event.id,
|
|
466
|
+
seq: ack.seq,
|
|
467
|
+
duplicate: message.startsWith('duplicate:'),
|
|
468
|
+
storeId: ack.storeId,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const retryable = error instanceof RoomClientError && error.code === 'DISCONNECTED';
|
|
473
|
+
if (!retryable || attempt >= 1 || this.options.autoReconnect === false)
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/** Open a subscription; it survives reconnections until closed. */
|
|
479
|
+
subscribe(subId, filters, handlers, cursor) {
|
|
480
|
+
return this.openSubscription(subId, filters, handlers, cursor, true);
|
|
481
|
+
}
|
|
482
|
+
openSubscription(subId, filters, handlers, cursor, autoPage) {
|
|
483
|
+
if (!/^[A-Za-z0-9._:-]{1,64}$/.test(subId)) {
|
|
484
|
+
throw new RoomClientError('subId must match ^[A-Za-z0-9._:-]{1,64}$', 'INVALID_SUB');
|
|
485
|
+
}
|
|
486
|
+
if (this.subscriptions.has(subId))
|
|
487
|
+
throw new RoomClientError(`subscription ${subId} already open`, 'DUPLICATE_SUB');
|
|
488
|
+
const parsed = parseRoomFilters(filters);
|
|
489
|
+
if (!parsed.ok)
|
|
490
|
+
throw new RoomClientError(`invalid filters: ${parsed.reason}`, 'INVALID_FILTER');
|
|
491
|
+
if (cursor && (!isStoreId(cursor.storeId) || !Number.isSafeInteger(cursor.throughSeq) || cursor.throughSeq < 0)) {
|
|
492
|
+
throw new RoomClientError('cursor must contain a valid storeId and non-negative throughSeq', 'INVALID_CURSOR');
|
|
493
|
+
}
|
|
494
|
+
const sub = {
|
|
495
|
+
subId,
|
|
496
|
+
filters: parsed.filters,
|
|
497
|
+
handlers,
|
|
498
|
+
...(cursor ? { cursor: { ...cursor } } : {}),
|
|
499
|
+
replaying: false,
|
|
500
|
+
autoPage,
|
|
501
|
+
replayGap: false,
|
|
502
|
+
replayEpochChanged: false,
|
|
503
|
+
closed: false,
|
|
504
|
+
backpressureRetries: 0,
|
|
505
|
+
retryTimers: new Set(),
|
|
506
|
+
};
|
|
507
|
+
this.subscriptions.set(subId, sub);
|
|
508
|
+
if (this.ready)
|
|
509
|
+
this.sendSubscribe(sub);
|
|
510
|
+
return {
|
|
511
|
+
subId,
|
|
512
|
+
cursor: () => (sub.cursor ? { ...sub.cursor } : undefined),
|
|
513
|
+
close: async () => {
|
|
514
|
+
if (sub.closed)
|
|
515
|
+
return;
|
|
516
|
+
sub.closed = true;
|
|
517
|
+
for (const timer of sub.retryTimers)
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
sub.retryTimers.clear();
|
|
520
|
+
this.subscriptions.delete(subId);
|
|
521
|
+
if (this.ready) {
|
|
522
|
+
await this.request('fleet.rooms.close', { subId }, 'fleet.rooms.unsubscribed').catch(() => undefined);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* One-shot read: stored messages matching `filters` (or after `cursor`),
|
|
529
|
+
* returned with the cursor to resume from next time.
|
|
530
|
+
*/
|
|
531
|
+
async fetch(filters, cursor, options = {}) {
|
|
532
|
+
const maxMessages = boundedPositiveInteger('maxMessages', options.maxMessages, DEFAULT_FETCH_MAX_MESSAGES, MAX_FETCH_MESSAGES);
|
|
533
|
+
const maxBytes = boundedPositiveInteger('maxBytes', options.maxBytes, DEFAULT_FETCH_MAX_BYTES, MAX_FETCH_BYTES);
|
|
534
|
+
const subId = `fetch-${Date.now().toString(36)}-${(++this.requestCounter).toString(36)}`;
|
|
535
|
+
const messages = [];
|
|
536
|
+
let retainedBytes = 0;
|
|
537
|
+
let pageTruncated = false;
|
|
538
|
+
let lastRetainedSeq;
|
|
539
|
+
return new Promise((resolve, reject) => {
|
|
540
|
+
let settled = false;
|
|
541
|
+
let handle;
|
|
542
|
+
const fail = (error) => {
|
|
543
|
+
if (settled)
|
|
544
|
+
return;
|
|
545
|
+
settled = true;
|
|
546
|
+
clearTimeout(timer);
|
|
547
|
+
void handle?.close();
|
|
548
|
+
reject(error);
|
|
549
|
+
};
|
|
550
|
+
const timer = setTimeout(() => {
|
|
551
|
+
fail(new RoomClientError('fetch timed out', 'TIMEOUT'));
|
|
552
|
+
}, this.requestTimeoutMs * 3);
|
|
553
|
+
try {
|
|
554
|
+
handle = this.openSubscription(subId, filters, {
|
|
555
|
+
onMessage: (message) => {
|
|
556
|
+
if (pageTruncated)
|
|
557
|
+
return;
|
|
558
|
+
const messageBytes = Buffer.byteLength(JSON.stringify(message), 'utf8');
|
|
559
|
+
if (messages.length >= maxMessages || retainedBytes + messageBytes > maxBytes) {
|
|
560
|
+
if (messages.length === 0) {
|
|
561
|
+
fail(new RoomClientError('one room message exceeds the configured fetch byte limit', 'LIMIT_EXCEEDED'));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
pageTruncated = true;
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
retainedBytes += messageBytes;
|
|
568
|
+
messages.push(message);
|
|
569
|
+
lastRetainedSeq = message.seq;
|
|
570
|
+
},
|
|
571
|
+
onEose: (info) => {
|
|
572
|
+
if (settled)
|
|
573
|
+
return;
|
|
574
|
+
settled = true;
|
|
575
|
+
clearTimeout(timer);
|
|
576
|
+
void handle?.close();
|
|
577
|
+
const throughSeq = pageTruncated && lastRetainedSeq !== undefined
|
|
578
|
+
? lastRetainedSeq
|
|
579
|
+
: info.throughSeq;
|
|
580
|
+
resolve({
|
|
581
|
+
messages,
|
|
582
|
+
cursor: { storeId: info.storeId, throughSeq },
|
|
583
|
+
gap: info.gap,
|
|
584
|
+
epochChanged: info.epochChanged,
|
|
585
|
+
truncated: pageTruncated || info.truncated,
|
|
586
|
+
});
|
|
587
|
+
},
|
|
588
|
+
onClosed: (message) => {
|
|
589
|
+
fail(new RoomClientError(message, 'CLOSED'));
|
|
590
|
+
},
|
|
591
|
+
}, cursor, false);
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
fail(error instanceof RoomClientError
|
|
595
|
+
? error
|
|
596
|
+
: new RoomClientError(error instanceof Error ? error.message : String(error), 'INVALID_SUB'));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
onSocketClosed() {
|
|
601
|
+
const wasReady = this.ready;
|
|
602
|
+
this.ready = false;
|
|
603
|
+
this.ws = null;
|
|
604
|
+
for (const [id, pending] of this.pending) {
|
|
605
|
+
clearTimeout(pending.timer);
|
|
606
|
+
pending.reject(new RoomClientError('connection closed', 'DISCONNECTED'));
|
|
607
|
+
this.pending.delete(id);
|
|
608
|
+
}
|
|
609
|
+
if (wasReady)
|
|
610
|
+
this.emit('disconnected');
|
|
611
|
+
if (this.closing || this.options.autoReconnect === false || !this.everConnected) {
|
|
612
|
+
for (const waiter of this.readyWaiters.splice(0)) {
|
|
613
|
+
waiter.reject(new RoomClientError('connection closed', 'DISCONNECTED'));
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.scheduleReconnect();
|
|
618
|
+
}
|
|
619
|
+
scheduleReconnect() {
|
|
620
|
+
if (this.reconnectTimer || this.closing)
|
|
621
|
+
return;
|
|
622
|
+
if (this.reconnectAttempts >= this.reconnectMaxAttempts) {
|
|
623
|
+
for (const waiter of this.readyWaiters.splice(0)) {
|
|
624
|
+
waiter.reject(new RoomClientError('reconnection attempts exhausted', 'DISCONNECTED'));
|
|
625
|
+
}
|
|
626
|
+
this.emit('reconnect-exhausted');
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const base = Math.min(this.reconnectMaxDelayMs, this.reconnectInitialDelayMs * 2 ** this.reconnectAttempts);
|
|
630
|
+
const delay = Math.floor(base / 2 + Math.random() * (base / 2));
|
|
631
|
+
this.reconnectAttempts++;
|
|
632
|
+
this.reconnectTimer = setTimeout(() => {
|
|
633
|
+
this.reconnectTimer = null;
|
|
634
|
+
if (this.closing)
|
|
635
|
+
return;
|
|
636
|
+
this.startOpen().catch((error) => {
|
|
637
|
+
this.emit('reconnect-error', error);
|
|
638
|
+
if (error instanceof RoomClientError && error.code === 'ROOM_AUTH_REFUSED') {
|
|
639
|
+
this.closing = true;
|
|
640
|
+
for (const waiter of this.readyWaiters.splice(0))
|
|
641
|
+
waiter.reject(error);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}, delay);
|
|
645
|
+
this.reconnectTimer.unref?.();
|
|
646
|
+
}
|
|
647
|
+
async close() {
|
|
648
|
+
this.closing = true;
|
|
649
|
+
if (this.reconnectTimer) {
|
|
650
|
+
clearTimeout(this.reconnectTimer);
|
|
651
|
+
this.reconnectTimer = null;
|
|
652
|
+
}
|
|
653
|
+
for (const waiter of this.readyWaiters.splice(0))
|
|
654
|
+
waiter.reject(new RoomClientError('client closed', 'CLOSED'));
|
|
655
|
+
for (const sub of [...this.subscriptions.values()])
|
|
656
|
+
this.closeSubscriptionLocally(sub, 'client closed');
|
|
657
|
+
for (const [id, pending] of this.pending) {
|
|
658
|
+
clearTimeout(pending.timer);
|
|
659
|
+
pending.reject(new RoomClientError('client closed', 'CLOSED'));
|
|
660
|
+
this.pending.delete(id);
|
|
661
|
+
}
|
|
662
|
+
const ws = this.ws;
|
|
663
|
+
if (!ws || ws.readyState === WebSocket.CLOSED)
|
|
664
|
+
return;
|
|
665
|
+
await new Promise((resolve) => {
|
|
666
|
+
ws.once('close', () => resolve());
|
|
667
|
+
try {
|
|
668
|
+
ws.close();
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
resolve();
|
|
672
|
+
}
|
|
673
|
+
setTimeout(() => {
|
|
674
|
+
try {
|
|
675
|
+
ws.terminate();
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
// ignore
|
|
679
|
+
}
|
|
680
|
+
resolve();
|
|
681
|
+
}, 1_000).unref?.();
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
//# sourceMappingURL=room-client.js.map
|