@vellumai/assistant 0.12.1 → 0.12.2-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +20 -21
- package/docs/architecture/turn-actor.md +15 -8
- package/docs/guardian-request-flow.md +16 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +360 -51
- package/package.json +3 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/anthropic-provider.test.ts +0 -63
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
- package/src/__tests__/assistant-event-hub.test.ts +44 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +19 -7
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +250 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/cu-unified-flow.test.ts +14 -4
- package/src/__tests__/document-sync-tags.test.ts +25 -0
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/heartbeat-service.test.ts +56 -40
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-cu-proxy.test.ts +596 -0
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
- package/src/__tests__/oauth-commands-routes.test.ts +168 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/openai-provider.test.ts +1 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-callback-registration.test.ts +8 -16
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
- package/src/__tests__/qdrant-manager.test.ts +132 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/service-account-token.test.ts +247 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/unicode.test.ts +13 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +96 -10
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/agent/loop.ts +1 -0
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/events/notification-intent.ts +2 -0
- package/src/api/events/user-message-echo.ts +7 -1
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/avatar/identity-avatar.ts +7 -2
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +155 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +92 -72
- package/src/calls/voice-triage-escalate.ts +178 -59
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/bash.test.ts +53 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/bash.help.ts +8 -13
- package/src/cli/commands/bash.ts +7 -25
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +255 -0
- package/src/config/__tests__/platform-rehydration.test.ts +99 -9
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
- package/src/config/bundled-skills/schedule/SKILL.md +3 -4
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +33 -13
- package/src/config/loader.ts +2 -0
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +201 -0
- package/src/config/platform-rehydration.ts +11 -27
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/credential-health/credential-health-service.ts +11 -17
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-error.ts +3 -2
- package/src/daemon/conversation-media-retry.ts +2 -1
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-runtime-assembly.ts +20 -7
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +21 -10
- package/src/daemon/conversation-tool-setup.ts +45 -59
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/dictation-profile-store.ts +2 -1
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +6 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-cu-proxy.ts +224 -19
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-proxy-preactivation.ts +2 -1
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/orphan-reaper.ts +1 -1
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/reaction-history-render.ts +2 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +122 -0
- package/src/desktop/desktop-chrome-policy.test.ts +79 -0
- package/src/desktop/desktop-chrome-policy.ts +47 -0
- package/src/desktop/desktop-chrome-session.ts +37 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +308 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +730 -0
- package/src/desktop/desktop-session-manager.ts +850 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-drag.ts +177 -0
- package/src/desktop/desktop-window-manager-config.test.ts +230 -0
- package/src/desktop/desktop-window-manager-config.ts +238 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/heartbeat/heartbeat-service.ts +21 -3
- package/src/home/__tests__/feed-writer.test.ts +22 -0
- package/src/home/feed-writer.ts +11 -2
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/protocol.test.ts +39 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/__tests__/session-controls.test.ts +79 -0
- package/src/live-voice/live-voice-photo.ts +25 -6
- package/src/live-voice/live-voice-session.ts +297 -699
- package/src/live-voice/protocol.ts +60 -2
- package/src/live-voice/session-controls.ts +111 -0
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/guardian-feed-projection.ts +125 -5
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/notifications/schedule-result-producer.ts +66 -22
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
- package/src/oauth/byo-connection.test.ts +105 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +100 -0
- package/src/oauth/connection-resolver.ts +120 -3
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/scope-utils.ts +24 -0
- package/src/oauth/seed-providers.ts +314 -27
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-queries.ts +2 -2
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/embeddings/qdrant-manager.ts +163 -18
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +4 -2
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
- package/src/plugins/defaults/memory/context-search/format.ts +3 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +28 -2
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
- package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
- package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
- package/src/plugins/defaults/memory/v3/card.ts +3 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
- package/src/plugins/defaults/memory/v3/sections.ts +9 -3
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/__tests__/retry-callsite.test.ts +41 -0
- package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
- package/src/providers/anthropic/client.ts +0 -47
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
- package/src/providers/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/endpoint-probe.ts +10 -1
- package/src/providers/inference/resolve-auth.ts +22 -4
- package/src/providers/inference/service-account-token.ts +248 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +212 -79
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/providers/opencode/client.test.ts +73 -4
- package/src/providers/opencode/client.ts +60 -3
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/outbound-request-sanitize.ts +96 -0
- package/src/providers/retry.ts +30 -19
- package/src/providers/unparseable-tool-args.ts +5 -1
- package/src/runtime/AGENTS.md +3 -1
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/host-observe.test.ts +40 -1
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/agent-wake.ts +2 -1
- package/src/runtime/assistant-event-hub.ts +21 -3
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/host-observe.ts +11 -1
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +90 -2
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/conversation-routes.ts +16 -3
- package/src/runtime/routes/credential-routes.ts +20 -14
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +61 -4
- package/src/runtime/routes/events-routes.ts +4 -0
- package/src/runtime/routes/host-cu-routes.ts +6 -0
- package/src/runtime/routes/index.ts +4 -2
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +6 -20
- package/src/runtime/routes/oauth-commands-routes.ts +50 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/routes/workspace-routes.test.ts +55 -26
- package/src/runtime/routes/workspace-routes.ts +46 -63
- package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
- package/src/runtime/routes/workspace-tree-walk.ts +240 -0
- package/src/runtime/routes/workspace-utils.ts +17 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/run-script.ts +2 -1
- package/src/schedule/schedule-store.ts +9 -2
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/security/untrusted-content.ts +3 -1
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
- package/src/tools/browser/network-recorder.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +121 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/filesystem/search.ts +2 -1
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/network/web-search.ts +6 -1
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/skills/scaffold-managed.ts +61 -3
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/types/host-capabilities.ts +5 -3
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/util/unicode.ts +15 -1
- package/src/watch/watch-retro.ts +6 -8
- package/src/watch/watch-timeline.ts +2 -2
- package/src/workflows/run-manager.ts +2 -1
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +6 -55
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/runtime/routes/debug-bash-routes.ts +0 -173
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
|
@@ -30,24 +30,15 @@ import {
|
|
|
30
30
|
} from "./errors.js";
|
|
31
31
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
32
32
|
import { RouteResponse } from "./types.js";
|
|
33
|
+
import { walkWorkspaceTree } from "./workspace-tree-walk.js";
|
|
33
34
|
import {
|
|
34
35
|
isTextMimeType,
|
|
35
36
|
MAX_INLINE_TEXT_SIZE,
|
|
36
37
|
resolveWorkspacePath,
|
|
38
|
+
workspaceEntrySchema,
|
|
37
39
|
writeWorkspaceFile,
|
|
38
40
|
} from "./workspace-utils.js";
|
|
39
41
|
|
|
40
|
-
const workspaceTreeEntrySchema = z.object({
|
|
41
|
-
name: z.string(),
|
|
42
|
-
path: z.string(),
|
|
43
|
-
type: z.enum(["file", "directory"]),
|
|
44
|
-
size: z.number().nullable(),
|
|
45
|
-
mimeType: z.string().nullable(),
|
|
46
|
-
modifiedAt: z.string(),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
type TreeEntry = z.infer<typeof workspaceTreeEntrySchema>;
|
|
50
|
-
|
|
51
42
|
// Recursive directory sizing walks the filesystem synchronously, so it is
|
|
52
43
|
// bounded by two caps that work together:
|
|
53
44
|
//
|
|
@@ -175,10 +166,11 @@ function publishSoundsConfigUpdatedForPaths(
|
|
|
175
166
|
// GET /v1/workspace/tree — list directory contents
|
|
176
167
|
// ---------------------------------------------------------------------------
|
|
177
168
|
|
|
178
|
-
function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
|
|
169
|
+
async function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
|
|
179
170
|
const requestedPath = queryParams?.path ?? "";
|
|
180
171
|
const showHidden = queryParams?.showHidden === "true";
|
|
181
172
|
const includeDirSizes = queryParams?.includeDirSizes === "true";
|
|
173
|
+
const recursive = queryParams?.recursive === "true";
|
|
182
174
|
const resolved = resolveWorkspacePath(requestedPath, {
|
|
183
175
|
allowHidden: showHidden,
|
|
184
176
|
});
|
|
@@ -186,62 +178,36 @@ function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
|
|
|
186
178
|
throw new BadRequestError("Invalid path");
|
|
187
179
|
}
|
|
188
180
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// subtree can't starve its siblings, while the shared remainder still caps
|
|
196
|
-
// total synchronous traversal for the whole listing.
|
|
197
|
-
const listingBudget: DirSizeBudget | undefined = includeDirSizes
|
|
181
|
+
// Listing-wide budget, only constructed when sizes are requested. Each
|
|
182
|
+
// directory draws a per-directory allotment from it so one oversized
|
|
183
|
+
// subtree can't starve its siblings, while the shared remainder still caps
|
|
184
|
+
// total synchronous traversal for the whole listing.
|
|
185
|
+
const listingBudget: DirSizeBudget | undefined =
|
|
186
|
+
includeDirSizes && !recursive
|
|
198
187
|
? { remaining: DIR_SIZE_TOTAL_ENTRY_BUDGET }
|
|
199
188
|
: undefined;
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
stats = statSync(fullPath);
|
|
212
|
-
} catch {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const isDir = stats.isDirectory();
|
|
217
|
-
const relativePath = fullPath.slice(workspaceDir.length + 1);
|
|
218
|
-
|
|
219
|
-
const dirSize =
|
|
220
|
-
isDir && listingBudget
|
|
221
|
-
? computeDirSizeWithinListing(fullPath, listingBudget)
|
|
222
|
-
: null;
|
|
223
|
-
|
|
224
|
-
entries.push({
|
|
225
|
-
name: entry.name,
|
|
226
|
-
path: relativePath,
|
|
227
|
-
type: isDir ? "directory" : "file",
|
|
228
|
-
size: isDir ? dirSize : stats.size,
|
|
229
|
-
mimeType: isDir ? null : Bun.file(fullPath).type,
|
|
230
|
-
modifiedAt: stats.mtime.toISOString(),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
entries.sort((a, b) => {
|
|
235
|
-
if (a.type !== b.type) {
|
|
236
|
-
return a.type === "directory" ? -1 : 1;
|
|
237
|
-
}
|
|
238
|
-
return a.name.localeCompare(b.name);
|
|
190
|
+
let walk: Awaited<ReturnType<typeof walkWorkspaceTree>>;
|
|
191
|
+
try {
|
|
192
|
+
walk = await walkWorkspaceTree({
|
|
193
|
+
rootPath: resolved,
|
|
194
|
+
workspaceDir: getWorkspaceDir(),
|
|
195
|
+
showHidden,
|
|
196
|
+
recursive,
|
|
197
|
+
directorySize: listingBudget
|
|
198
|
+
? (absPath) => computeDirSizeWithinListing(absPath, listingBudget)
|
|
199
|
+
: undefined,
|
|
239
200
|
});
|
|
240
|
-
|
|
241
|
-
return { path: requestedPath, entries };
|
|
242
201
|
} catch {
|
|
243
202
|
throw new NotFoundError("Directory not found");
|
|
244
203
|
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
path: requestedPath,
|
|
207
|
+
entries: walk.entries,
|
|
208
|
+
truncated: walk.truncated,
|
|
209
|
+
skipped: walk.skipped,
|
|
210
|
+
};
|
|
245
211
|
}
|
|
246
212
|
|
|
247
213
|
// ---------------------------------------------------------------------------
|
|
@@ -569,14 +535,31 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
569
535
|
{
|
|
570
536
|
name: "includeDirSizes",
|
|
571
537
|
description:
|
|
572
|
-
"Compute recursive byte size for each directory entry (true/false). Budget-bounded
|
|
538
|
+
"Compute recursive byte size for each directory entry (true/false). Budget-bounded: large subtrees may return size: null. Not applied to a recursive listing, which carries every file for the client to sum.",
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "recursive",
|
|
542
|
+
description:
|
|
543
|
+
"List the whole subtree in one response (true/false). Depth-first, each directory's entries in listing order, so grouping by parent path recovers per-directory listings. Bounded: sets truncated when the entry cap or deadline stops the walk, and does not enter directories the workspace gitignore rules exclude, which are named in skipped.",
|
|
573
544
|
},
|
|
574
545
|
],
|
|
575
546
|
responseBody: z.object({
|
|
576
547
|
path: z.string(),
|
|
577
548
|
entries: z
|
|
578
|
-
.array(
|
|
549
|
+
.array(workspaceEntrySchema)
|
|
579
550
|
.describe("Directory entry objects"),
|
|
551
|
+
truncated: z
|
|
552
|
+
.boolean()
|
|
553
|
+
.optional()
|
|
554
|
+
.describe(
|
|
555
|
+
"A recursive walk stopped at its entry cap or deadline, so deeper entries are missing. Absent from assistants that predate recursive listings.",
|
|
556
|
+
),
|
|
557
|
+
skipped: z
|
|
558
|
+
.array(z.string())
|
|
559
|
+
.optional()
|
|
560
|
+
.describe(
|
|
561
|
+
"Directories a recursive walk listed but did not enter, whether because a workspace gitignore rule excludes them, they are symlinks, or they could not be read. Each can still be listed on its own. Absent from assistants that predate recursive listings.",
|
|
562
|
+
),
|
|
580
563
|
}),
|
|
581
564
|
handler: handleWorkspaceTree,
|
|
582
565
|
},
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recursive workspace walk: what it enters, what it lists without
|
|
3
|
+
* entering, the order it reports, and the bounds that stop it.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
chmodSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import { walkWorkspaceTree } from "./workspace-tree-walk.js";
|
|
16
|
+
|
|
17
|
+
const workspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
18
|
+
const root = join(workspaceDir, "walk-fixture");
|
|
19
|
+
|
|
20
|
+
function write(relative: string, content = "x") {
|
|
21
|
+
const abs = join(root, relative);
|
|
22
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
23
|
+
writeFileSync(abs, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
mkdirSync(root, { recursive: true });
|
|
28
|
+
write("b-dir/deep/leaf.md");
|
|
29
|
+
write("b-dir/file-in-b.md");
|
|
30
|
+
write("a-dir/file-in-a.md");
|
|
31
|
+
write("top.md");
|
|
32
|
+
write(".secret/inside-hidden.md");
|
|
33
|
+
write("node_modules/dep/index.js");
|
|
34
|
+
// Rules with an inner slash (data/db/) anchor to the workspace root, so
|
|
35
|
+
// these live there rather than under the fixture directory.
|
|
36
|
+
mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
|
|
37
|
+
writeFileSync(join(workspaceDir, "data", "db", "state.sqlite"), "x");
|
|
38
|
+
mkdirSync(join(workspaceDir, "data", "avatar"), { recursive: true });
|
|
39
|
+
writeFileSync(join(workspaceDir, "data", "avatar", "face.png"), "x");
|
|
40
|
+
mkdirSync(join(workspaceDir, "walk-link-target"), { recursive: true });
|
|
41
|
+
writeFileSync(join(workspaceDir, "walk-link-target", "behind.md"), "x");
|
|
42
|
+
try {
|
|
43
|
+
symlinkSync(
|
|
44
|
+
join(workspaceDir, "walk-link-target"),
|
|
45
|
+
join(root, "linked"),
|
|
46
|
+
"dir",
|
|
47
|
+
);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function walk(
|
|
56
|
+
overrides: Partial<Parameters<typeof walkWorkspaceTree>[0]> = {},
|
|
57
|
+
) {
|
|
58
|
+
return walkWorkspaceTree({
|
|
59
|
+
rootPath: root,
|
|
60
|
+
workspaceDir,
|
|
61
|
+
showHidden: false,
|
|
62
|
+
recursive: true,
|
|
63
|
+
...overrides,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("walkWorkspaceTree recursive", () => {
|
|
68
|
+
test("a non-recursive walk lists one level and reports nothing skipped", async () => {
|
|
69
|
+
const result = await walk({ recursive: false });
|
|
70
|
+
expect(result.entries.map((e) => e.path)).not.toContain(
|
|
71
|
+
"walk-fixture/a-dir/file-in-a.md",
|
|
72
|
+
);
|
|
73
|
+
expect(result.entries.map((e) => e.name)).toContain("a-dir");
|
|
74
|
+
expect(result).toMatchObject({ truncated: false, skipped: [] });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("lists every level, depth-first, each directory in listing order", async () => {
|
|
78
|
+
const { entries } = await walk();
|
|
79
|
+
const paths = entries.map((e) => e.path.replace(/^walk-fixture\//, ""));
|
|
80
|
+
const indexOf = (p: string) => paths.indexOf(p);
|
|
81
|
+
expect(indexOf("a-dir/file-in-a.md")).toBeGreaterThan(-1);
|
|
82
|
+
expect(indexOf("b-dir/deep/leaf.md")).toBeGreaterThan(-1);
|
|
83
|
+
// Root listing first (directories before files), then a-dir's contents
|
|
84
|
+
// before b-dir's, and b-dir's subdirectory before its file.
|
|
85
|
+
expect(indexOf("a-dir")).toBeLessThan(indexOf("top.md"));
|
|
86
|
+
expect(indexOf("top.md")).toBeLessThan(indexOf("a-dir/file-in-a.md"));
|
|
87
|
+
expect(indexOf("a-dir/file-in-a.md")).toBeLessThan(indexOf("b-dir/deep"));
|
|
88
|
+
expect(indexOf("b-dir/deep")).toBeLessThan(indexOf("b-dir/file-in-b.md"));
|
|
89
|
+
expect(indexOf("b-dir/file-in-b.md")).toBeLessThan(
|
|
90
|
+
indexOf("b-dir/deep/leaf.md"),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("grouping by parent recovers each directory's own listing", async () => {
|
|
95
|
+
const { entries } = await walk();
|
|
96
|
+
const shallow = await walkWorkspaceTree({
|
|
97
|
+
rootPath: join(root, "b-dir"),
|
|
98
|
+
workspaceDir,
|
|
99
|
+
showHidden: false,
|
|
100
|
+
recursive: false,
|
|
101
|
+
});
|
|
102
|
+
const underB = entries.filter(
|
|
103
|
+
(e) =>
|
|
104
|
+
e.path.startsWith("walk-fixture/b-dir/") &&
|
|
105
|
+
!e.path.slice("walk-fixture/b-dir/".length).includes("/"),
|
|
106
|
+
);
|
|
107
|
+
expect(underB.map((e) => e.path)).toEqual(
|
|
108
|
+
shallow.entries.map((e) => e.path),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("paths use forward slashes", async () => {
|
|
113
|
+
const { entries } = await walk();
|
|
114
|
+
expect(entries.every((e) => !e.path.includes("\\"))).toBe(true);
|
|
115
|
+
expect(entries.map((e) => e.path)).toContain(
|
|
116
|
+
"walk-fixture/b-dir/deep/leaf.md",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("hidden directories are neither listed nor entered unless asked", async () => {
|
|
121
|
+
const hidden = (await walk()).entries.map((e) => e.path);
|
|
122
|
+
expect(hidden).not.toContain("walk-fixture/.secret");
|
|
123
|
+
expect(hidden).not.toContain("walk-fixture/.secret/inside-hidden.md");
|
|
124
|
+
const shown = (await walk({ showHidden: true })).entries.map((e) => e.path);
|
|
125
|
+
expect(shown).toContain("walk-fixture/.secret/inside-hidden.md");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a dependency directory is listed at any depth but not entered", async () => {
|
|
129
|
+
const { entries, skipped } = await walk();
|
|
130
|
+
const paths = entries.map((e) => e.path);
|
|
131
|
+
expect(paths).toContain("walk-fixture/node_modules");
|
|
132
|
+
expect(paths).not.toContain("walk-fixture/node_modules/dep");
|
|
133
|
+
expect(skipped).toContain("walk-fixture/node_modules");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("root-anchored runtime state is listed, not entered, and named", async () => {
|
|
137
|
+
const { entries, skipped } = await walk({ rootPath: workspaceDir });
|
|
138
|
+
const paths = entries.map((e) => e.path);
|
|
139
|
+
expect(paths).toContain("data/db");
|
|
140
|
+
expect(paths).not.toContain("data/db/state.sqlite");
|
|
141
|
+
// A directory the rules do not exclude is entered, and a file the rules
|
|
142
|
+
// match (*.png) is still listed: only descent is decided by them.
|
|
143
|
+
expect(paths).toContain("data/avatar/face.png");
|
|
144
|
+
expect(skipped).toContain("data/db");
|
|
145
|
+
expect(skipped).not.toContain("data/avatar");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("a symlinked directory is listed, not entered, and reported skipped", async () => {
|
|
149
|
+
const { entries, skipped } = await walk();
|
|
150
|
+
const paths = entries.map((e) => e.path);
|
|
151
|
+
expect(paths).toContain("walk-fixture/linked");
|
|
152
|
+
expect(paths).not.toContain("walk-fixture/linked/behind.md");
|
|
153
|
+
expect(skipped).toContain("walk-fixture/linked");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("an unreadable directory is listed, not entered, and reported skipped", async () => {
|
|
157
|
+
// Root bypasses permission bits, so the check proves nothing there.
|
|
158
|
+
if (process.getuid?.() === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const locked = join(root, "locked");
|
|
162
|
+
mkdirSync(locked, { recursive: true });
|
|
163
|
+
writeFileSync(join(locked, "inside.md"), "x");
|
|
164
|
+
chmodSync(locked, 0o000);
|
|
165
|
+
try {
|
|
166
|
+
const { entries, skipped, truncated } = await walk();
|
|
167
|
+
const paths = entries.map((e) => e.path);
|
|
168
|
+
expect(paths).toContain("walk-fixture/locked");
|
|
169
|
+
expect(paths).not.toContain("walk-fixture/locked/inside.md");
|
|
170
|
+
expect(skipped).toContain("walk-fixture/locked");
|
|
171
|
+
expect(truncated).toBe(false);
|
|
172
|
+
} finally {
|
|
173
|
+
chmodSync(locked, 0o755);
|
|
174
|
+
rmSync(locked, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a directory wider than the remaining room is not scanned", async () => {
|
|
179
|
+
// Root (5) leaves one slot at a cap of 6; a-dir (1) fits and b-dir (2)
|
|
180
|
+
// does not. Nothing under b-dir is stat'd: its walk-time stat would
|
|
181
|
+
// otherwise show up as a listed entry.
|
|
182
|
+
const wide = join(root, "wide");
|
|
183
|
+
mkdirSync(wide, { recursive: true });
|
|
184
|
+
for (let i = 0; i < 50; i++) {
|
|
185
|
+
writeFileSync(join(wide, `w${i}.md`), "x");
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const result = await walk({ maxEntries: 7 });
|
|
189
|
+
const paths = result.entries.map((e) => e.path);
|
|
190
|
+
expect(paths).toContain("walk-fixture/wide");
|
|
191
|
+
expect(paths.some((p) => p.startsWith("walk-fixture/wide/"))).toBe(false);
|
|
192
|
+
expect(result.truncated).toBe(true);
|
|
193
|
+
} finally {
|
|
194
|
+
rmSync(wide, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("the deadline is honored inside a directory, not only between them", async () => {
|
|
199
|
+
// A clock that advances one second per read: the deadline is set at 0,
|
|
200
|
+
// the check before the first nested directory reads 1000 and passes,
|
|
201
|
+
// and the first entry inside it reads 2000 and stops the listing. Were
|
|
202
|
+
// the deadline checked only between directories, that one-entry
|
|
203
|
+
// directory would be carried and the walk would stop one step later.
|
|
204
|
+
let reads = 0;
|
|
205
|
+
const result = await walk({ maxWalkMs: 1500, now: () => reads++ * 1000 });
|
|
206
|
+
expect(result.truncated).toBe(true);
|
|
207
|
+
expect(result.entries.length).toBe(5);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("the entry cap stops descent and reports truncated", async () => {
|
|
211
|
+
// The fixture root lists five entries and holds nine in all.
|
|
212
|
+
const result = await walk({ maxEntries: 6 });
|
|
213
|
+
expect(result.entries.length).toBeLessThanOrEqual(6);
|
|
214
|
+
expect(result.entries.length).toBeGreaterThanOrEqual(5);
|
|
215
|
+
expect(result.truncated).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a directory is carried whole or not at all", async () => {
|
|
219
|
+
// Root (5) plus a-dir (1) fit in 6; b-dir's two entries do not, so
|
|
220
|
+
// neither of them appears rather than one of them.
|
|
221
|
+
const { entries } = await walk({ maxEntries: 6 });
|
|
222
|
+
const paths = entries.map((e) => e.path);
|
|
223
|
+
expect(paths).toContain("walk-fixture/a-dir/file-in-a.md");
|
|
224
|
+
expect(paths).not.toContain("walk-fixture/b-dir/deep");
|
|
225
|
+
expect(paths).not.toContain("walk-fixture/b-dir/file-in-b.md");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("the root listing is whole even under a cap smaller than it", async () => {
|
|
229
|
+
const result = await walk({ maxEntries: 1 });
|
|
230
|
+
expect(result.entries.map((e) => e.name)).toContain("top.md");
|
|
231
|
+
expect(result.truncated).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("the deadline stops the walk and reports truncated", async () => {
|
|
235
|
+
const result = await walk({ maxWalkMs: -1 });
|
|
236
|
+
expect(result.truncated).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("directory sizes are null in a recursive walk even when a sizer is given", async () => {
|
|
240
|
+
const { entries } = await walk({ directorySize: () => 42 });
|
|
241
|
+
const dir = entries.find((e) => e.type === "directory");
|
|
242
|
+
expect(dir?.size).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory walk behind GET /v1/workspace/tree.
|
|
3
|
+
*
|
|
4
|
+
* One directory by default. With `recursive`, the whole subtree in one
|
|
5
|
+
* response, so a client can search or expand without a request per folder.
|
|
6
|
+
* The walk yields the event loop between directories and is bounded by an
|
|
7
|
+
* entry cap and a deadline; it does not descend into the runtime-state
|
|
8
|
+
* directories the workspace's own gitignore rules name.
|
|
9
|
+
*/
|
|
10
|
+
import type { Dirent, Stats } from "node:fs";
|
|
11
|
+
import { readdir, stat } from "node:fs/promises";
|
|
12
|
+
import { join, sep } from "node:path";
|
|
13
|
+
|
|
14
|
+
import ignore from "ignore";
|
|
15
|
+
|
|
16
|
+
import { WORKSPACE_GITIGNORE_RULES } from "../../workspace/git-service.js";
|
|
17
|
+
import type { WorkspaceEntry } from "./workspace-utils.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Entries a recursive listing returns before it stops descending and
|
|
21
|
+
* reports `truncated`. The root's own listing is always whole, so the
|
|
22
|
+
* answer is never worse than a non-recursive one.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_RECURSIVE_ENTRIES = 20_000;
|
|
25
|
+
|
|
26
|
+
/** Wall-clock budget for one recursive walk. */
|
|
27
|
+
export const MAX_RECURSIVE_WALK_MS = 3_000;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Directories the recursive walk lists but does not enter: whatever the
|
|
31
|
+
* workspace's own gitignore rules exclude (databases, caches, dependency
|
|
32
|
+
* trees, logs), which is the one maintained statement of what in a
|
|
33
|
+
* workspace is runtime state rather than the user's files. Files those rules
|
|
34
|
+
* match are still listed; only descent is decided here.
|
|
35
|
+
*/
|
|
36
|
+
const descentRules = ignore().add(WORKSPACE_GITIGNORE_RULES);
|
|
37
|
+
|
|
38
|
+
/** git never lists its own metadata directory, and neither does this walk. */
|
|
39
|
+
const GIT_DIR = ".git";
|
|
40
|
+
|
|
41
|
+
function isDescentSkipped(relativeDirPath: string, name: string): boolean {
|
|
42
|
+
return name === GIT_DIR || descentRules.ignores(`${relativeDirPath}/`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WorkspaceTreeWalk {
|
|
46
|
+
entries: WorkspaceEntry[];
|
|
47
|
+
/** The entry cap or the deadline stopped a recursive walk early. */
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Workspace-relative directories the walk listed but did not enter, for
|
|
51
|
+
* any reason: a gitignore rule, a symlink, or a directory that could not
|
|
52
|
+
* be read. A client that wants their contents asks for them one at a time.
|
|
53
|
+
*/
|
|
54
|
+
skipped: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WorkspaceTreeWalkOptions {
|
|
58
|
+
/** Absolute directory to list. */
|
|
59
|
+
rootPath: string;
|
|
60
|
+
/** Absolute workspace root; entry paths are relative to it. */
|
|
61
|
+
workspaceDir: string;
|
|
62
|
+
showHidden: boolean;
|
|
63
|
+
recursive: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Per-directory size for directory entries in a non-recursive listing.
|
|
66
|
+
* Ignored when `recursive`, where directories report `size: null`.
|
|
67
|
+
*/
|
|
68
|
+
directorySize?: (absPath: string) => number | null;
|
|
69
|
+
maxEntries?: number;
|
|
70
|
+
maxWalkMs?: number;
|
|
71
|
+
/** Clock for the deadline. Tests hand in a scripted one. */
|
|
72
|
+
now?: () => number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function byDirectoriesThenName(a: WorkspaceEntry, b: WorkspaceEntry): number {
|
|
76
|
+
if (a.type !== b.type) {
|
|
77
|
+
return a.type === "directory" ? -1 : 1;
|
|
78
|
+
}
|
|
79
|
+
return a.name.localeCompare(b.name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ListedDirectory {
|
|
83
|
+
entries: WorkspaceEntry[];
|
|
84
|
+
/** Directory entries in listing order, with whether each is a symlink. */
|
|
85
|
+
subdirectories: Array<{
|
|
86
|
+
absPath: string;
|
|
87
|
+
relativePath: string;
|
|
88
|
+
name: string;
|
|
89
|
+
isSymlink: boolean;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What a nested directory may cost a recursive walk. The listing stops
|
|
95
|
+
* before any stat work when the directory has more entries than the
|
|
96
|
+
* response has room for, and between stats once the deadline passes, so a
|
|
97
|
+
* single wide or slow directory cannot carry the walk past its bound.
|
|
98
|
+
*/
|
|
99
|
+
interface ListingBudget {
|
|
100
|
+
room: number;
|
|
101
|
+
deadline: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One directory's entries, sorted directories first then by name. Types
|
|
106
|
+
* follow `stat`, so a symlinked directory reads as a directory; the caller
|
|
107
|
+
* decides whether to enter it. Returns `null` when a budget stops the
|
|
108
|
+
* listing, in which case none of it is reported.
|
|
109
|
+
*/
|
|
110
|
+
async function listDirectory(
|
|
111
|
+
absDir: string,
|
|
112
|
+
{
|
|
113
|
+
workspaceDir,
|
|
114
|
+
showHidden,
|
|
115
|
+
directorySize,
|
|
116
|
+
now = Date.now,
|
|
117
|
+
}: WorkspaceTreeWalkOptions,
|
|
118
|
+
budget?: ListingBudget,
|
|
119
|
+
): Promise<ListedDirectory | null> {
|
|
120
|
+
const dirents: Dirent[] = (
|
|
121
|
+
await readdir(absDir, { withFileTypes: true })
|
|
122
|
+
).filter((dirent) => showHidden || !dirent.name.startsWith("."));
|
|
123
|
+
if (budget && dirents.length > budget.room) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const entries: WorkspaceEntry[] = [];
|
|
127
|
+
const subdirectories: ListedDirectory["subdirectories"] = [];
|
|
128
|
+
|
|
129
|
+
for (const dirent of dirents) {
|
|
130
|
+
if (budget && now() > budget.deadline) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const absPath = join(absDir, dirent.name);
|
|
134
|
+
let stats: Stats;
|
|
135
|
+
try {
|
|
136
|
+
stats = await stat(absPath);
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const isDir = stats.isDirectory();
|
|
141
|
+
// Wire paths use "/" on every host; clients key expansion and
|
|
142
|
+
// ancestry on them and never see the daemon's own separator.
|
|
143
|
+
const relativePath = absPath
|
|
144
|
+
.slice(workspaceDir.length + 1)
|
|
145
|
+
.split(sep)
|
|
146
|
+
.join("/");
|
|
147
|
+
entries.push({
|
|
148
|
+
name: dirent.name,
|
|
149
|
+
path: relativePath,
|
|
150
|
+
type: isDir ? "directory" : "file",
|
|
151
|
+
size: isDir ? (directorySize?.(absPath) ?? null) : stats.size,
|
|
152
|
+
mimeType: isDir ? null : Bun.file(absPath).type,
|
|
153
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
154
|
+
});
|
|
155
|
+
if (isDir) {
|
|
156
|
+
subdirectories.push({
|
|
157
|
+
absPath,
|
|
158
|
+
relativePath,
|
|
159
|
+
name: dirent.name,
|
|
160
|
+
isSymlink: dirent.isSymbolicLink(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
entries.sort(byDirectoriesThenName);
|
|
166
|
+
subdirectories.sort((a, b) => a.name.localeCompare(b.name));
|
|
167
|
+
return { entries, subdirectories };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* List `rootPath`. A recursive walk is depth-first with each directory's
|
|
172
|
+
* entries in listing order and never part of a directory, so grouping the
|
|
173
|
+
* result by parent path gives every directory's listing exactly as a
|
|
174
|
+
* non-recursive request would, or nothing for it at all.
|
|
175
|
+
*
|
|
176
|
+
* Symlinked directories are listed but never entered, so a link cannot make
|
|
177
|
+
* the walk cycle or leave the workspace; they are reported in `skipped`, as
|
|
178
|
+
* is a directory found mid-walk that cannot be read. The root's own read
|
|
179
|
+
* error is the caller's to report.
|
|
180
|
+
*/
|
|
181
|
+
export async function walkWorkspaceTree(
|
|
182
|
+
options: WorkspaceTreeWalkOptions,
|
|
183
|
+
): Promise<WorkspaceTreeWalk> {
|
|
184
|
+
const {
|
|
185
|
+
rootPath,
|
|
186
|
+
recursive,
|
|
187
|
+
maxEntries = MAX_RECURSIVE_ENTRIES,
|
|
188
|
+
maxWalkMs = MAX_RECURSIVE_WALK_MS,
|
|
189
|
+
now = Date.now,
|
|
190
|
+
} = options;
|
|
191
|
+
// A recursive listing carries every file, so a client sums directory
|
|
192
|
+
// sizes itself and no per-directory sizer runs.
|
|
193
|
+
const listOptions = recursive
|
|
194
|
+
? { ...options, directorySize: undefined }
|
|
195
|
+
: options;
|
|
196
|
+
// The root is listed without a budget: its listing is always whole.
|
|
197
|
+
const root = (await listDirectory(rootPath, listOptions))!;
|
|
198
|
+
if (!recursive) {
|
|
199
|
+
return { entries: root.entries, truncated: false, skipped: [] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const deadline = now() + maxWalkMs;
|
|
203
|
+
const entries: WorkspaceEntry[] = [...root.entries];
|
|
204
|
+
const skipped: string[] = [];
|
|
205
|
+
// Reversed so the stack pops subdirectories in listing order.
|
|
206
|
+
const stack = root.subdirectories.slice().reverse();
|
|
207
|
+
|
|
208
|
+
while (stack.length > 0) {
|
|
209
|
+
const dir = stack.pop()!;
|
|
210
|
+
if (dir.isSymlink || isDescentSkipped(dir.relativePath, dir.name)) {
|
|
211
|
+
skipped.push(dir.relativePath);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (entries.length >= maxEntries || now() > deadline) {
|
|
215
|
+
return { entries, truncated: true, skipped };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let listed: ListedDirectory | null;
|
|
219
|
+
try {
|
|
220
|
+
listed = await listDirectory(dir.absPath, listOptions, {
|
|
221
|
+
room: maxEntries - entries.length,
|
|
222
|
+
deadline,
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
skipped.push(dir.relativePath);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// A directory is carried whole or not at all, so a client grouping the
|
|
229
|
+
// result by parent never mistakes part of a listing for all of it.
|
|
230
|
+
if (listed === null) {
|
|
231
|
+
return { entries, truncated: true, skipped };
|
|
232
|
+
}
|
|
233
|
+
entries.push(...listed.entries);
|
|
234
|
+
for (let i = listed.subdirectories.length - 1; i >= 0; i--) {
|
|
235
|
+
stack.push(listed.subdirectories[i]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { entries, truncated: false, skipped };
|
|
240
|
+
}
|
|
@@ -8,9 +8,26 @@ import {
|
|
|
8
8
|
} from "node:fs";
|
|
9
9
|
import { dirname, resolve, sep } from "node:path";
|
|
10
10
|
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
11
13
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
12
14
|
import { BadRequestError, ConflictError } from "./errors.js";
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* One workspace entry as the tree endpoint reports it. `size` is null for a
|
|
18
|
+
* directory whose recursive size was not computed.
|
|
19
|
+
*/
|
|
20
|
+
export const workspaceEntrySchema = z.object({
|
|
21
|
+
name: z.string(),
|
|
22
|
+
path: z.string(),
|
|
23
|
+
type: z.enum(["file", "directory"]),
|
|
24
|
+
size: z.number().nullable(),
|
|
25
|
+
mimeType: z.string().nullable(),
|
|
26
|
+
modifiedAt: z.string(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type WorkspaceEntry = z.infer<typeof workspaceEntrySchema>;
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
32
|
* Resolves a user-provided relative path to an absolute path within the workspace.
|
|
16
33
|
* Returns the resolved absolute path, or undefined if the path escapes the workspace root.
|