@poolzin/pool-bot 2026.2.17 → 2026.2.18
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/CHANGELOG.md +17 -0
- package/dist/agents/agent-scope.js +4 -0
- package/dist/agents/announce-idempotency.js +14 -0
- package/dist/agents/auth-profiles.resolve-auth-profile-order.fixtures.js +23 -0
- package/dist/agents/bash-tools.exec-runtime.js +438 -0
- package/dist/agents/bash-tools.shared.js +6 -0
- package/dist/agents/cli-runner/reliability.js +61 -0
- package/dist/agents/cli-watchdog-defaults.js +11 -0
- package/dist/agents/command-poll-backoff.js +63 -0
- package/dist/agents/current-time.js +16 -0
- package/dist/agents/model-alias-lines.js +18 -0
- package/dist/agents/model-auth-label.js +61 -0
- package/dist/agents/models-config.e2e-harness.js +115 -0
- package/dist/agents/ollama-stream.js +11 -3
- package/dist/agents/openclaw-tools.js +135 -0
- package/dist/agents/pi-auth-json.js +118 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +147 -0
- package/dist/agents/pi-embedded-subscribe.e2e-harness.js +90 -0
- package/dist/agents/pi-embedded-subscribe.handlers.compaction.js +63 -0
- package/dist/agents/pi-embedded-subscribe.handlers.tools.media.test-helpers.js +30 -0
- package/dist/agents/pi-extensions/session-manager-runtime-registry.js +23 -0
- package/dist/agents/pi-tools.js +2 -0
- package/dist/agents/queued-file-writer.js +22 -0
- package/dist/agents/sandbox/docker.js +133 -40
- package/dist/agents/sandbox/fs-bridge.js +146 -0
- package/dist/agents/sandbox/fs-paths.js +205 -0
- package/dist/agents/sandbox/hash.js +4 -0
- package/dist/agents/sandbox-paths.js +3 -0
- package/dist/agents/session-dirs.js +20 -0
- package/dist/agents/skills/filter.js +24 -0
- package/dist/agents/skills/tools-dir.js +9 -0
- package/dist/agents/skills-install-download.js +290 -0
- package/dist/agents/skills-install-output.js +30 -0
- package/dist/agents/skills-install.download-test-utils.js +36 -0
- package/dist/agents/skills.e2e-test-helpers.js +13 -0
- package/dist/agents/subagent-announce-queue.js +59 -15
- package/dist/agents/subagent-depth.js +137 -0
- package/dist/agents/subagent-registry.js +448 -96
- package/dist/agents/subagent-spawn.js +262 -0
- package/dist/agents/test-helpers/fast-tool-stubs.js +18 -0
- package/dist/agents/test-helpers/host-sandbox-fs-bridge.js +74 -0
- package/dist/agents/tool-display-common.js +782 -0
- package/dist/agents/tools/image-tool.js +1 -1
- package/dist/agents/tools/sessions-access.js +178 -0
- package/dist/agents/tools/sessions-resolution.js +206 -0
- package/dist/agents/tools/subagents-tool.js +616 -0
- package/dist/agents/workspace-dir.js +18 -0
- package/dist/agents/workspace-dirs.js +14 -0
- package/dist/agents/workspace.js +70 -0
- package/dist/auto-reply/heartbeat-reply-payload.js +18 -0
- package/dist/auto-reply/reply/commands-export-session.js +163 -0
- package/dist/auto-reply/reply/commands-mesh.js +245 -0
- package/dist/auto-reply/reply/commands-setunset.js +28 -0
- package/dist/auto-reply/reply/commands-slash-parse.js +31 -0
- package/dist/auto-reply/reply/commands-system-prompt.js +117 -0
- package/dist/auto-reply/reply/directive-handling.levels.js +17 -0
- package/dist/auto-reply/reply/directive-handling.params.js +1 -0
- package/dist/auto-reply/reply/directive-parsing.js +36 -0
- package/dist/auto-reply/reply/dispatcher-registry.js +43 -0
- package/dist/auto-reply/reply/elevated-unavailable.js +20 -0
- package/dist/auto-reply/reply/reply-delivery.js +92 -0
- package/dist/auto-reply/reply/session-reset-prompt.js +1 -0
- package/dist/auto-reply/reply/session-run-accounting.js +33 -0
- package/dist/auto-reply/reply.directive.directive-behavior.e2e-harness.js +115 -0
- package/dist/auto-reply/reply.directive.directive-behavior.e2e-mocks.js +12 -0
- package/dist/browser/bridge-auth-registry.js +26 -0
- package/dist/browser/client-actions-url.js +10 -0
- package/dist/browser/control-auth.js +73 -0
- package/dist/browser/csrf.js +64 -0
- package/dist/browser/http-auth.js +52 -0
- package/dist/browser/paths.js +37 -0
- package/dist/browser/proxy-files.js +32 -0
- package/dist/browser/pw-ai-state.js +7 -0
- package/dist/browser/resolved-config-refresh.js +42 -0
- package/dist/browser/routes/path-output.js +1 -0
- package/dist/browser/server-context.chrome-test-harness.js +20 -0
- package/dist/browser/server-middleware.js +31 -0
- package/dist/browser/test-port.js +16 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/file-resolver.js +43 -0
- package/dist/channels/account-summary.js +19 -0
- package/dist/channels/draft-stream-loop.js +77 -0
- package/dist/channels/plugins/account-helpers.js +26 -0
- package/dist/channels/telegram/allow-from.js +10 -0
- package/dist/cli/browser-cli-resize.js +22 -0
- package/dist/cli/browser-cli-shared.js +8 -0
- package/dist/cli/clawbot-cli.js +5 -0
- package/dist/cli/completion-cli.js +566 -0
- package/dist/cli/config-cli.js +63 -5
- package/dist/cli/daemon-cli/lifecycle-core.js +256 -0
- package/dist/cli/daemon-cli/register-service-commands.js +60 -0
- package/dist/cli/daemon-cli-compat.js +80 -0
- package/dist/cli/nodes-cli/pairing-render.js +26 -0
- package/dist/cli/program/action-reparse.js +17 -0
- package/dist/cli/program/command-registry.js +17 -0
- package/dist/cli/program/program-context.js +8 -0
- package/dist/cli/program/register.subclis.js +7 -0
- package/dist/cli/program/routes.js +233 -0
- package/dist/cli/qr-cli.js +132 -0
- package/dist/cli/requirements-test-fixtures.js +17 -0
- package/dist/cli/respawn-policy.js +4 -0
- package/dist/cli/shared/parse-port.js +18 -0
- package/dist/cli/skills-cli.format.js +241 -0
- package/dist/cli/update-cli/progress.js +121 -0
- package/dist/cli/update-cli/restart-helper.js +108 -0
- package/dist/cli/update-cli/shared.js +196 -0
- package/dist/cli/update-cli/status.js +97 -0
- package/dist/cli/update-cli/suppress-deprecations.js +17 -0
- package/dist/cli/update-cli/update-command.js +506 -0
- package/dist/cli/update-cli/wizard.js +130 -0
- package/dist/cli/update-cli.js +3 -9
- package/dist/cli/windows-argv.js +69 -0
- package/dist/commands/auth-choice-legacy.js +20 -0
- package/dist/commands/auth-choice.apply-helpers.js +8 -0
- package/dist/commands/channel-test-helpers.js +19 -0
- package/dist/commands/cleanup-plan.js +10 -0
- package/dist/commands/cleanup-utils.js +7 -0
- package/dist/commands/config-validation.js +15 -0
- package/dist/commands/doctor-completion.js +112 -0
- package/dist/commands/doctor-memory-search.js +119 -0
- package/dist/commands/doctor-session-locks.js +73 -0
- package/dist/commands/doctor.e2e-harness.js +364 -0
- package/dist/commands/gateway-presence.js +19 -0
- package/dist/commands/model-default.js +35 -0
- package/dist/commands/models/fallbacks-shared.js +102 -0
- package/dist/commands/models/shared.js +24 -0
- package/dist/commands/onboard-auth.config-gateways.js +64 -0
- package/dist/commands/onboard-auth.config-litellm.js +45 -0
- package/dist/commands/onboard-auth.config-shared.js +116 -0
- package/dist/commands/onboard-config.js +16 -0
- package/dist/commands/onboard-non-interactive.test-helpers.js +31 -0
- package/dist/commands/onboard-provider-auth-flags.js +136 -0
- package/dist/commands/openai-codex-oauth.js +40 -0
- package/dist/commands/test-runtime-config-helpers.js +21 -0
- package/dist/commands/test-wizard-helpers.js +68 -0
- package/dist/commands/vllm-setup.js +66 -0
- package/dist/compat/legacy-names.js +2 -0
- package/dist/config/backup-rotation.js +19 -0
- package/dist/config/env-preserve.js +122 -0
- package/dist/config/includes-scan.js +78 -0
- package/dist/config/plugins-allowlist.js +13 -0
- package/dist/config/schema.help.js +256 -0
- package/dist/config/schema.hints.js +189 -0
- package/dist/config/schema.irc.js +20 -0
- package/dist/config/schema.labels.js +317 -0
- package/dist/config/sessions/delivery-info.js +40 -0
- package/dist/config/types.irc.js +1 -0
- package/dist/config/zod-schema.agent-model.js +10 -0
- package/dist/config/zod-schema.allowdeny.js +35 -0
- package/dist/config/zod-schema.sensitive.js +4 -0
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
- package/dist/cron/isolated-agent/skills-snapshot.js +26 -0
- package/dist/cron/isolated-agent/subagent-followup.js +127 -0
- package/dist/cron/isolated-agent.mocks.js +12 -0
- package/dist/cron/isolated-agent.test-setup.js +22 -0
- package/dist/cron/legacy-delivery.js +43 -0
- package/dist/cron/webhook-url.js +22 -0
- package/dist/daemon/arg-split.js +40 -0
- package/dist/daemon/exec-file.js +23 -0
- package/dist/daemon/output.js +6 -0
- package/dist/daemon/runtime-format.js +31 -0
- package/dist/daemon/schtasks-exec.js +4 -0
- package/dist/daemon/service-audit.js +22 -0
- package/dist/discord/client.js +41 -0
- package/dist/discord/components-registry.js +57 -0
- package/dist/discord/components.js +816 -0
- package/dist/discord/guilds.js +12 -0
- package/dist/discord/monitor/gateway-plugin.js +48 -0
- package/dist/discord/monitor/presence.js +30 -0
- package/dist/discord/send.components.js +115 -0
- package/dist/discord/send.shared.js +4 -0
- package/dist/discord/ui.js +26 -0
- package/dist/discord/voice-message.js +254 -0
- package/dist/gateway/agent-event-assistant-text.js +5 -0
- package/dist/gateway/agent-prompt.js +33 -0
- package/dist/gateway/auth-rate-limit.js +136 -0
- package/dist/gateway/channel-health-monitor.js +114 -0
- package/dist/gateway/control-ui-contract.js +1 -0
- package/dist/gateway/control-ui-csp.js +15 -0
- package/dist/gateway/gateway-config-prompts.shared.js +25 -0
- package/dist/gateway/http-auth-helpers.js +18 -0
- package/dist/gateway/http-common.js +18 -0
- package/dist/gateway/http-endpoint-helpers.js +27 -0
- package/dist/gateway/node-invoke-sanitize.js +11 -0
- package/dist/gateway/node-invoke-system-run-approval.js +205 -0
- package/dist/gateway/probe-auth.js +21 -0
- package/dist/gateway/protocol/index.js +7 -2
- package/dist/gateway/protocol/schema/mesh.js +54 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +7 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server/ws-connection/auth-messages.js +54 -0
- package/dist/gateway/server-channels.js +11 -0
- package/dist/gateway/server-methods/attachment-normalize.js +16 -0
- package/dist/gateway/server-methods/base-hash.js +8 -0
- package/dist/gateway/server-methods/mesh.js +700 -0
- package/dist/gateway/server-methods/nodes.handlers.invoke-result.js +55 -0
- package/dist/gateway/server-methods/restart-request.js +13 -0
- package/dist/gateway/server-methods/validation.js +8 -0
- package/dist/gateway/server.agent.gateway-server-agent.mocks.js +35 -0
- package/dist/gateway/server.e2e-registry-helpers.js +1 -0
- package/dist/gateway/server.e2e-ws-harness.js +20 -0
- package/dist/gateway/test-helpers.js +2 -0
- package/dist/gateway/test-helpers.server.js +3 -1
- package/dist/gateway/test-http-response.js +12 -0
- package/dist/gateway/test-openai-responses-model.js +20 -0
- package/dist/gateway/test-temp-config.js +30 -0
- package/dist/gateway/test-with-server.js +32 -0
- package/dist/hooks/bundled/bootstrap-extra-files/handler.js +46 -0
- package/dist/imessage/monitor/abort-handler.js +23 -0
- package/dist/imessage/monitor/inbound-processing.js +346 -0
- package/dist/imessage/monitor/parse-notification.js +64 -0
- package/dist/imessage/target-parsing-helpers.js +92 -0
- package/dist/infra/archive.js +244 -20
- package/dist/infra/detect-package-manager.js +26 -0
- package/dist/infra/exec-approvals-allowlist.js +257 -0
- package/dist/infra/exec-approvals-analysis.js +770 -0
- package/dist/infra/exec-approvals.js +13 -0
- package/dist/infra/file-lock.js +1 -0
- package/dist/infra/gemini-auth.js +39 -0
- package/dist/infra/heartbeat-active-hours.js +85 -0
- package/dist/infra/heartbeat-events-filter.js +50 -0
- package/dist/infra/heartbeat-runner.test-utils.js +39 -0
- package/dist/infra/http-body.js +265 -0
- package/dist/infra/install-package-dir.js +50 -0
- package/dist/infra/install-safe-path.js +49 -0
- package/dist/infra/json-files.js +49 -0
- package/dist/infra/jsonl-socket.js +52 -0
- package/dist/infra/map-size.js +14 -0
- package/dist/infra/net/hostname.js +7 -0
- package/dist/infra/npm-registry-spec.js +39 -0
- package/dist/infra/openclaw-root.js +109 -0
- package/dist/infra/outbound/delivery-queue.js +214 -0
- package/dist/infra/outbound/identity.js +23 -0
- package/dist/infra/outbound/message-action-params.js +307 -0
- package/dist/infra/outbound/tool-payload.js +21 -0
- package/dist/infra/package-json.js +23 -0
- package/dist/infra/pairing-files.js +19 -0
- package/dist/infra/pairing-token.js +9 -0
- package/dist/infra/path-prepend.js +51 -0
- package/dist/infra/process-respawn.js +49 -0
- package/dist/infra/runtime-status.js +16 -0
- package/dist/infra/session-cost-usage.types.js +1 -0
- package/dist/infra/session-maintenance-warning.js +89 -0
- package/dist/infra/system-run-command.js +78 -0
- package/dist/infra/tmp-openclaw-dir.js +81 -0
- package/dist/infra/tmp-poolbot-dir.js +2 -0
- package/dist/infra/update-channels.js +19 -0
- package/dist/line/actions.js +45 -0
- package/dist/line/channel-access-token.js +9 -0
- package/dist/line/flex-templates/basic-cards.js +332 -0
- package/dist/line/flex-templates/common.js +18 -0
- package/dist/line/flex-templates/media-control-cards.js +453 -0
- package/dist/line/flex-templates/message.js +10 -0
- package/dist/line/flex-templates/schedule-cards.js +399 -0
- package/dist/line/flex-templates/types.js +1 -0
- package/dist/line/webhook-node.js +100 -0
- package/dist/line/webhook-utils.js +11 -0
- package/dist/logging/timestamps.js +14 -0
- package/dist/markdown/whatsapp.js +62 -0
- package/dist/media/base64.js +34 -0
- package/dist/media/local-roots.js +32 -0
- package/dist/media/outbound-attachment.js +10 -0
- package/dist/media/read-response-with-limit.js +41 -0
- package/dist/media/sniff-mime-from-base64.js +19 -0
- package/dist/media-understanding/audio-preflight.js +67 -0
- package/dist/media-understanding/fs.js +13 -0
- package/dist/media-understanding/output-extract.js +26 -0
- package/dist/media-understanding/providers/audio.test-helpers.js +34 -0
- package/dist/media-understanding/providers/google/inline-data.js +64 -0
- package/dist/media-understanding/providers/shared.js +7 -0
- package/dist/media-understanding/runner.entries.js +459 -0
- package/dist/memory/batch-error-utils.js +11 -0
- package/dist/memory/batch-http.js +27 -0
- package/dist/memory/batch-output.js +29 -0
- package/dist/memory/batch-runner.js +22 -0
- package/dist/memory/batch-upload.js +23 -0
- package/dist/memory/batch-utils.js +26 -0
- package/dist/memory/embeddings-debug.js +11 -0
- package/dist/memory/embeddings-remote-client.js +22 -0
- package/dist/memory/embeddings-remote-fetch.js +14 -0
- package/dist/memory/manager-embedding-ops.js +616 -0
- package/dist/memory/manager-sync-ops.js +953 -0
- package/dist/memory/qmd-manager.js +1061 -0
- package/dist/memory/qmd-query-parser.js +107 -0
- package/dist/memory/qmd-scope.js +93 -0
- package/dist/memory/search-manager.js +0 -1
- package/dist/memory/sync-index.js +21 -0
- package/dist/memory/sync-progress.js +22 -0
- package/dist/memory/sync-stale.js +30 -0
- package/dist/memory/test-embeddings-mock.js +16 -0
- package/dist/memory/test-manager-helpers.js +14 -0
- package/dist/memory/test-runtime-mocks.js +11 -0
- package/dist/node-host/invoke-browser.js +177 -0
- package/dist/node-host/invoke.js +685 -0
- package/dist/pairing/setup-code.js +285 -0
- package/dist/plugin-sdk/account-id.js +1 -0
- package/dist/plugin-sdk/agent-media-payload.js +13 -0
- package/dist/plugin-sdk/allow-from.js +47 -0
- package/dist/plugin-sdk/command-auth.js +23 -0
- package/dist/plugin-sdk/config-paths.js +9 -0
- package/dist/plugin-sdk/file-lock.js +116 -0
- package/dist/plugin-sdk/json-store.js +31 -0
- package/dist/plugin-sdk/onboarding.js +28 -0
- package/dist/plugin-sdk/provider-auth-result.js +29 -0
- package/dist/plugin-sdk/slack-message-actions.js +133 -0
- package/dist/plugin-sdk/status-helpers.js +35 -0
- package/dist/plugin-sdk/text-chunking.js +31 -0
- package/dist/plugin-sdk/tool-send.js +12 -0
- package/dist/plugin-sdk/webhook-path.js +27 -0
- package/dist/plugin-sdk/webhook-targets.js +34 -0
- package/dist/plugins/hooks.test-helpers.js +21 -0
- package/dist/plugins/uninstall.js +171 -0
- package/dist/process/supervisor/adapters/child.js +143 -0
- package/dist/process/supervisor/adapters/env.js +13 -0
- package/dist/process/supervisor/adapters/pty.js +148 -0
- package/dist/process/supervisor/index.js +10 -0
- package/dist/process/supervisor/registry.js +117 -0
- package/dist/process/supervisor/supervisor.js +244 -0
- package/dist/process/supervisor/types.js +1 -0
- package/dist/providers/google-shared.test-helpers.js +75 -0
- package/dist/security/audit-channel.js +419 -0
- package/dist/security/audit-tool-policy.js +1 -0
- package/dist/security/scan-paths.js +12 -0
- package/dist/sessions/input-provenance.js +55 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/shared/chat-content.js +31 -0
- package/dist/shared/chat-envelope.js +45 -0
- package/dist/shared/config-eval.js +117 -0
- package/dist/shared/device-auth.js +16 -0
- package/dist/shared/entry-metadata.js +9 -0
- package/dist/shared/entry-status.js +25 -0
- package/dist/shared/frontmatter.js +98 -0
- package/dist/shared/model-param-b.js +19 -0
- package/dist/shared/net/ipv4.js +17 -0
- package/dist/shared/node-match.js +53 -0
- package/dist/shared/requirements.js +128 -0
- package/dist/shared/subagents-format.js +84 -0
- package/dist/shared/usage-aggregates.js +28 -0
- package/dist/signal/monitor/mentions.js +45 -0
- package/dist/signal/rpc-context.js +19 -0
- package/dist/slack/blocks-fallback.js +76 -0
- package/dist/slack/blocks-input.js +40 -0
- package/dist/slack/draft-stream.js +106 -0
- package/dist/slack/message-actions.js +51 -0
- package/dist/slack/modal-metadata.js +32 -0
- package/dist/slack/monitor/events/interactions.js +462 -0
- package/dist/slack/monitor/room-context.js +17 -0
- package/dist/slack/stream-mode.js +41 -0
- package/dist/telegram/bot-native-command-menu.js +64 -0
- package/dist/telegram/bot.media.e2e-harness.js +81 -0
- package/dist/telegram/button-types.js +1 -0
- package/dist/telegram/group-access.js +65 -0
- package/dist/telegram/outbound-params.js +21 -0
- package/dist/telegram/poll-vote-cache.js +21 -0
- package/dist/terminal/health-style.js +36 -0
- package/dist/test-utils/chunk-test-helpers.js +21 -0
- package/dist/test-utils/env.js +72 -0
- package/dist/test-utils/exec-assertions.js +12 -0
- package/dist/test-utils/imessage-test-plugin.js +54 -0
- package/dist/test-utils/mock-http-response.js +17 -0
- package/dist/test-utils/vitest-mock-fn.js +1 -0
- package/dist/tts/tts-core.js +550 -0
- package/dist/utils/chunk-items.js +10 -0
- package/dist/utils/reaction-level.js +52 -0
- package/dist/utils/safe-json.js +22 -0
- package/dist/utils/with-timeout.js +14 -0
- package/dist/web/media.js +17 -5
- package/dist/whatsapp/resolve-outbound-target.js +42 -0
- package/dist/wizard/onboarding.completion.js +74 -0
- package/extensions/bluebubbles/src/account-resolve.ts +29 -0
- package/extensions/bluebubbles/src/monitor-normalize.ts +796 -0
- package/extensions/bluebubbles/src/monitor-processing.ts +1007 -0
- package/extensions/bluebubbles/src/monitor-reply-cache.ts +185 -0
- package/extensions/bluebubbles/src/monitor-shared.ts +51 -0
- package/extensions/bluebubbles/src/multipart.ts +32 -0
- package/extensions/bluebubbles/src/send-helpers.ts +53 -0
- package/extensions/bluebubbles/src/test-harness.ts +50 -0
- package/extensions/bluebubbles/src/test-mocks.ts +11 -0
- package/extensions/device-pair/index.ts +554 -0
- package/extensions/discord/src/channel.js +366 -0
- package/extensions/discord/src/runtime.js +10 -0
- package/extensions/feishu/index.ts +63 -0
- package/extensions/feishu/src/accounts.ts +114 -0
- package/extensions/feishu/src/bitable.ts +739 -0
- package/extensions/feishu/src/bot.ts +965 -0
- package/extensions/feishu/src/channel.ts +351 -0
- package/extensions/feishu/src/client.ts +118 -0
- package/extensions/feishu/src/config-schema.ts +206 -0
- package/extensions/feishu/src/dedup.ts +33 -0
- package/extensions/feishu/src/directory.ts +177 -0
- package/extensions/feishu/src/doc-schema.ts +47 -0
- package/extensions/feishu/src/docx.ts +536 -0
- package/extensions/feishu/src/drive-schema.ts +46 -0
- package/extensions/feishu/src/drive.ts +227 -0
- package/extensions/feishu/src/dynamic-agent.ts +131 -0
- package/extensions/feishu/src/media.ts +449 -0
- package/extensions/feishu/src/mention.ts +126 -0
- package/extensions/feishu/src/monitor.ts +330 -0
- package/extensions/feishu/src/onboarding.ts +359 -0
- package/extensions/feishu/src/outbound.ts +55 -0
- package/extensions/feishu/src/perm-schema.ts +52 -0
- package/extensions/feishu/src/perm.ts +173 -0
- package/extensions/feishu/src/policy.ts +84 -0
- package/extensions/feishu/src/probe.ts +44 -0
- package/extensions/feishu/src/reactions.ts +160 -0
- package/extensions/feishu/src/reply-dispatcher.ts +239 -0
- package/extensions/feishu/src/runtime.ts +14 -0
- package/extensions/feishu/src/send-result.ts +29 -0
- package/extensions/feishu/src/send.ts +335 -0
- package/extensions/feishu/src/streaming-card.ts +223 -0
- package/extensions/feishu/src/targets.ts +78 -0
- package/extensions/feishu/src/tools-config.ts +21 -0
- package/extensions/feishu/src/types.ts +81 -0
- package/extensions/feishu/src/typing.ts +80 -0
- package/extensions/feishu/src/wiki-schema.ts +55 -0
- package/extensions/feishu/src/wiki.ts +232 -0
- package/extensions/imessage/src/channel.js +253 -0
- package/extensions/imessage/src/runtime.js +10 -0
- package/extensions/irc/index.ts +17 -0
- package/extensions/irc/src/accounts.ts +268 -0
- package/extensions/irc/src/channel.ts +367 -0
- package/extensions/irc/src/client.ts +439 -0
- package/extensions/irc/src/config-schema.ts +97 -0
- package/extensions/irc/src/connect-options.ts +30 -0
- package/extensions/irc/src/control-chars.ts +22 -0
- package/extensions/irc/src/inbound.ts +334 -0
- package/extensions/irc/src/monitor.ts +147 -0
- package/extensions/irc/src/normalize.ts +117 -0
- package/extensions/irc/src/onboarding.ts +479 -0
- package/extensions/irc/src/policy.ts +157 -0
- package/extensions/irc/src/probe.ts +53 -0
- package/extensions/irc/src/protocol.ts +169 -0
- package/extensions/irc/src/runtime.ts +14 -0
- package/extensions/irc/src/send.ts +88 -0
- package/extensions/irc/src/types.ts +93 -0
- package/extensions/matrix/src/matrix/client-bootstrap.ts +39 -0
- package/extensions/mattermost/src/mattermost/monitor-onchar.ts +25 -0
- package/extensions/mattermost/src/mattermost/monitor-websocket.ts +221 -0
- package/extensions/mattermost/src/mattermost/reactions.ts +130 -0
- package/extensions/mattermost/src/mattermost/reconnect.ts +103 -0
- package/extensions/minimax-portal-auth/index.ts +161 -0
- package/extensions/minimax-portal-auth/oauth.ts +247 -0
- package/extensions/msteams/src/file-lock.ts +1 -0
- package/extensions/msteams/src/graph.ts +92 -0
- package/extensions/msteams/src/mentions.ts +114 -0
- package/extensions/msteams/src/test-runtime.ts +16 -0
- package/extensions/openai-codex-auth/index.ts +177 -0
- package/extensions/phone-control/index.ts +421 -0
- package/extensions/shared/resolve-target-test-helpers.ts +66 -0
- package/extensions/signal/src/channel.js +273 -0
- package/extensions/signal/src/runtime.js +10 -0
- package/extensions/slack/src/channel.js +489 -0
- package/extensions/slack/src/runtime.js +10 -0
- package/extensions/talk-voice/index.ts +150 -0
- package/extensions/telegram/src/channel.js +424 -0
- package/extensions/telegram/src/runtime.js +10 -0
- package/extensions/thread-ownership/index.ts +133 -0
- package/extensions/tlon/src/account-fields.ts +25 -0
- package/extensions/tlon/src/urbit/base-url.ts +57 -0
- package/extensions/tlon/src/urbit/channel-client.ts +157 -0
- package/extensions/tlon/src/urbit/channel-ops.ts +164 -0
- package/extensions/tlon/src/urbit/context.ts +47 -0
- package/extensions/tlon/src/urbit/errors.ts +51 -0
- package/extensions/tlon/src/urbit/fetch.ts +39 -0
- package/extensions/twitch/src/test-fixtures.ts +30 -0
- package/extensions/voice-call/src/allowlist.ts +19 -0
- package/extensions/whatsapp/src/channel.js +429 -0
- package/extensions/whatsapp/src/runtime.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
|
7
|
+
import { resolveStateDir } from "../config/paths.js";
|
|
8
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
9
|
+
import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js";
|
|
10
|
+
import { listSessionFilesForAgent, buildSessionEntry, } from "./session-files.js";
|
|
11
|
+
import { requireNodeSqlite } from "./sqlite.js";
|
|
12
|
+
import { parseQmdQueryJson } from "./qmd-query-parser.js";
|
|
13
|
+
const log = createSubsystemLogger("memory");
|
|
14
|
+
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
|
15
|
+
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
|
16
|
+
const MAX_QMD_OUTPUT_CHARS = 200_000;
|
|
17
|
+
const NUL_MARKER_RE = /(?:\^@|\\0|\\x00|\\u0000|null\s*byte|nul\s*byte)/i;
|
|
18
|
+
export class QmdMemoryManager {
|
|
19
|
+
static async create(params) {
|
|
20
|
+
const resolved = params.resolved.qmd;
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const manager = new QmdMemoryManager({ cfg: params.cfg, agentId: params.agentId, resolved });
|
|
25
|
+
await manager.initialize(params.mode ?? "full");
|
|
26
|
+
return manager;
|
|
27
|
+
}
|
|
28
|
+
cfg;
|
|
29
|
+
agentId;
|
|
30
|
+
qmd;
|
|
31
|
+
workspaceDir;
|
|
32
|
+
stateDir;
|
|
33
|
+
agentStateDir;
|
|
34
|
+
qmdDir;
|
|
35
|
+
xdgConfigHome;
|
|
36
|
+
xdgCacheHome;
|
|
37
|
+
indexPath;
|
|
38
|
+
env;
|
|
39
|
+
collectionRoots = new Map();
|
|
40
|
+
sources = new Set();
|
|
41
|
+
docPathCache = new Map();
|
|
42
|
+
exportedSessionState = new Map();
|
|
43
|
+
maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
|
44
|
+
sessionExporter;
|
|
45
|
+
updateTimer = null;
|
|
46
|
+
pendingUpdate = null;
|
|
47
|
+
queuedForcedUpdate = null;
|
|
48
|
+
queuedForcedRuns = 0;
|
|
49
|
+
closed = false;
|
|
50
|
+
db = null;
|
|
51
|
+
lastUpdateAt = null;
|
|
52
|
+
lastEmbedAt = null;
|
|
53
|
+
attemptedNullByteCollectionRepair = false;
|
|
54
|
+
constructor(params) {
|
|
55
|
+
this.cfg = params.cfg;
|
|
56
|
+
this.agentId = params.agentId;
|
|
57
|
+
this.qmd = params.resolved;
|
|
58
|
+
this.workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
|
|
59
|
+
this.stateDir = resolveStateDir(process.env, os.homedir);
|
|
60
|
+
this.agentStateDir = path.join(this.stateDir, "agents", this.agentId);
|
|
61
|
+
this.qmdDir = path.join(this.agentStateDir, "qmd");
|
|
62
|
+
// QMD uses XDG base dirs for its internal state.
|
|
63
|
+
// Collections are managed via `qmd collection add` and stored inside the index DB.
|
|
64
|
+
// - config: $XDG_CONFIG_HOME (contexts, etc.)
|
|
65
|
+
// - cache: $XDG_CACHE_HOME/qmd/index.sqlite
|
|
66
|
+
this.xdgConfigHome = path.join(this.qmdDir, "xdg-config");
|
|
67
|
+
this.xdgCacheHome = path.join(this.qmdDir, "xdg-cache");
|
|
68
|
+
this.indexPath = path.join(this.xdgCacheHome, "qmd", "index.sqlite");
|
|
69
|
+
this.env = {
|
|
70
|
+
...process.env,
|
|
71
|
+
XDG_CONFIG_HOME: this.xdgConfigHome,
|
|
72
|
+
XDG_CACHE_HOME: this.xdgCacheHome,
|
|
73
|
+
NO_COLOR: "1",
|
|
74
|
+
};
|
|
75
|
+
this.sessionExporter = this.qmd.sessions.enabled
|
|
76
|
+
? {
|
|
77
|
+
dir: this.qmd.sessions.exportDir ?? path.join(this.qmdDir, "sessions"),
|
|
78
|
+
retentionMs: this.qmd.sessions.retentionDays
|
|
79
|
+
? this.qmd.sessions.retentionDays * 24 * 60 * 60 * 1000
|
|
80
|
+
: undefined,
|
|
81
|
+
collectionName: this.pickSessionCollectionName(),
|
|
82
|
+
}
|
|
83
|
+
: null;
|
|
84
|
+
if (this.sessionExporter) {
|
|
85
|
+
this.qmd.collections = [
|
|
86
|
+
...this.qmd.collections,
|
|
87
|
+
{
|
|
88
|
+
name: this.sessionExporter.collectionName,
|
|
89
|
+
path: this.sessionExporter.dir,
|
|
90
|
+
pattern: "**/*.md",
|
|
91
|
+
kind: "sessions",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async initialize(mode) {
|
|
97
|
+
this.bootstrapCollections();
|
|
98
|
+
if (mode === "status") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await fs.mkdir(this.xdgConfigHome, { recursive: true });
|
|
102
|
+
await fs.mkdir(this.xdgCacheHome, { recursive: true });
|
|
103
|
+
await fs.mkdir(path.dirname(this.indexPath), { recursive: true });
|
|
104
|
+
if (this.sessionExporter) {
|
|
105
|
+
await fs.mkdir(this.sessionExporter.dir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
// QMD stores its ML models under $XDG_CACHE_HOME/qmd/models/. Because we
|
|
108
|
+
// override XDG_CACHE_HOME to isolate the index per-agent, qmd would not
|
|
109
|
+
// find models installed at the default location (~/.cache/qmd/models/) and
|
|
110
|
+
// would attempt to re-download them on every invocation. Symlink the
|
|
111
|
+
// default models directory into our custom cache so the index stays
|
|
112
|
+
// isolated while models are shared.
|
|
113
|
+
await this.symlinkSharedModels();
|
|
114
|
+
await this.ensureCollections();
|
|
115
|
+
if (this.qmd.update.onBoot) {
|
|
116
|
+
const bootRun = this.runUpdate("boot", true);
|
|
117
|
+
if (this.qmd.update.waitForBootSync) {
|
|
118
|
+
await bootRun.catch((err) => {
|
|
119
|
+
log.warn(`qmd boot update failed: ${String(err)}`);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
void bootRun.catch((err) => {
|
|
124
|
+
log.warn(`qmd boot update failed: ${String(err)}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (this.qmd.update.intervalMs > 0) {
|
|
129
|
+
this.updateTimer = setInterval(() => {
|
|
130
|
+
void this.runUpdate("interval").catch((err) => {
|
|
131
|
+
log.warn(`qmd update failed (${String(err)})`);
|
|
132
|
+
});
|
|
133
|
+
}, this.qmd.update.intervalMs);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
bootstrapCollections() {
|
|
137
|
+
this.collectionRoots.clear();
|
|
138
|
+
this.sources.clear();
|
|
139
|
+
for (const collection of this.qmd.collections) {
|
|
140
|
+
const kind = collection.kind === "sessions" ? "sessions" : "memory";
|
|
141
|
+
this.collectionRoots.set(collection.name, { path: collection.path, kind });
|
|
142
|
+
this.sources.add(kind);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async ensureCollections() {
|
|
146
|
+
// QMD collections are persisted inside the index database and must be created
|
|
147
|
+
// via the CLI. Prefer listing existing collections when supported, otherwise
|
|
148
|
+
// fall back to best-effort idempotent `qmd collection add`.
|
|
149
|
+
const existing = new Map();
|
|
150
|
+
try {
|
|
151
|
+
const result = await this.runQmd(["collection", "list", "--json"], {
|
|
152
|
+
timeoutMs: this.qmd.update.commandTimeoutMs,
|
|
153
|
+
});
|
|
154
|
+
const parsed = JSON.parse(result.stdout);
|
|
155
|
+
if (Array.isArray(parsed)) {
|
|
156
|
+
for (const entry of parsed) {
|
|
157
|
+
if (typeof entry === "string") {
|
|
158
|
+
existing.set(entry, {});
|
|
159
|
+
}
|
|
160
|
+
else if (entry && typeof entry === "object") {
|
|
161
|
+
const name = entry.name;
|
|
162
|
+
if (typeof name === "string") {
|
|
163
|
+
const listedPath = entry.path;
|
|
164
|
+
const listedPattern = entry.pattern;
|
|
165
|
+
const listedMask = entry.mask;
|
|
166
|
+
existing.set(name, {
|
|
167
|
+
path: typeof listedPath === "string" ? listedPath : undefined,
|
|
168
|
+
pattern: typeof listedPattern === "string"
|
|
169
|
+
? listedPattern
|
|
170
|
+
: typeof listedMask === "string"
|
|
171
|
+
? listedMask
|
|
172
|
+
: undefined,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// ignore; older qmd versions might not support list --json.
|
|
181
|
+
}
|
|
182
|
+
for (const collection of this.qmd.collections) {
|
|
183
|
+
const listed = existing.get(collection.name);
|
|
184
|
+
if (listed && !this.shouldRebindCollection(collection, listed)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (listed) {
|
|
188
|
+
try {
|
|
189
|
+
await this.removeCollection(collection.name);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
if (!this.isCollectionMissingError(message)) {
|
|
194
|
+
log.warn(`qmd collection remove failed for ${collection.name}: ${message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await this.ensureCollectionPath(collection);
|
|
200
|
+
await this.addCollection(collection.path, collection.name, collection.pattern);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
204
|
+
if (this.isCollectionAlreadyExistsError(message)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async ensureCollectionPath(collection) {
|
|
212
|
+
if (!this.isDirectoryGlobPattern(collection.pattern)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await fs.mkdir(collection.path, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
isDirectoryGlobPattern(pattern) {
|
|
218
|
+
return pattern.includes("*") || pattern.includes("?") || pattern.includes("[");
|
|
219
|
+
}
|
|
220
|
+
isCollectionAlreadyExistsError(message) {
|
|
221
|
+
const lower = message.toLowerCase();
|
|
222
|
+
return lower.includes("already exists") || lower.includes("exists");
|
|
223
|
+
}
|
|
224
|
+
isCollectionMissingError(message) {
|
|
225
|
+
const lower = message.toLowerCase();
|
|
226
|
+
return (lower.includes("not found") || lower.includes("does not exist") || lower.includes("missing"));
|
|
227
|
+
}
|
|
228
|
+
async addCollection(pathArg, name, pattern) {
|
|
229
|
+
await this.runQmd(["collection", "add", pathArg, "--name", name, "--mask", pattern], {
|
|
230
|
+
timeoutMs: this.qmd.update.commandTimeoutMs,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
async removeCollection(name) {
|
|
234
|
+
await this.runQmd(["collection", "remove", name], {
|
|
235
|
+
timeoutMs: this.qmd.update.commandTimeoutMs,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
shouldRebindCollection(collection, listed) {
|
|
239
|
+
if (!listed.path) {
|
|
240
|
+
// Older qmd versions may only return names from `collection list --json`.
|
|
241
|
+
// Force sessions collections to rebind so per-agent session export paths stay isolated.
|
|
242
|
+
return collection.kind === "sessions";
|
|
243
|
+
}
|
|
244
|
+
if (!this.pathsMatch(listed.path, collection.path)) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (typeof listed.pattern === "string" && listed.pattern !== collection.pattern) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
pathsMatch(left, right) {
|
|
253
|
+
const normalize = (value) => {
|
|
254
|
+
const resolved = path.isAbsolute(value)
|
|
255
|
+
? path.resolve(value)
|
|
256
|
+
: path.resolve(this.workspaceDir, value);
|
|
257
|
+
const normalized = path.normalize(resolved);
|
|
258
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
259
|
+
};
|
|
260
|
+
return normalize(left) === normalize(right);
|
|
261
|
+
}
|
|
262
|
+
shouldRepairNullByteCollectionError(err) {
|
|
263
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
264
|
+
const lower = message.toLowerCase();
|
|
265
|
+
return ((lower.includes("enotdir") || lower.includes("not a directory")) &&
|
|
266
|
+
NUL_MARKER_RE.test(message));
|
|
267
|
+
}
|
|
268
|
+
async tryRepairNullByteCollections(err, reason) {
|
|
269
|
+
if (this.attemptedNullByteCollectionRepair) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (!this.shouldRepairNullByteCollectionError(err)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
this.attemptedNullByteCollectionRepair = true;
|
|
276
|
+
log.warn(`qmd update failed with suspected null-byte collection metadata (${reason}); rebuilding managed collections and retrying once`);
|
|
277
|
+
for (const collection of this.qmd.collections) {
|
|
278
|
+
try {
|
|
279
|
+
await this.removeCollection(collection.name);
|
|
280
|
+
}
|
|
281
|
+
catch (removeErr) {
|
|
282
|
+
const removeMessage = removeErr instanceof Error ? removeErr.message : String(removeErr);
|
|
283
|
+
if (!this.isCollectionMissingError(removeMessage)) {
|
|
284
|
+
log.warn(`qmd collection remove failed for ${collection.name}: ${removeMessage}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
await this.addCollection(collection.path, collection.name, collection.pattern);
|
|
289
|
+
}
|
|
290
|
+
catch (addErr) {
|
|
291
|
+
const addMessage = addErr instanceof Error ? addErr.message : String(addErr);
|
|
292
|
+
if (!this.isCollectionAlreadyExistsError(addMessage)) {
|
|
293
|
+
log.warn(`qmd collection add failed for ${collection.name}: ${addMessage}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
async search(query, opts) {
|
|
300
|
+
if (!this.isScopeAllowed(opts?.sessionKey)) {
|
|
301
|
+
this.logScopeDenied(opts?.sessionKey);
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
const trimmed = query.trim();
|
|
305
|
+
if (!trimmed) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
await this.waitForPendingUpdateBeforeSearch();
|
|
309
|
+
const limit = Math.min(this.qmd.limits.maxResults, opts?.maxResults ?? this.qmd.limits.maxResults);
|
|
310
|
+
const collectionNames = this.listManagedCollectionNames();
|
|
311
|
+
if (collectionNames.length === 0) {
|
|
312
|
+
log.warn("qmd query skipped: no managed collections configured");
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
const qmdSearchCommand = this.qmd.searchMode ?? "query";
|
|
316
|
+
let parsed;
|
|
317
|
+
try {
|
|
318
|
+
if (qmdSearchCommand === "query" && collectionNames.length > 1) {
|
|
319
|
+
parsed = await this.runQueryAcrossCollections(trimmed, limit, collectionNames);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit);
|
|
323
|
+
args.push(...this.buildCollectionFilterArgs(collectionNames));
|
|
324
|
+
// Always scope to managed collections (default + custom). Even for `search`/`vsearch`,
|
|
325
|
+
// pass collection filters; if a given QMD build rejects these flags, we fall back to `query`.
|
|
326
|
+
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
|
327
|
+
parsed = parseQmdQueryJson(result.stdout, result.stderr);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
if (qmdSearchCommand !== "query" && this.isUnsupportedQmdOptionError(err)) {
|
|
332
|
+
log.warn(`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`);
|
|
333
|
+
try {
|
|
334
|
+
if (collectionNames.length > 1) {
|
|
335
|
+
parsed = await this.runQueryAcrossCollections(trimmed, limit, collectionNames);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const fallbackArgs = this.buildSearchArgs("query", trimmed, limit);
|
|
339
|
+
fallbackArgs.push(...this.buildCollectionFilterArgs(collectionNames));
|
|
340
|
+
const fallback = await this.runQmd(fallbackArgs, {
|
|
341
|
+
timeoutMs: this.qmd.limits.timeoutMs,
|
|
342
|
+
});
|
|
343
|
+
parsed = parseQmdQueryJson(fallback.stdout, fallback.stderr);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (fallbackErr) {
|
|
347
|
+
log.warn(`qmd query fallback failed: ${String(fallbackErr)}`);
|
|
348
|
+
throw fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
log.warn(`qmd ${qmdSearchCommand} failed: ${String(err)}`);
|
|
353
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const results = [];
|
|
357
|
+
for (const entry of parsed) {
|
|
358
|
+
const doc = await this.resolveDocLocation(entry.docid);
|
|
359
|
+
if (!doc) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const snippet = entry.snippet?.slice(0, this.qmd.limits.maxSnippetChars) ?? "";
|
|
363
|
+
const lines = this.extractSnippetLines(snippet);
|
|
364
|
+
const score = typeof entry.score === "number" ? entry.score : 0;
|
|
365
|
+
const minScore = opts?.minScore ?? 0;
|
|
366
|
+
if (score < minScore) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
results.push({
|
|
370
|
+
path: doc.rel,
|
|
371
|
+
startLine: lines.startLine,
|
|
372
|
+
endLine: lines.endLine,
|
|
373
|
+
score,
|
|
374
|
+
snippet,
|
|
375
|
+
source: doc.source,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return this.clampResultsByInjectedChars(results.slice(0, limit));
|
|
379
|
+
}
|
|
380
|
+
async sync(params) {
|
|
381
|
+
if (params?.progress) {
|
|
382
|
+
params.progress({ completed: 0, total: 1, label: "Updating QMD index…" });
|
|
383
|
+
}
|
|
384
|
+
await this.runUpdate(params?.reason ?? "manual", params?.force);
|
|
385
|
+
if (params?.progress) {
|
|
386
|
+
params.progress({ completed: 1, total: 1, label: "QMD index updated" });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async readFile(params) {
|
|
390
|
+
const relPath = params.relPath?.trim();
|
|
391
|
+
if (!relPath) {
|
|
392
|
+
throw new Error("path required");
|
|
393
|
+
}
|
|
394
|
+
const absPath = this.resolveReadPath(relPath);
|
|
395
|
+
if (!absPath.endsWith(".md")) {
|
|
396
|
+
throw new Error("path required");
|
|
397
|
+
}
|
|
398
|
+
const stat = await fs.lstat(absPath);
|
|
399
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
400
|
+
throw new Error("path required");
|
|
401
|
+
}
|
|
402
|
+
if (params.from !== undefined || params.lines !== undefined) {
|
|
403
|
+
const text = await this.readPartialText(absPath, params.from, params.lines);
|
|
404
|
+
return { text, path: relPath };
|
|
405
|
+
}
|
|
406
|
+
const content = await fs.readFile(absPath, "utf-8");
|
|
407
|
+
if (!params.from && !params.lines) {
|
|
408
|
+
return { text: content, path: relPath };
|
|
409
|
+
}
|
|
410
|
+
const lines = content.split("\n");
|
|
411
|
+
const start = Math.max(1, params.from ?? 1);
|
|
412
|
+
const count = Math.max(1, params.lines ?? lines.length);
|
|
413
|
+
const slice = lines.slice(start - 1, start - 1 + count);
|
|
414
|
+
return { text: slice.join("\n"), path: relPath };
|
|
415
|
+
}
|
|
416
|
+
status() {
|
|
417
|
+
const counts = this.readCounts();
|
|
418
|
+
return {
|
|
419
|
+
backend: "qmd",
|
|
420
|
+
provider: "qmd",
|
|
421
|
+
model: "qmd",
|
|
422
|
+
requestedProvider: "qmd",
|
|
423
|
+
files: counts.totalDocuments,
|
|
424
|
+
chunks: counts.totalDocuments,
|
|
425
|
+
dirty: false,
|
|
426
|
+
workspaceDir: this.workspaceDir,
|
|
427
|
+
dbPath: this.indexPath,
|
|
428
|
+
sources: Array.from(this.sources),
|
|
429
|
+
sourceCounts: counts.sourceCounts,
|
|
430
|
+
vector: { enabled: true, available: true },
|
|
431
|
+
batch: {
|
|
432
|
+
enabled: false,
|
|
433
|
+
failures: 0,
|
|
434
|
+
limit: 0,
|
|
435
|
+
wait: false,
|
|
436
|
+
concurrency: 0,
|
|
437
|
+
pollIntervalMs: 0,
|
|
438
|
+
timeoutMs: 0,
|
|
439
|
+
},
|
|
440
|
+
custom: {
|
|
441
|
+
qmd: {
|
|
442
|
+
collections: this.qmd.collections.length,
|
|
443
|
+
lastUpdateAt: this.lastUpdateAt,
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async probeEmbeddingAvailability() {
|
|
449
|
+
return { ok: true };
|
|
450
|
+
}
|
|
451
|
+
async probeVectorAvailability() {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
async close() {
|
|
455
|
+
if (this.closed) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.closed = true;
|
|
459
|
+
if (this.updateTimer) {
|
|
460
|
+
clearInterval(this.updateTimer);
|
|
461
|
+
this.updateTimer = null;
|
|
462
|
+
}
|
|
463
|
+
this.queuedForcedRuns = 0;
|
|
464
|
+
await this.pendingUpdate?.catch(() => undefined);
|
|
465
|
+
await this.queuedForcedUpdate?.catch(() => undefined);
|
|
466
|
+
if (this.db) {
|
|
467
|
+
this.db.close();
|
|
468
|
+
this.db = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async runUpdate(reason, force, opts) {
|
|
472
|
+
if (this.closed) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (this.pendingUpdate) {
|
|
476
|
+
if (force) {
|
|
477
|
+
return this.enqueueForcedUpdate(reason);
|
|
478
|
+
}
|
|
479
|
+
return this.pendingUpdate;
|
|
480
|
+
}
|
|
481
|
+
if (this.queuedForcedUpdate && !opts?.fromForcedQueue) {
|
|
482
|
+
if (force) {
|
|
483
|
+
return this.enqueueForcedUpdate(reason);
|
|
484
|
+
}
|
|
485
|
+
return this.queuedForcedUpdate;
|
|
486
|
+
}
|
|
487
|
+
if (this.shouldSkipUpdate(force)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const run = async () => {
|
|
491
|
+
if (this.sessionExporter) {
|
|
492
|
+
await this.exportSessions();
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
await this.runQmd(["update"], { timeoutMs: this.qmd.update.updateTimeoutMs });
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
if (!(await this.tryRepairNullByteCollections(err, reason))) {
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
await this.runQmd(["update"], { timeoutMs: this.qmd.update.updateTimeoutMs });
|
|
502
|
+
}
|
|
503
|
+
const embedIntervalMs = this.qmd.update.embedIntervalMs;
|
|
504
|
+
const shouldEmbed = Boolean(force) ||
|
|
505
|
+
this.lastEmbedAt === null ||
|
|
506
|
+
(embedIntervalMs > 0 && Date.now() - this.lastEmbedAt > embedIntervalMs);
|
|
507
|
+
if (shouldEmbed) {
|
|
508
|
+
try {
|
|
509
|
+
await this.runQmd(["embed"], { timeoutMs: this.qmd.update.embedTimeoutMs });
|
|
510
|
+
this.lastEmbedAt = Date.now();
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
log.warn(`qmd embed failed (${reason}): ${String(err)}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
this.lastUpdateAt = Date.now();
|
|
517
|
+
this.docPathCache.clear();
|
|
518
|
+
};
|
|
519
|
+
this.pendingUpdate = run().finally(() => {
|
|
520
|
+
this.pendingUpdate = null;
|
|
521
|
+
});
|
|
522
|
+
await this.pendingUpdate;
|
|
523
|
+
}
|
|
524
|
+
enqueueForcedUpdate(reason) {
|
|
525
|
+
this.queuedForcedRuns += 1;
|
|
526
|
+
if (!this.queuedForcedUpdate) {
|
|
527
|
+
this.queuedForcedUpdate = this.drainForcedUpdates(reason).finally(() => {
|
|
528
|
+
this.queuedForcedUpdate = null;
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return this.queuedForcedUpdate;
|
|
532
|
+
}
|
|
533
|
+
async drainForcedUpdates(reason) {
|
|
534
|
+
await this.pendingUpdate?.catch(() => undefined);
|
|
535
|
+
while (!this.closed && this.queuedForcedRuns > 0) {
|
|
536
|
+
this.queuedForcedRuns -= 1;
|
|
537
|
+
await this.runUpdate(`${reason}:queued`, true, { fromForcedQueue: true });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Symlink the default QMD models directory into our custom XDG_CACHE_HOME so
|
|
542
|
+
* that the pre-installed ML models (~/.cache/qmd/models/) are reused rather
|
|
543
|
+
* than re-downloaded for every agent. If the default models directory does
|
|
544
|
+
* not exist, or a models directory/symlink already exists in the target, this
|
|
545
|
+
* is a no-op.
|
|
546
|
+
*/
|
|
547
|
+
async symlinkSharedModels() {
|
|
548
|
+
// process.env is never modified — only this.env (passed to child_process
|
|
549
|
+
// spawn) overrides XDG_CACHE_HOME. So reading it here gives us the
|
|
550
|
+
// user's original value, which is where `qmd` downloaded its models.
|
|
551
|
+
//
|
|
552
|
+
// On Windows, well-behaved apps (including Rust `dirs` / Go os.UserCacheDir)
|
|
553
|
+
// store caches under %LOCALAPPDATA% rather than ~/.cache. Fall back to
|
|
554
|
+
// LOCALAPPDATA when XDG_CACHE_HOME is not set on Windows.
|
|
555
|
+
const defaultCacheHome = process.env.XDG_CACHE_HOME ||
|
|
556
|
+
(process.platform === "win32" ? process.env.LOCALAPPDATA : undefined) ||
|
|
557
|
+
path.join(os.homedir(), ".cache");
|
|
558
|
+
const defaultModelsDir = path.join(defaultCacheHome, "qmd", "models");
|
|
559
|
+
const targetModelsDir = path.join(this.xdgCacheHome, "qmd", "models");
|
|
560
|
+
try {
|
|
561
|
+
// Check if the default models directory exists.
|
|
562
|
+
// Missing path is normal on first run and should be silent.
|
|
563
|
+
const stat = await fs.stat(defaultModelsDir).catch((err) => {
|
|
564
|
+
if (err.code === "ENOENT") {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
throw err;
|
|
568
|
+
});
|
|
569
|
+
if (!stat?.isDirectory()) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// Check if something already exists at the target path
|
|
573
|
+
try {
|
|
574
|
+
await fs.lstat(targetModelsDir);
|
|
575
|
+
// Already exists (directory, symlink, or file) – leave it alone
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// Does not exist – proceed to create symlink
|
|
580
|
+
}
|
|
581
|
+
// On Windows, creating directory symlinks requires either Administrator
|
|
582
|
+
// privileges or Developer Mode. Fall back to a directory junction which
|
|
583
|
+
// works without elevated privileges (junctions are always absolute-path,
|
|
584
|
+
// which is fine here since both paths are already absolute).
|
|
585
|
+
try {
|
|
586
|
+
await fs.symlink(defaultModelsDir, targetModelsDir, "dir");
|
|
587
|
+
}
|
|
588
|
+
catch (symlinkErr) {
|
|
589
|
+
const code = symlinkErr.code;
|
|
590
|
+
if (process.platform === "win32" && (code === "EPERM" || code === "ENOTSUP")) {
|
|
591
|
+
await fs.symlink(defaultModelsDir, targetModelsDir, "junction");
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
throw symlinkErr;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
log.debug(`symlinked qmd models: ${defaultModelsDir} → ${targetModelsDir}`);
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
// Non-fatal: if we can't symlink, qmd will fall back to downloading
|
|
601
|
+
log.warn(`failed to symlink qmd models directory: ${String(err)}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async runQmd(args, opts) {
|
|
605
|
+
return await new Promise((resolve, reject) => {
|
|
606
|
+
const child = spawn(this.qmd.command, args, {
|
|
607
|
+
env: this.env,
|
|
608
|
+
cwd: this.workspaceDir,
|
|
609
|
+
});
|
|
610
|
+
let stdout = "";
|
|
611
|
+
let stderr = "";
|
|
612
|
+
let stdoutTruncated = false;
|
|
613
|
+
let stderrTruncated = false;
|
|
614
|
+
const timer = opts?.timeoutMs
|
|
615
|
+
? setTimeout(() => {
|
|
616
|
+
child.kill("SIGKILL");
|
|
617
|
+
reject(new Error(`qmd ${args.join(" ")} timed out after ${opts.timeoutMs}ms`));
|
|
618
|
+
}, opts.timeoutMs)
|
|
619
|
+
: null;
|
|
620
|
+
child.stdout.on("data", (data) => {
|
|
621
|
+
const next = appendOutputWithCap(stdout, data.toString("utf8"), this.maxQmdOutputChars);
|
|
622
|
+
stdout = next.text;
|
|
623
|
+
stdoutTruncated = stdoutTruncated || next.truncated;
|
|
624
|
+
});
|
|
625
|
+
child.stderr.on("data", (data) => {
|
|
626
|
+
const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
|
|
627
|
+
stderr = next.text;
|
|
628
|
+
stderrTruncated = stderrTruncated || next.truncated;
|
|
629
|
+
});
|
|
630
|
+
child.on("error", (err) => {
|
|
631
|
+
if (timer) {
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
}
|
|
634
|
+
reject(err);
|
|
635
|
+
});
|
|
636
|
+
child.on("close", (code) => {
|
|
637
|
+
if (timer) {
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
}
|
|
640
|
+
if (stdoutTruncated || stderrTruncated) {
|
|
641
|
+
reject(new Error(`qmd ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (code === 0) {
|
|
645
|
+
resolve({ stdout, stderr });
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
reject(new Error(`qmd ${args.join(" ")} failed (code ${code}): ${stderr || stdout}`));
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async readPartialText(absPath, from, lines) {
|
|
654
|
+
const start = Math.max(1, from ?? 1);
|
|
655
|
+
const count = Math.max(1, lines ?? Number.POSITIVE_INFINITY);
|
|
656
|
+
const handle = await fs.open(absPath);
|
|
657
|
+
const stream = handle.createReadStream({ encoding: "utf-8" });
|
|
658
|
+
const rl = readline.createInterface({
|
|
659
|
+
input: stream,
|
|
660
|
+
crlfDelay: Infinity,
|
|
661
|
+
});
|
|
662
|
+
const selected = [];
|
|
663
|
+
let index = 0;
|
|
664
|
+
try {
|
|
665
|
+
for await (const line of rl) {
|
|
666
|
+
index += 1;
|
|
667
|
+
if (index < start) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (selected.length >= count) {
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
selected.push(line);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
rl.close();
|
|
678
|
+
await handle.close();
|
|
679
|
+
}
|
|
680
|
+
return selected.slice(0, count).join("\n");
|
|
681
|
+
}
|
|
682
|
+
ensureDb() {
|
|
683
|
+
if (this.db) {
|
|
684
|
+
return this.db;
|
|
685
|
+
}
|
|
686
|
+
const { DatabaseSync } = requireNodeSqlite();
|
|
687
|
+
this.db = new DatabaseSync(this.indexPath, { readOnly: true });
|
|
688
|
+
// Keep QMD recall responsive when the updater holds a write lock.
|
|
689
|
+
this.db.exec("PRAGMA busy_timeout = 1");
|
|
690
|
+
return this.db;
|
|
691
|
+
}
|
|
692
|
+
async exportSessions() {
|
|
693
|
+
if (!this.sessionExporter) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const exportDir = this.sessionExporter.dir;
|
|
697
|
+
await fs.mkdir(exportDir, { recursive: true });
|
|
698
|
+
const files = await listSessionFilesForAgent(this.agentId);
|
|
699
|
+
const keep = new Set();
|
|
700
|
+
const tracked = new Set();
|
|
701
|
+
const cutoff = this.sessionExporter.retentionMs
|
|
702
|
+
? Date.now() - this.sessionExporter.retentionMs
|
|
703
|
+
: null;
|
|
704
|
+
for (const sessionFile of files) {
|
|
705
|
+
const entry = await buildSessionEntry(sessionFile);
|
|
706
|
+
if (!entry) {
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (cutoff && entry.mtimeMs < cutoff) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const target = path.join(exportDir, `${path.basename(sessionFile, ".jsonl")}.md`);
|
|
713
|
+
tracked.add(sessionFile);
|
|
714
|
+
const state = this.exportedSessionState.get(sessionFile);
|
|
715
|
+
if (!state || state.hash !== entry.hash || state.mtimeMs !== entry.mtimeMs) {
|
|
716
|
+
await fs.writeFile(target, this.renderSessionMarkdown(entry), "utf-8");
|
|
717
|
+
}
|
|
718
|
+
this.exportedSessionState.set(sessionFile, {
|
|
719
|
+
hash: entry.hash,
|
|
720
|
+
mtimeMs: entry.mtimeMs,
|
|
721
|
+
target,
|
|
722
|
+
});
|
|
723
|
+
keep.add(target);
|
|
724
|
+
}
|
|
725
|
+
const exported = await fs.readdir(exportDir).catch(() => []);
|
|
726
|
+
for (const name of exported) {
|
|
727
|
+
if (!name.endsWith(".md")) {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const full = path.join(exportDir, name);
|
|
731
|
+
if (!keep.has(full)) {
|
|
732
|
+
await fs.rm(full, { force: true });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const [sessionFile, state] of this.exportedSessionState) {
|
|
736
|
+
if (!tracked.has(sessionFile) || !state.target.startsWith(exportDir + path.sep)) {
|
|
737
|
+
this.exportedSessionState.delete(sessionFile);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
renderSessionMarkdown(entry) {
|
|
742
|
+
const header = `# Session ${path.basename(entry.absPath, path.extname(entry.absPath))}`;
|
|
743
|
+
const body = entry.content?.trim().length ? entry.content.trim() : "(empty)";
|
|
744
|
+
return `${header}\n\n${body}\n`;
|
|
745
|
+
}
|
|
746
|
+
pickSessionCollectionName() {
|
|
747
|
+
const existing = new Set(this.qmd.collections.map((collection) => collection.name));
|
|
748
|
+
const base = `sessions-${this.sanitizeCollectionNameSegment(this.agentId)}`;
|
|
749
|
+
if (!existing.has(base)) {
|
|
750
|
+
return base;
|
|
751
|
+
}
|
|
752
|
+
let counter = 2;
|
|
753
|
+
let candidate = `${base}-${counter}`;
|
|
754
|
+
while (existing.has(candidate)) {
|
|
755
|
+
counter += 1;
|
|
756
|
+
candidate = `${base}-${counter}`;
|
|
757
|
+
}
|
|
758
|
+
return candidate;
|
|
759
|
+
}
|
|
760
|
+
sanitizeCollectionNameSegment(input) {
|
|
761
|
+
const lower = input.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
762
|
+
const trimmed = lower.replace(/^-+|-+$/g, "");
|
|
763
|
+
return trimmed || "agent";
|
|
764
|
+
}
|
|
765
|
+
async resolveDocLocation(docid) {
|
|
766
|
+
if (!docid) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
const normalized = docid.startsWith("#") ? docid.slice(1) : docid;
|
|
770
|
+
if (!normalized) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
const cached = this.docPathCache.get(normalized);
|
|
774
|
+
if (cached) {
|
|
775
|
+
return cached;
|
|
776
|
+
}
|
|
777
|
+
const db = this.ensureDb();
|
|
778
|
+
let row;
|
|
779
|
+
try {
|
|
780
|
+
const exact = db
|
|
781
|
+
.prepare("SELECT collection, path FROM documents WHERE hash = ? AND active = 1 LIMIT 1")
|
|
782
|
+
.get(normalized);
|
|
783
|
+
row = exact;
|
|
784
|
+
if (!row) {
|
|
785
|
+
row = db
|
|
786
|
+
.prepare("SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1 LIMIT 1")
|
|
787
|
+
.get(`${normalized}%`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
if (this.isSqliteBusyError(err)) {
|
|
792
|
+
log.debug(`qmd index is busy while resolving doc path: ${String(err)}`);
|
|
793
|
+
throw this.createQmdBusyError(err);
|
|
794
|
+
}
|
|
795
|
+
throw err;
|
|
796
|
+
}
|
|
797
|
+
if (!row) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
const location = this.toDocLocation(row.collection, row.path);
|
|
801
|
+
if (!location) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
this.docPathCache.set(normalized, location);
|
|
805
|
+
return location;
|
|
806
|
+
}
|
|
807
|
+
extractSnippetLines(snippet) {
|
|
808
|
+
const match = SNIPPET_HEADER_RE.exec(snippet);
|
|
809
|
+
if (match) {
|
|
810
|
+
const start = Number(match[1]);
|
|
811
|
+
const count = Number(match[2]);
|
|
812
|
+
if (Number.isFinite(start) && Number.isFinite(count)) {
|
|
813
|
+
return { startLine: start, endLine: start + count - 1 };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const lines = snippet.split("\n").length;
|
|
817
|
+
return { startLine: 1, endLine: lines };
|
|
818
|
+
}
|
|
819
|
+
readCounts() {
|
|
820
|
+
try {
|
|
821
|
+
const db = this.ensureDb();
|
|
822
|
+
const rows = db
|
|
823
|
+
.prepare("SELECT collection, COUNT(*) as c FROM documents WHERE active = 1 GROUP BY collection")
|
|
824
|
+
.all();
|
|
825
|
+
const bySource = new Map();
|
|
826
|
+
for (const source of this.sources) {
|
|
827
|
+
bySource.set(source, { files: 0, chunks: 0 });
|
|
828
|
+
}
|
|
829
|
+
let total = 0;
|
|
830
|
+
for (const row of rows) {
|
|
831
|
+
const root = this.collectionRoots.get(row.collection);
|
|
832
|
+
const source = root?.kind ?? "memory";
|
|
833
|
+
const entry = bySource.get(source) ?? { files: 0, chunks: 0 };
|
|
834
|
+
entry.files += row.c ?? 0;
|
|
835
|
+
entry.chunks += row.c ?? 0;
|
|
836
|
+
bySource.set(source, entry);
|
|
837
|
+
total += row.c ?? 0;
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
totalDocuments: total,
|
|
841
|
+
sourceCounts: Array.from(bySource.entries()).map(([source, value]) => ({
|
|
842
|
+
source,
|
|
843
|
+
files: value.files,
|
|
844
|
+
chunks: value.chunks,
|
|
845
|
+
})),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
catch (err) {
|
|
849
|
+
log.warn(`failed to read qmd index stats: ${String(err)}`);
|
|
850
|
+
return {
|
|
851
|
+
totalDocuments: 0,
|
|
852
|
+
sourceCounts: Array.from(this.sources).map((source) => ({ source, files: 0, chunks: 0 })),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
logScopeDenied(sessionKey) {
|
|
857
|
+
const channel = deriveQmdScopeChannel(sessionKey) ?? "unknown";
|
|
858
|
+
const chatType = deriveQmdScopeChatType(sessionKey) ?? "unknown";
|
|
859
|
+
const key = sessionKey?.trim() || "<none>";
|
|
860
|
+
log.warn(`qmd search denied by scope (channel=${channel}, chatType=${chatType}, session=${key})`);
|
|
861
|
+
}
|
|
862
|
+
isScopeAllowed(sessionKey) {
|
|
863
|
+
return isQmdScopeAllowed(this.qmd.scope, sessionKey);
|
|
864
|
+
}
|
|
865
|
+
toDocLocation(collection, collectionRelativePath) {
|
|
866
|
+
const root = this.collectionRoots.get(collection);
|
|
867
|
+
if (!root) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
const normalizedRelative = collectionRelativePath.replace(/\\/g, "/");
|
|
871
|
+
const absPath = path.normalize(path.resolve(root.path, collectionRelativePath));
|
|
872
|
+
const relativeToWorkspace = path.relative(this.workspaceDir, absPath);
|
|
873
|
+
const relPath = this.buildSearchPath(collection, normalizedRelative, relativeToWorkspace, absPath);
|
|
874
|
+
return { rel: relPath, abs: absPath, source: root.kind };
|
|
875
|
+
}
|
|
876
|
+
buildSearchPath(collection, collectionRelativePath, relativeToWorkspace, absPath) {
|
|
877
|
+
const insideWorkspace = this.isInsideWorkspace(relativeToWorkspace);
|
|
878
|
+
if (insideWorkspace) {
|
|
879
|
+
const normalized = relativeToWorkspace.replace(/\\/g, "/");
|
|
880
|
+
if (!normalized) {
|
|
881
|
+
return path.basename(absPath);
|
|
882
|
+
}
|
|
883
|
+
return normalized;
|
|
884
|
+
}
|
|
885
|
+
const sanitized = collectionRelativePath.replace(/^\/+/, "");
|
|
886
|
+
return `qmd/${collection}/${sanitized}`;
|
|
887
|
+
}
|
|
888
|
+
isInsideWorkspace(relativePath) {
|
|
889
|
+
if (!relativePath) {
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
if (relativePath.startsWith("..")) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (relativePath.startsWith(`..${path.sep}`)) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return !path.isAbsolute(relativePath);
|
|
899
|
+
}
|
|
900
|
+
resolveReadPath(relPath) {
|
|
901
|
+
if (relPath.startsWith("qmd/")) {
|
|
902
|
+
const [, collection, ...rest] = relPath.split("/");
|
|
903
|
+
if (!collection || rest.length === 0) {
|
|
904
|
+
throw new Error("invalid qmd path");
|
|
905
|
+
}
|
|
906
|
+
const root = this.collectionRoots.get(collection);
|
|
907
|
+
if (!root) {
|
|
908
|
+
throw new Error(`unknown qmd collection: ${collection}`);
|
|
909
|
+
}
|
|
910
|
+
const joined = rest.join("/");
|
|
911
|
+
const resolved = path.resolve(root.path, joined);
|
|
912
|
+
if (!this.isWithinRoot(root.path, resolved)) {
|
|
913
|
+
throw new Error("qmd path escapes collection");
|
|
914
|
+
}
|
|
915
|
+
return resolved;
|
|
916
|
+
}
|
|
917
|
+
const absPath = path.resolve(this.workspaceDir, relPath);
|
|
918
|
+
if (!this.isWithinWorkspace(absPath)) {
|
|
919
|
+
throw new Error("path escapes workspace");
|
|
920
|
+
}
|
|
921
|
+
return absPath;
|
|
922
|
+
}
|
|
923
|
+
isWithinWorkspace(absPath) {
|
|
924
|
+
const normalizedWorkspace = this.workspaceDir.endsWith(path.sep)
|
|
925
|
+
? this.workspaceDir
|
|
926
|
+
: `${this.workspaceDir}${path.sep}`;
|
|
927
|
+
if (absPath === this.workspaceDir) {
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
const candidate = absPath.endsWith(path.sep) ? absPath : `${absPath}${path.sep}`;
|
|
931
|
+
return candidate.startsWith(normalizedWorkspace);
|
|
932
|
+
}
|
|
933
|
+
isWithinRoot(root, candidate) {
|
|
934
|
+
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
935
|
+
if (candidate === root) {
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
const next = candidate.endsWith(path.sep) ? candidate : `${candidate}${path.sep}`;
|
|
939
|
+
return next.startsWith(normalizedRoot);
|
|
940
|
+
}
|
|
941
|
+
clampResultsByInjectedChars(results) {
|
|
942
|
+
const budget = this.qmd.limits.maxInjectedChars;
|
|
943
|
+
if (!budget || budget <= 0) {
|
|
944
|
+
return results;
|
|
945
|
+
}
|
|
946
|
+
let remaining = budget;
|
|
947
|
+
const clamped = [];
|
|
948
|
+
for (const entry of results) {
|
|
949
|
+
if (remaining <= 0) {
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
const snippet = entry.snippet ?? "";
|
|
953
|
+
if (snippet.length <= remaining) {
|
|
954
|
+
clamped.push(entry);
|
|
955
|
+
remaining -= snippet.length;
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
const trimmed = snippet.slice(0, Math.max(0, remaining));
|
|
959
|
+
clamped.push({ ...entry, snippet: trimmed });
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return clamped;
|
|
964
|
+
}
|
|
965
|
+
shouldSkipUpdate(force) {
|
|
966
|
+
if (force) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
const debounceMs = this.qmd.update.debounceMs;
|
|
970
|
+
if (debounceMs <= 0) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
if (!this.lastUpdateAt) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
return Date.now() - this.lastUpdateAt < debounceMs;
|
|
977
|
+
}
|
|
978
|
+
isSqliteBusyError(err) {
|
|
979
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
980
|
+
const normalized = message.toLowerCase();
|
|
981
|
+
return normalized.includes("sqlite_busy") || normalized.includes("database is locked");
|
|
982
|
+
}
|
|
983
|
+
isUnsupportedQmdOptionError(err) {
|
|
984
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
985
|
+
const normalized = message.toLowerCase();
|
|
986
|
+
return (normalized.includes("unknown flag") ||
|
|
987
|
+
normalized.includes("unknown option") ||
|
|
988
|
+
normalized.includes("unrecognized option") ||
|
|
989
|
+
normalized.includes("flag provided but not defined") ||
|
|
990
|
+
normalized.includes("unexpected argument"));
|
|
991
|
+
}
|
|
992
|
+
createQmdBusyError(err) {
|
|
993
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
994
|
+
return new Error(`qmd index busy while reading results: ${message}`);
|
|
995
|
+
}
|
|
996
|
+
async waitForPendingUpdateBeforeSearch() {
|
|
997
|
+
const pending = this.pendingUpdate;
|
|
998
|
+
if (!pending) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
await Promise.race([
|
|
1002
|
+
pending.catch(() => undefined),
|
|
1003
|
+
new Promise((resolve) => setTimeout(resolve, SEARCH_PENDING_UPDATE_WAIT_MS)),
|
|
1004
|
+
]);
|
|
1005
|
+
}
|
|
1006
|
+
async runQueryAcrossCollections(query, limit, collectionNames) {
|
|
1007
|
+
log.debug(`qmd query multi-collection workaround active (${collectionNames.length} collections)`);
|
|
1008
|
+
const bestByDocId = new Map();
|
|
1009
|
+
for (const collectionName of collectionNames) {
|
|
1010
|
+
const args = this.buildSearchArgs("query", query, limit);
|
|
1011
|
+
args.push("-c", collectionName);
|
|
1012
|
+
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
|
|
1013
|
+
const parsed = parseQmdQueryJson(result.stdout, result.stderr);
|
|
1014
|
+
for (const entry of parsed) {
|
|
1015
|
+
if (typeof entry.docid !== "string" || !entry.docid.trim()) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const prev = bestByDocId.get(entry.docid);
|
|
1019
|
+
const prevScore = typeof prev?.score === "number" ? prev.score : Number.NEGATIVE_INFINITY;
|
|
1020
|
+
const nextScore = typeof entry.score === "number" ? entry.score : Number.NEGATIVE_INFINITY;
|
|
1021
|
+
if (!prev || nextScore > prevScore) {
|
|
1022
|
+
bestByDocId.set(entry.docid, entry);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return [...bestByDocId.values()].toSorted((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
1027
|
+
}
|
|
1028
|
+
listManagedCollectionNames() {
|
|
1029
|
+
const seen = new Set();
|
|
1030
|
+
const names = [];
|
|
1031
|
+
for (const collection of this.qmd.collections) {
|
|
1032
|
+
const name = collection.name?.trim();
|
|
1033
|
+
if (!name || seen.has(name)) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
seen.add(name);
|
|
1037
|
+
names.push(name);
|
|
1038
|
+
}
|
|
1039
|
+
return names;
|
|
1040
|
+
}
|
|
1041
|
+
buildCollectionFilterArgs(collectionNames) {
|
|
1042
|
+
if (collectionNames.length === 0) {
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
const names = collectionNames.filter(Boolean);
|
|
1046
|
+
return names.flatMap((name) => ["-c", name]);
|
|
1047
|
+
}
|
|
1048
|
+
buildSearchArgs(command, query, limit) {
|
|
1049
|
+
if (command === "query") {
|
|
1050
|
+
return ["query", query, "--json", "-n", String(limit)];
|
|
1051
|
+
}
|
|
1052
|
+
return [command, query, "--json", "-n", String(limit)];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function appendOutputWithCap(current, chunk, maxChars) {
|
|
1056
|
+
const appended = current + chunk;
|
|
1057
|
+
if (appended.length <= maxChars) {
|
|
1058
|
+
return { text: appended, truncated: false };
|
|
1059
|
+
}
|
|
1060
|
+
return { text: appended.slice(-maxChars), truncated: true };
|
|
1061
|
+
}
|