@vellumai/assistant 0.11.4 → 0.11.5-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 +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -45,7 +45,11 @@
|
|
|
45
45
|
* 6. Verify the run drained the buffer. `runResult.ok` only means the
|
|
46
46
|
* background run completed — the trim itself is delegated to the agent.
|
|
47
47
|
* A run that completes without shrinking the buffer is reported as
|
|
48
|
-
* `invoked` with `noProgress: true` and enqueues no follow-ups.
|
|
48
|
+
* `invoked` with `noProgress: true` and enqueues no follow-ups. The
|
|
49
|
+
* post-run page index is also read for `danglingLinks` (structural
|
|
50
|
+
* references with no target page): reported on the outcome and in the
|
|
51
|
+
* log, and fed into the NEXT pass's prompt as a repair step like
|
|
52
|
+
* `parseFailures`. They never gate follow-ups.
|
|
49
53
|
* 7. On progress, enqueue `memory_v2_reembed` (re-index any pages the agent
|
|
50
54
|
* touched). Tracking touched pages via mtime would be more precise but
|
|
51
55
|
* is fragile across filesystems; the embedder's content-hash cache makes
|
|
@@ -104,6 +108,7 @@ import {
|
|
|
104
108
|
tryAcquireLock,
|
|
105
109
|
} from "./consolidation-lock.js";
|
|
106
110
|
import { getPageIndex, type PageParseFailure } from "./page-index.js";
|
|
111
|
+
import type { DanglingLink } from "./page-links.js";
|
|
107
112
|
import { resolveConsolidationPrompt } from "./prompts/consolidation.js";
|
|
108
113
|
import { resolveSubstrateTuning } from "./tuning.js";
|
|
109
114
|
|
|
@@ -296,8 +301,17 @@ export type ConsolidationOutcome =
|
|
|
296
301
|
* follow-ups were enqueued.
|
|
297
302
|
*/
|
|
298
303
|
noProgress: boolean;
|
|
304
|
+
/**
|
|
305
|
+
* Structural references (`links:`, `[[wikilinks]]`, `edges:`) in the
|
|
306
|
+
* post-run corpus whose target page does not exist; `null` when the
|
|
307
|
+
* post-run index could not be read.
|
|
308
|
+
*/
|
|
309
|
+
danglingLinks: number | null;
|
|
299
310
|
};
|
|
300
311
|
|
|
312
|
+
/** Dangling links named in the post-run warn line before the count takes over. */
|
|
313
|
+
const MAX_LOGGED_DANGLING_LINKS = 20;
|
|
314
|
+
|
|
301
315
|
export async function memoryV2ConsolidateJob(
|
|
302
316
|
_job: MemoryJob,
|
|
303
317
|
config: AssistantConfig,
|
|
@@ -419,16 +433,20 @@ export async function memoryV2ConsolidateJob(
|
|
|
419
433
|
// v2-only install must keep producing `summary:`-bearing fragment pages.
|
|
420
434
|
const memoryV3Live = isMemoryV3Live(config);
|
|
421
435
|
// Pages the index build dropped (malformed frontmatter) or degraded
|
|
422
|
-
// (unterminated fence)
|
|
423
|
-
//
|
|
424
|
-
// fail because the index
|
|
436
|
+
// (unterminated fence), and structural references with no target page,
|
|
437
|
+
// rendered into the prompt's repair steps so the agent fixes them this
|
|
438
|
+
// pass. Best-effort: prompt assembly must never fail because the index
|
|
439
|
+
// build did.
|
|
425
440
|
let parseFailures: PageParseFailure[] = [];
|
|
441
|
+
let danglingLinks: DanglingLink[] = [];
|
|
426
442
|
try {
|
|
427
|
-
|
|
443
|
+
const index = await getPageIndex(getWorkspaceDir());
|
|
444
|
+
parseFailures = index.parseFailures;
|
|
445
|
+
danglingLinks = index.danglingLinks;
|
|
428
446
|
} catch (err) {
|
|
429
447
|
log.warn(
|
|
430
448
|
{ err },
|
|
431
|
-
"consolidation: page-index read failed; omitting the repair
|
|
449
|
+
"consolidation: page-index read failed; omitting the repair sections",
|
|
432
450
|
);
|
|
433
451
|
}
|
|
434
452
|
const prompt = resolveConsolidationPrompt(
|
|
@@ -438,6 +456,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
438
456
|
includeCorePagesSection: memoryV3Live,
|
|
439
457
|
articleShape: memoryV3Live ? "v3" : "v2",
|
|
440
458
|
parseFailures,
|
|
459
|
+
danglingLinks,
|
|
441
460
|
},
|
|
442
461
|
);
|
|
443
462
|
|
|
@@ -496,6 +515,13 @@ export async function memoryV2ConsolidateJob(
|
|
|
496
515
|
const bufferLinesAfter = countBufferLines(bufferPath);
|
|
497
516
|
const noProgress = bufferLinesAfter >= bufferLinesBefore;
|
|
498
517
|
|
|
518
|
+
// The agent's file-tool writes invalidate the page index, so this read
|
|
519
|
+
// sees the post-run corpus.
|
|
520
|
+
const danglingAfter = await readDanglingLinks(
|
|
521
|
+
runResult.conversationId,
|
|
522
|
+
danglingLinks.length,
|
|
523
|
+
);
|
|
524
|
+
|
|
499
525
|
// Failure-state bookkeeping. A skipped run (`skipReason`) never invoked
|
|
500
526
|
// the agent, so it neither clears nor records. A completed run that made
|
|
501
527
|
// no progress behaves like a failure for scheduling — the size trigger
|
|
@@ -526,6 +552,7 @@ export async function memoryV2ConsolidateJob(
|
|
|
526
552
|
deferredEntries,
|
|
527
553
|
followUpJobIds: [],
|
|
528
554
|
noProgress: true,
|
|
555
|
+
danglingLinks: danglingAfter,
|
|
529
556
|
};
|
|
530
557
|
}
|
|
531
558
|
|
|
@@ -577,12 +604,47 @@ export async function memoryV2ConsolidateJob(
|
|
|
577
604
|
deferredEntries,
|
|
578
605
|
followUpJobIds,
|
|
579
606
|
noProgress: false,
|
|
607
|
+
danglingLinks: danglingAfter,
|
|
580
608
|
};
|
|
581
609
|
} finally {
|
|
582
610
|
releaseLock(lockPath);
|
|
583
611
|
}
|
|
584
612
|
}
|
|
585
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Post-run dangling-link count; warns (with a capped sample and the pre-run
|
|
616
|
+
* count) when any remain. `null` when the index cannot be read.
|
|
617
|
+
*/
|
|
618
|
+
async function readDanglingLinks(
|
|
619
|
+
conversationId: string,
|
|
620
|
+
danglingBefore: number,
|
|
621
|
+
): Promise<number | null> {
|
|
622
|
+
let dangling: DanglingLink[];
|
|
623
|
+
try {
|
|
624
|
+
dangling = (await getPageIndex(getWorkspaceDir())).danglingLinks;
|
|
625
|
+
} catch (err) {
|
|
626
|
+
log.warn(
|
|
627
|
+
{ err, conversationId },
|
|
628
|
+
"consolidation: post-run page-index read failed; dangling links unknown",
|
|
629
|
+
);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
if (dangling.length > 0) {
|
|
633
|
+
log.warn(
|
|
634
|
+
{
|
|
635
|
+
conversationId,
|
|
636
|
+
danglingBefore,
|
|
637
|
+
danglingAfter: dangling.length,
|
|
638
|
+
sample: dangling
|
|
639
|
+
.slice(0, MAX_LOGGED_DANGLING_LINKS)
|
|
640
|
+
.map((d) => `${d.from} -> ${d.to} (${d.kind})`),
|
|
641
|
+
},
|
|
642
|
+
"consolidation left structural links whose target page does not exist; the next pass renders them as a repair step",
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return dangling.length;
|
|
646
|
+
}
|
|
647
|
+
|
|
586
648
|
/**
|
|
587
649
|
* Read `memory/buffer.md`. Missing file → empty string so the skip-on-empty
|
|
588
650
|
* branch doesn't have to distinguish "no file" from "blank file".
|
|
@@ -166,37 +166,6 @@ export function getReachable(
|
|
|
166
166
|
return result;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
export interface EdgeValidationResult {
|
|
170
|
-
ok: boolean;
|
|
171
|
-
/**
|
|
172
|
-
* Outgoing-edge targets that don't correspond to any known slug. Sorted by
|
|
173
|
-
* `(from, to)` for deterministic output.
|
|
174
|
-
*/
|
|
175
|
-
missing: Array<{ from: string; to: string }>;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Validate every outgoing edge target against `knownSlugs`. Returns a sorted
|
|
180
|
-
* list of `(from, to)` pairs whose `to` slug has no corresponding concept
|
|
181
|
-
* page on disk.
|
|
182
|
-
*/
|
|
183
|
-
export function validateEdgeTargets(
|
|
184
|
-
index: EdgeIndex,
|
|
185
|
-
knownSlugs: ReadonlySet<string>,
|
|
186
|
-
): EdgeValidationResult {
|
|
187
|
-
const missing: Array<{ from: string; to: string }> = [];
|
|
188
|
-
const sources = [...index.outgoing.keys()].sort();
|
|
189
|
-
for (const from of sources) {
|
|
190
|
-
const targets = [...(index.outgoing.get(from) ?? [])].sort();
|
|
191
|
-
for (const to of targets) {
|
|
192
|
-
if (!knownSlugs.has(to)) {
|
|
193
|
-
missing.push({ from, to });
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return { ok: missing.length === 0, missing };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
169
|
/**
|
|
201
170
|
* Total count of directed edges in the index — i.e. the sum of every page's
|
|
202
171
|
* outgoing-edge fanout. Each (from, to) pair counts once.
|
|
@@ -29,7 +29,9 @@ import {
|
|
|
29
29
|
tryAcquireLock,
|
|
30
30
|
} from "./consolidation-lock.js";
|
|
31
31
|
import { parseOriginDate } from "./page-index.js";
|
|
32
|
+
import { findDanglingLinks } from "./page-links.js";
|
|
32
33
|
import {
|
|
34
|
+
isValidSlug,
|
|
33
35
|
listPages,
|
|
34
36
|
parsePageContent,
|
|
35
37
|
validateSlug,
|
|
@@ -127,6 +129,14 @@ const REINDEX_JOB_TYPES: readonly MemoryJobType[] = [
|
|
|
127
129
|
* consolidation lock, so a page committed by another writer between
|
|
128
130
|
* validation and the write phase is still skipped rather than
|
|
129
131
|
* overwritten.
|
|
132
|
+
* 5. One more non-blocking warning, once the on-disk snapshot exists: a
|
|
133
|
+
* `links:`, `[[wikilink]]`, or `edges:` target that is neither on disk
|
|
134
|
+
* nor a valid page in this batch. Retrieval drops such a reference, so
|
|
135
|
+
* the caller either stages the missing page or turns the reference into
|
|
136
|
+
* prose. Warned, not rejected: a page is not worth losing over one link,
|
|
137
|
+
* and a multi-batch import legitimately references a later batch.
|
|
138
|
+
* Targets under the synthetic capability prefixes are not checked (their
|
|
139
|
+
* catalog is not on disk).
|
|
130
140
|
*
|
|
131
141
|
* With `dryRun` the summary is returned without touching disk or the lock;
|
|
132
142
|
* the dry-run collision snapshot is lock-free and therefore advisory.
|
|
@@ -250,6 +260,7 @@ export async function ingestPages(
|
|
|
250
260
|
// concurrent writer can change what a later real run would do.
|
|
251
261
|
const existingSlugs = new Set(await listPages(workspaceDir));
|
|
252
262
|
classifyCollisions(pending, existingSlugs, overwrite);
|
|
263
|
+
warnDanglingLinks(pending, existingSlugs);
|
|
253
264
|
return summarize(results, dryRun);
|
|
254
265
|
}
|
|
255
266
|
|
|
@@ -264,6 +275,7 @@ export async function ingestPages(
|
|
|
264
275
|
// writer after validation still classifies as a collision.
|
|
265
276
|
const existingSlugs = new Set(await listPages(workspaceDir));
|
|
266
277
|
const toWrite = classifyCollisions(pending, existingSlugs, overwrite);
|
|
278
|
+
warnDanglingLinks(pending, existingSlugs);
|
|
267
279
|
const summary = summarize(results, dryRun);
|
|
268
280
|
|
|
269
281
|
for (const page of toWrite) {
|
|
@@ -319,6 +331,53 @@ function classifyCollisions(
|
|
|
319
331
|
return toWrite;
|
|
320
332
|
}
|
|
321
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Append a dangling-link warning to every page about to be written whose
|
|
336
|
+
* structural references name a slug that is neither on disk nor a valid page
|
|
337
|
+
* in this batch. Runs after collision classification so pages the batch will
|
|
338
|
+
* not write (`skipped_exists`) are not warned about.
|
|
339
|
+
*/
|
|
340
|
+
function warnDanglingLinks(
|
|
341
|
+
pending: readonly PendingPage[],
|
|
342
|
+
existingSlugs: ReadonlySet<string>,
|
|
343
|
+
): void {
|
|
344
|
+
const knownSlugs = new Set(existingSlugs);
|
|
345
|
+
for (const { page } of pending) {
|
|
346
|
+
knownSlugs.add(page.slug);
|
|
347
|
+
}
|
|
348
|
+
const toWrite = pending.filter(({ result }) => result.action === "written");
|
|
349
|
+
const dangling = findDanglingLinks(
|
|
350
|
+
toWrite.map(({ page }) => page),
|
|
351
|
+
knownSlugs,
|
|
352
|
+
isValidSlug,
|
|
353
|
+
).filter(
|
|
354
|
+
(d) => !RESERVED_SLUG_PREFIXES.some((prefix) => d.to.startsWith(prefix)),
|
|
355
|
+
);
|
|
356
|
+
if (dangling.length === 0) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const targetsBySlug = new Map<string, string[]>();
|
|
360
|
+
for (const { from, to } of dangling) {
|
|
361
|
+
const targets = targetsBySlug.get(from) ?? [];
|
|
362
|
+
if (!targets.includes(to)) {
|
|
363
|
+
targets.push(to);
|
|
364
|
+
}
|
|
365
|
+
targetsBySlug.set(from, targets);
|
|
366
|
+
}
|
|
367
|
+
for (const { page, result } of toWrite) {
|
|
368
|
+
const targets = targetsBySlug.get(page.slug);
|
|
369
|
+
if (targets === undefined) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
result.warnings.push(
|
|
373
|
+
`link target${targets.length === 1 ? "" : "s"} not on disk or in ` +
|
|
374
|
+
`this batch: ${targets.join(", ")}; stage the missing page or ` +
|
|
375
|
+
`turn the reference into prose (retrieval drops a link whose ` +
|
|
376
|
+
`target page does not exist)`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
322
381
|
function summarize(
|
|
323
382
|
results: IngestPageResult[],
|
|
324
383
|
dryRun: boolean,
|
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { getLogger } from "../logging.js";
|
|
23
|
-
import {
|
|
23
|
+
import { type DanglingLink, findDanglingLinks } from "./page-links.js";
|
|
24
|
+
import {
|
|
25
|
+
getPageMtimeMs,
|
|
26
|
+
isValidSlug,
|
|
27
|
+
listPages,
|
|
28
|
+
readPage,
|
|
29
|
+
} from "./page-store.js";
|
|
30
|
+
import type { ConceptPage } from "./types.js";
|
|
24
31
|
|
|
25
32
|
// Dynamic import for `./skill-store.js` happens inside `getPageIndex` so that
|
|
26
33
|
// modules that only need `invalidatePageIndex` (page-store.ts,
|
|
@@ -103,8 +110,9 @@ export interface PageParseFailure {
|
|
|
103
110
|
* ASCII so IDs are deterministic across rebuilds with the same input. The
|
|
104
111
|
* `bySlug` and `byId` maps are convenience lookups; `rendered` is the prompt
|
|
105
112
|
* block consumed by the router. `parseFailures` lists the pages this build
|
|
106
|
-
* dropped or degraded
|
|
107
|
-
*
|
|
113
|
+
* dropped or degraded and `danglingLinks` the structural references whose
|
|
114
|
+
* target has no entry, so consumers (the consolidation prompt, the validate
|
|
115
|
+
* route) can surface them for repair.
|
|
108
116
|
*/
|
|
109
117
|
export interface PageIndex {
|
|
110
118
|
entries: PageIndexEntry[];
|
|
@@ -112,6 +120,8 @@ export interface PageIndex {
|
|
|
112
120
|
byId: Map<number, PageIndexEntry>;
|
|
113
121
|
rendered: string;
|
|
114
122
|
parseFailures: PageParseFailure[];
|
|
123
|
+
/** Resolved against every `bySlug` key, synthetic rows included. */
|
|
124
|
+
danglingLinks: DanglingLink[];
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
interface CachedIndex {
|
|
@@ -187,6 +197,7 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
187
197
|
|
|
188
198
|
const drafts: DraftEntry[] = [];
|
|
189
199
|
const parseFailures: PageParseFailure[] = [];
|
|
200
|
+
const linkSources: ConceptPage[] = [];
|
|
190
201
|
for (let i = 0; i < settled.length; i++) {
|
|
191
202
|
const result = settled[i];
|
|
192
203
|
const slug = slugs[i];
|
|
@@ -232,6 +243,7 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
232
243
|
modifiedAt: mtimeMs,
|
|
233
244
|
freshAt: resolveFreshAt(page.frontmatter.origin_date, mtimeMs),
|
|
234
245
|
});
|
|
246
|
+
linkSources.push(page);
|
|
235
247
|
}
|
|
236
248
|
|
|
237
249
|
for (const entry of skillEntries) {
|
|
@@ -276,8 +288,8 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
276
288
|
return entry;
|
|
277
289
|
});
|
|
278
290
|
|
|
279
|
-
// Edges whose target slug isn't in the index are
|
|
280
|
-
//
|
|
291
|
+
// Edges whose target slug isn't in the index are not resolved here; they are
|
|
292
|
+
// reported in `danglingLinks` below instead.
|
|
281
293
|
for (let i = 0; i < entries.length; i++) {
|
|
282
294
|
const draft = drafts[i];
|
|
283
295
|
const resolved: number[] = [];
|
|
@@ -297,8 +309,21 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
|
|
|
297
309
|
a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,
|
|
298
310
|
);
|
|
299
311
|
|
|
312
|
+
const danglingLinks = findDanglingLinks(
|
|
313
|
+
linkSources,
|
|
314
|
+
new Set(bySlug.keys()),
|
|
315
|
+
isValidSlug,
|
|
316
|
+
);
|
|
317
|
+
|
|
300
318
|
const rendered = renderIndex(entries);
|
|
301
|
-
const index: PageIndex = {
|
|
319
|
+
const index: PageIndex = {
|
|
320
|
+
entries,
|
|
321
|
+
bySlug,
|
|
322
|
+
byId,
|
|
323
|
+
rendered,
|
|
324
|
+
parseFailures,
|
|
325
|
+
danglingLinks,
|
|
326
|
+
};
|
|
302
327
|
cache = { workspaceDir, index };
|
|
303
328
|
return index;
|
|
304
329
|
}
|
|
@@ -525,9 +550,10 @@ function buildLocalPageIndex(
|
|
|
525
550
|
bySlug: localBySlug,
|
|
526
551
|
byId: localById,
|
|
527
552
|
rendered: renderIndex(localEntries),
|
|
528
|
-
// Parse failures are
|
|
529
|
-
// batch
|
|
553
|
+
// Parse failures and dangling links are properties of the workspace
|
|
554
|
+
// build, not of a carved batch: the full index is the reporting surface.
|
|
530
555
|
parseFailures: [],
|
|
556
|
+
danglingLinks: [],
|
|
531
557
|
};
|
|
532
558
|
}
|
|
533
559
|
|
|
@@ -626,5 +652,6 @@ function emptyPageIndex(): PageIndex {
|
|
|
626
652
|
byId: new Map(),
|
|
627
653
|
rendered: "",
|
|
628
654
|
parseFailures: [],
|
|
655
|
+
danglingLinks: [],
|
|
629
656
|
};
|
|
630
657
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SUBSTRATE (v2+v3).
|
|
2
|
+
/**
|
|
3
|
+
* Memory substrate: the single owner of concept-page link syntax.
|
|
4
|
+
*
|
|
5
|
+
* A `links:` frontmatter entry is `"<target-slug> <sep> <why>"` with the
|
|
6
|
+
* separator {@link LINK_SEPARATOR}, split on its first occurrence (no
|
|
7
|
+
* separator: bare slug). An inline `[[wikilink]]` may carry `|display` and/or
|
|
8
|
+
* `#anchor`, neither part of the target. `edges:` entries are bare slugs.
|
|
9
|
+
*
|
|
10
|
+
* Every reader of these shapes imports from here (the v3 edge lane, the
|
|
11
|
+
* pending-buffer hints, the page index, ingest); do not add a second wikilink
|
|
12
|
+
* matcher or `links:` splitter, for the same reason `buffer-format.ts` keeps
|
|
13
|
+
* one buffer-entry matcher.
|
|
14
|
+
*
|
|
15
|
+
* Leaf module: imports nothing from the rest of the substrate.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Space, U+2014 EM DASH, space. */
|
|
19
|
+
export const LINK_SEPARATOR = ` ${String.fromCodePoint(0x2014)} `;
|
|
20
|
+
|
|
21
|
+
/** Global; group 1 is the raw inner text. Pair with {@link wikilinkTarget}. */
|
|
22
|
+
export const WIKILINK_REGEX = /\[\[([^\]]+)\]\]/g;
|
|
23
|
+
|
|
24
|
+
export function parseLinkEntry(entry: string): {
|
|
25
|
+
target: string;
|
|
26
|
+
description: string | undefined;
|
|
27
|
+
} {
|
|
28
|
+
const sep = entry.indexOf(LINK_SEPARATOR);
|
|
29
|
+
if (sep === -1) {
|
|
30
|
+
return { target: entry.trim(), description: undefined };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
target: entry.slice(0, sep).trim(),
|
|
34
|
+
description: entry.slice(sep + LINK_SEPARATOR.length).trim() || undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The slug a wikilink's inner text names; empty for `[[|label]]` / `[[#anchor]]`. */
|
|
39
|
+
export function wikilinkTarget(inner: string): string {
|
|
40
|
+
let target = inner;
|
|
41
|
+
const pipe = target.indexOf("|");
|
|
42
|
+
if (pipe !== -1) {
|
|
43
|
+
target = target.slice(0, pipe);
|
|
44
|
+
}
|
|
45
|
+
const hash = target.indexOf("#");
|
|
46
|
+
if (hash !== -1) {
|
|
47
|
+
target = target.slice(0, hash);
|
|
48
|
+
}
|
|
49
|
+
return target.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every wikilink target in `text`, in order, empty targets dropped, duplicates kept. */
|
|
53
|
+
export function extractWikilinkTargets(text: string): string[] {
|
|
54
|
+
const targets: string[] = [];
|
|
55
|
+
for (const match of text.matchAll(WIKILINK_REGEX)) {
|
|
56
|
+
const target = wikilinkTarget(match[1]);
|
|
57
|
+
if (target) {
|
|
58
|
+
targets.push(target);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return targets;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type PageLinkKind = "links" | "wikilink" | "edges";
|
|
65
|
+
|
|
66
|
+
/** A structural reference from page `from` to slug `to` that has no page behind it. */
|
|
67
|
+
export interface DanglingLink {
|
|
68
|
+
from: string;
|
|
69
|
+
to: string;
|
|
70
|
+
kind: PageLinkKind;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The slice of a `ConceptPage` the finder reads (a parsed page satisfies it). */
|
|
74
|
+
export interface PageLinkSource {
|
|
75
|
+
slug: string;
|
|
76
|
+
frontmatter: {
|
|
77
|
+
links?: readonly string[] | undefined;
|
|
78
|
+
edges?: readonly string[] | undefined;
|
|
79
|
+
};
|
|
80
|
+
/** Frontmatter-stripped body. */
|
|
81
|
+
body: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Structural references on `pages` whose target is not in `knownSlugs`.
|
|
86
|
+
* Only slug-shaped targets (`isSlug`, the page store's rule) are references
|
|
87
|
+
* at all: `[[ -f foo ]]` in a shell snippet is not a link to a page. Self
|
|
88
|
+
* references are ignored, each `(from, to, kind)` is reported once, and the
|
|
89
|
+
* result is sorted by `(from, to, kind)` so renderings are byte-stable.
|
|
90
|
+
*/
|
|
91
|
+
export function findDanglingLinks(
|
|
92
|
+
pages: readonly PageLinkSource[],
|
|
93
|
+
knownSlugs: ReadonlySet<string>,
|
|
94
|
+
isSlug: (target: string) => boolean,
|
|
95
|
+
): DanglingLink[] {
|
|
96
|
+
const seen = new Set<string>();
|
|
97
|
+
const dangling: DanglingLink[] = [];
|
|
98
|
+
const report = (from: string, to: string, kind: PageLinkKind): void => {
|
|
99
|
+
if (to === from || knownSlugs.has(to) || !isSlug(to)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const key = `${kind} ${from} ${to}`;
|
|
103
|
+
if (seen.has(key)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
seen.add(key);
|
|
107
|
+
dangling.push({ from, to, kind });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (const page of pages) {
|
|
111
|
+
for (const entry of page.frontmatter.links ?? []) {
|
|
112
|
+
report(page.slug, parseLinkEntry(entry).target, "links");
|
|
113
|
+
}
|
|
114
|
+
for (const target of extractWikilinkTargets(page.body)) {
|
|
115
|
+
report(page.slug, target, "wikilink");
|
|
116
|
+
}
|
|
117
|
+
for (const target of page.frontmatter.edges ?? []) {
|
|
118
|
+
report(page.slug, target.trim(), "edges");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
dangling.sort(
|
|
123
|
+
(a, b) =>
|
|
124
|
+
compareStrings(a.from, b.from) ||
|
|
125
|
+
compareStrings(a.to, b.to) ||
|
|
126
|
+
compareStrings(a.kind, b.kind),
|
|
127
|
+
);
|
|
128
|
+
return dangling;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compareStrings(a: string, b: string): number {
|
|
132
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
133
|
+
}
|
|
@@ -153,6 +153,16 @@ export function validateSlug(slug: string): void {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/** Boolean form of {@link validateSlug}. */
|
|
157
|
+
export function isValidSlug(slug: string): boolean {
|
|
158
|
+
try {
|
|
159
|
+
validateSlug(slug);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
156
166
|
// ---------------------------------------------------------------------------
|
|
157
167
|
// Path helpers
|
|
158
168
|
// ---------------------------------------------------------------------------
|