mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// voice-runtime-fetcher.mjs
|
|
2
|
+
//
|
|
3
|
+
// Single-source whisper.cpp runtime resolution: every user converges on the
|
|
4
|
+
// same upstream binary, but the *variant* (CPU base vs cuBLAS-11.8 vs
|
|
5
|
+
// cuBLAS-12.4) is selected per machine from a deterministic CUDA-toolkit
|
|
6
|
+
// detection. No heuristics: a variant matches only when its required CUDA
|
|
7
|
+
// major version is present in the local toolkit (cublas64_*.dll discovered
|
|
8
|
+
// in standard install paths or CUDA_PATH). The base-cpu variant is the
|
|
9
|
+
// requires:null bucket and is selected when nothing else matches.
|
|
10
|
+
//
|
|
11
|
+
// Layout: <dataDir>/voice-runtime/whisper-<ver>-<variantId>/
|
|
12
|
+
// <dataDir>/voice-runtime/active-version
|
|
13
|
+
// <dataDir>/voice/models/<manifest.model.filename>
|
|
14
|
+
// Atomic swap: write active-version.tmp then rename → active-version.
|
|
15
|
+
// GC: removes stale whisper-* / staging-* dirs on every ensureWhisperRuntime.
|
|
16
|
+
//
|
|
17
|
+
// Public API:
|
|
18
|
+
// ensureWhisperRuntime(dataDir, onProgress?) → { whisperCmd, version, variantId }
|
|
19
|
+
// ensureWhisperModel(dataDir, onProgress?) → { modelPath, modelId, size }
|
|
20
|
+
// ensureFfmpegRuntime(dataDir, onProgress?) → { ffmpegPath, version }
|
|
21
|
+
// resolveManagedWhisperCmd(dataDir) → string | null (read-only check)
|
|
22
|
+
// resolveManagedWhisperModel(dataDir) → string | null (read-only check)
|
|
23
|
+
// resolveManagedFfmpegPath(dataDir) → string | null (read-only check)
|
|
24
|
+
// resolveVoiceRuntime(dataDir) → runtime descriptor; managed
|
|
25
|
+
// whisper.cpp only
|
|
26
|
+
|
|
27
|
+
import { createHash } from 'crypto'
|
|
28
|
+
import {
|
|
29
|
+
chmodSync, closeSync,
|
|
30
|
+
createReadStream, createWriteStream, existsSync, mkdirSync, openSync,
|
|
31
|
+
readFileSync, readdirSync, rmSync, writeFileSync,
|
|
32
|
+
} from 'fs'
|
|
33
|
+
import { setTimeout as sleep } from 'timers/promises'
|
|
34
|
+
import { readFile } from 'fs/promises'
|
|
35
|
+
import { join, dirname } from 'path'
|
|
36
|
+
import { fileURLToPath } from 'url'
|
|
37
|
+
import { pipeline } from 'stream/promises'
|
|
38
|
+
import { Readable, Transform } from 'stream'
|
|
39
|
+
import { spawnSync } from 'child_process'
|
|
40
|
+
import { createGunzip } from 'zlib'
|
|
41
|
+
import { renameWithRetrySync, writeFileAtomicSync } from '../../shared/atomic-file.mjs'
|
|
42
|
+
import { windowsProgramRoots, windowsSystemRoot } from '../../agent/orchestrator/tools/builtin/windows-roots.mjs'
|
|
43
|
+
|
|
44
|
+
const BUNDLED_MANIFEST_PATH = fileURLToPath(new URL('../data/voice-runtime-manifest.json', import.meta.url))
|
|
45
|
+
const MANIFEST_URL = 'https://raw.githubusercontent.com/trib-plugin/mixdog/main/src/channels/data/voice-runtime-manifest.json'
|
|
46
|
+
const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY'])
|
|
47
|
+
// Hard ceiling on how long _withInstallLock will defer to an existing
|
|
48
|
+
// lock holder before reclaiming. Installs download + unpack runtime
|
|
49
|
+
// binaries; 30 minutes is well beyond any legitimate install yet
|
|
50
|
+
// short enough that a stale/recycled/self pid cannot hang installers
|
|
51
|
+
// forever. The check is age-only fallback — a verified-dead pid is
|
|
52
|
+
// still reclaimed immediately.
|
|
53
|
+
const LOCK_MAX_AGE_MS = 30 * 60 * 1000
|
|
54
|
+
|
|
55
|
+
function _readInstallLockToken(lockPath) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = readFileSync(lockPath, 'utf8').trim()
|
|
58
|
+
if (!raw) return null
|
|
59
|
+
const [pidLine, tsLine = ''] = raw.split(/\r?\n/)
|
|
60
|
+
const pid = Number(pidLine)
|
|
61
|
+
const ts = Number(tsLine)
|
|
62
|
+
if (!Number.isFinite(pid) || pid <= 0) return null
|
|
63
|
+
return { pid, ts, token: `${pidLine}\n${tsLine}` }
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _installLockTokenMatches(lockPath, expectedPid, expectedTs, expectedToken) {
|
|
70
|
+
const current = _readInstallLockToken(lockPath)
|
|
71
|
+
return current?.pid === expectedPid &&
|
|
72
|
+
Object.is(current.ts, expectedTs) &&
|
|
73
|
+
current.token === expectedToken
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function platformKey() {
|
|
77
|
+
const os = process.platform === 'win32' ? 'win32' : process.platform
|
|
78
|
+
return `${os}-${process.arch}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function _withInstallLock(rootDir, lockName, fn, { pollMs = 250 } = {}) {
|
|
82
|
+
mkdirSync(rootDir, { recursive: true })
|
|
83
|
+
const lockPath = join(rootDir, `.${lockName}.lock`)
|
|
84
|
+
let fd = null
|
|
85
|
+
// Track when this caller first observed the lock as held by a live
|
|
86
|
+
// pid OTHER than this process. Once that wait crosses
|
|
87
|
+
// LOCK_MAX_AGE_MS we treat the lock as abandoned regardless of
|
|
88
|
+
// process.kill(pid, 0) — a recycled/unrelated pid can keep looking
|
|
89
|
+
// "alive" indefinitely without ever releasing this lockfile.
|
|
90
|
+
let foreignWaitSince = 0
|
|
91
|
+
let samePidWaitSince = 0
|
|
92
|
+
while (true) {
|
|
93
|
+
try {
|
|
94
|
+
fd = openSync(lockPath, 'wx')
|
|
95
|
+
// Record pid + acquire timestamp so a later waiter can age out a
|
|
96
|
+
// truly abandoned lock. Single-line `${pid}\n${ms}` keeps backward
|
|
97
|
+
// compat with the pid-only reader path (Number(raw) parses the
|
|
98
|
+
// first line). Older lockfiles without a timestamp simply have
|
|
99
|
+
// no age signal and fall back to pid-liveness alone.
|
|
100
|
+
try { writeFileSync(lockPath, `${process.pid}\n${Date.now()}`) } catch {}
|
|
101
|
+
break
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (!LOCK_WAIT_CODES.has(err.code)) throw err
|
|
104
|
+
try {
|
|
105
|
+
const holder = _readInstallLockToken(lockPath)
|
|
106
|
+
if (!holder) {
|
|
107
|
+
// empty or invalid PID — orphan lockfile, reclaim
|
|
108
|
+
try { rmSync(lockPath, { force: true }) } catch {}
|
|
109
|
+
foreignWaitSince = 0
|
|
110
|
+
samePidWaitSince = 0
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
const { pid: holderPid, ts: holderTs, token: holderToken } = holder
|
|
114
|
+
if (holderPid === process.pid) {
|
|
115
|
+
// Another concurrent install call within this same process holds
|
|
116
|
+
// the lock. Wait for its release() to remove the lockfile, then
|
|
117
|
+
// retry the wx-create so installs serialize instead of racing.
|
|
118
|
+
// Age fallback: a same-pid lockfile that survives past
|
|
119
|
+
// LOCK_MAX_AGE_MS without release() is stale. Timestamped
|
|
120
|
+
// locks use on-disk age; legacy pid-only locks use this
|
|
121
|
+
// waiter's first-observed time so PID reuse cannot hang forever.
|
|
122
|
+
const ageMs = Number.isFinite(holderTs) && holderTs > 0
|
|
123
|
+
? Date.now() - holderTs
|
|
124
|
+
: (samePidWaitSince ? Date.now() - samePidWaitSince : 0)
|
|
125
|
+
if (!Number.isFinite(holderTs) || holderTs <= 0) {
|
|
126
|
+
if (!samePidWaitSince) samePidWaitSince = Date.now()
|
|
127
|
+
}
|
|
128
|
+
if (ageMs > LOCK_MAX_AGE_MS) {
|
|
129
|
+
if (_installLockTokenMatches(lockPath, holderPid, holderTs, holderToken)) {
|
|
130
|
+
try { rmSync(lockPath, { force: true }) } catch {}
|
|
131
|
+
foreignWaitSince = 0
|
|
132
|
+
samePidWaitSince = 0
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await sleep(pollMs)
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
samePidWaitSince = 0
|
|
140
|
+
try { process.kill(holderPid, 0) }
|
|
141
|
+
catch {
|
|
142
|
+
try { rmSync(lockPath, { force: true }) } catch {}
|
|
143
|
+
foreignWaitSince = 0
|
|
144
|
+
samePidWaitSince = 0
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
// Live foreign pid. Apply age ceiling so a recycled/unrelated
|
|
148
|
+
// pid (e.g. a long-lived shell that happens to share the pid
|
|
149
|
+
// of a long-dead installer) cannot block installs forever.
|
|
150
|
+
// Prefer the on-disk timestamp; fall back to the first time
|
|
151
|
+
// THIS waiter saw the lock if the file predates the timestamp
|
|
152
|
+
// format.
|
|
153
|
+
const ageMs = Number.isFinite(holderTs) && holderTs > 0
|
|
154
|
+
? Date.now() - holderTs
|
|
155
|
+
: (foreignWaitSince ? Date.now() - foreignWaitSince : 0)
|
|
156
|
+
if (!foreignWaitSince) foreignWaitSince = Date.now()
|
|
157
|
+
if (ageMs > LOCK_MAX_AGE_MS) {
|
|
158
|
+
if (_installLockTokenMatches(lockPath, holderPid, holderTs, holderToken)) {
|
|
159
|
+
try { rmSync(lockPath, { force: true }) } catch {}
|
|
160
|
+
foreignWaitSince = 0
|
|
161
|
+
samePidWaitSince = 0
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {}
|
|
166
|
+
await sleep(pollMs)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let released = false
|
|
170
|
+
const release = () => {
|
|
171
|
+
if (released) return
|
|
172
|
+
released = true
|
|
173
|
+
try { if (fd != null) closeSync(fd) } catch {}
|
|
174
|
+
try { rmSync(lockPath, { force: true }) } catch {}
|
|
175
|
+
}
|
|
176
|
+
process.on('exit', release)
|
|
177
|
+
try { return await fn() } finally { release() }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function loadManifest(dataDir) {
|
|
181
|
+
const cachedPath = join(dataDir, 'voice-runtime', 'manifest.json')
|
|
182
|
+
if (existsSync(cachedPath)) {
|
|
183
|
+
try { return JSON.parse(readFileSync(cachedPath, 'utf8')) } catch {}
|
|
184
|
+
}
|
|
185
|
+
if (existsSync(BUNDLED_MANIFEST_PATH)) {
|
|
186
|
+
return JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
|
|
187
|
+
}
|
|
188
|
+
const res = await fetch(MANIFEST_URL, { signal: AbortSignal.timeout(30_000) })
|
|
189
|
+
if (!res.ok) throw new Error(`[voice-runtime] manifest fetch failed: ${res.status} ${res.statusText}`)
|
|
190
|
+
const manifest = await res.json()
|
|
191
|
+
mkdirSync(join(dataDir, 'voice-runtime'), { recursive: true })
|
|
192
|
+
writeFileSync(cachedPath, JSON.stringify(manifest, null, 2))
|
|
193
|
+
return manifest
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function sha256File(filePath) {
|
|
197
|
+
const data = await readFile(filePath)
|
|
198
|
+
return createHash('sha256').update(data).digest('hex')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function verifySha256(filePath, expected) {
|
|
202
|
+
const actual = await sha256File(filePath)
|
|
203
|
+
if (actual !== expected) {
|
|
204
|
+
throw new Error(`[voice-runtime] sha256 mismatch for ${filePath}: expected ${expected}, got ${actual}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Deterministic CUDA toolkit detection on Windows.
|
|
209
|
+
// Returns the set of CUDA major versions discoverable on this machine — a
|
|
210
|
+
// `cublas64_<major>.dll` file in any standard CUDA toolkit install dir or in
|
|
211
|
+
// CUDA_PATH. Anything not on disk doesn't count; no heuristics, no PATH-only
|
|
212
|
+
// guesses.
|
|
213
|
+
function detectCudaMajorsWin32() {
|
|
214
|
+
const found = new Set()
|
|
215
|
+
const dirs = []
|
|
216
|
+
|
|
217
|
+
const envKeys = Object.keys(process.env).filter(k =>
|
|
218
|
+
k === 'CUDA_PATH' || /^CUDA_PATH_V\d+_\d+$/i.test(k)
|
|
219
|
+
)
|
|
220
|
+
for (const k of envKeys) {
|
|
221
|
+
const p = process.env[k]
|
|
222
|
+
if (p) dirs.push(join(p, 'bin'))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const root of windowsProgramRoots()) {
|
|
226
|
+
const standardRoot = join(root, 'NVIDIA GPU Computing Toolkit', 'CUDA')
|
|
227
|
+
if (!existsSync(standardRoot)) continue
|
|
228
|
+
try {
|
|
229
|
+
for (const e of readdirSync(standardRoot)) {
|
|
230
|
+
if (/^v\d+\.\d+/.test(e)) dirs.push(join(standardRoot, e, 'bin'))
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const d of dirs) {
|
|
236
|
+
if (!existsSync(d)) continue
|
|
237
|
+
try {
|
|
238
|
+
for (const f of readdirSync(d)) {
|
|
239
|
+
const m = /^cublas64_(\d+)\.dll$/i.exec(f)
|
|
240
|
+
if (m) found.add(Number(m[1]))
|
|
241
|
+
}
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
return found
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Linux: scan LD_LIBRARY_PATH + standard paths for libcublas.so.<major>.
|
|
248
|
+
function detectCudaMajorsLinux() {
|
|
249
|
+
const found = new Set()
|
|
250
|
+
const dirs = []
|
|
251
|
+
const ldPath = process.env.LD_LIBRARY_PATH || ''
|
|
252
|
+
for (const p of ldPath.split(':')) { if (p) dirs.push(p) }
|
|
253
|
+
dirs.push(
|
|
254
|
+
'/usr/local/cuda/lib64',
|
|
255
|
+
'/usr/lib/x86_64-linux-gnu',
|
|
256
|
+
'/usr/lib/aarch64-linux-gnu',
|
|
257
|
+
)
|
|
258
|
+
const cudaPath = process.env.CUDA_PATH
|
|
259
|
+
if (cudaPath) dirs.push(cudaPath + '/lib64')
|
|
260
|
+
for (const d of dirs) {
|
|
261
|
+
if (!existsSync(d)) continue
|
|
262
|
+
try {
|
|
263
|
+
for (const f of readdirSync(d)) {
|
|
264
|
+
const m = /^libcublas\.so\.(\d+)$/.exec(f)
|
|
265
|
+
if (m) found.add(Number(m[1]))
|
|
266
|
+
}
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
return found
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function detectCudaMajors() {
|
|
273
|
+
if (process.platform === 'win32') return detectCudaMajorsWin32()
|
|
274
|
+
if (process.platform === 'darwin') {
|
|
275
|
+
// darwin uses Metal — CUDA not applicable, skip detection entirely.
|
|
276
|
+
return new Set()
|
|
277
|
+
}
|
|
278
|
+
// linux / WSL: probe for libcublas.so.<major>
|
|
279
|
+
return detectCudaMajorsLinux()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Deterministic NVIDIA driver presence check. The driver ships nvidia-smi.exe
|
|
283
|
+
// and nvml.dll into system32 whenever a supported card is detected and the
|
|
284
|
+
// user accepts the install. Either file's presence proves a usable driver —
|
|
285
|
+
// no nvidia-smi runtime invocation needed (avoids 50-200ms process spawn).
|
|
286
|
+
function hasNvidiaDriver() {
|
|
287
|
+
if (process.platform !== 'win32') return false
|
|
288
|
+
const sys = windowsSystemRoot()
|
|
289
|
+
if (!sys) return false
|
|
290
|
+
for (const f of ['System32\\nvidia-smi.exe', 'System32\\nvml.dll']) {
|
|
291
|
+
if (existsSync(join(sys, f))) return true
|
|
292
|
+
}
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Deterministic variant selection by explicit priority — manifest array order
|
|
297
|
+
// never influences the pick:
|
|
298
|
+
// 1. Highest matching CUDA major (CUDA > everything)
|
|
299
|
+
// 2. nvidia-driver generic (driver present, no toolkit required)
|
|
300
|
+
// 3. requires:null catch-all
|
|
301
|
+
function pickVariant(variants, env) {
|
|
302
|
+
if (!Array.isArray(variants) || variants.length === 0) return null
|
|
303
|
+
// Priority 1: highest matching CUDA major
|
|
304
|
+
let bestCuda = null
|
|
305
|
+
for (const v of variants) {
|
|
306
|
+
if (v.requires?.cudaMajor == null) continue
|
|
307
|
+
const major = Number(v.requires.cudaMajor)
|
|
308
|
+
if (env.cudaMajors.has(major)) {
|
|
309
|
+
if (bestCuda == null || major > Number(bestCuda.requires.cudaMajor)) bestCuda = v
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (bestCuda) return bestCuda
|
|
313
|
+
// Priority 2: driver-generic (nvidia driver present, no CUDA toolkit required)
|
|
314
|
+
const driverV = variants.find(v => v.requires?.nvidiaDriver === true && env.hasNvidiaDriver)
|
|
315
|
+
if (driverV) return driverV
|
|
316
|
+
// Priority 3: catch-all (requires: null)
|
|
317
|
+
return variants.find(v => v.requires == null) ?? null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Process bundled extras: download supplementary archives (e.g. NVIDIA
|
|
321
|
+
// cublas wheels) and lift selected files into the runtime directory.
|
|
322
|
+
async function processExtras(extras, stagingDir, onProgress) {
|
|
323
|
+
if (!Array.isArray(extras) || extras.length === 0) return
|
|
324
|
+
for (const extra of extras) {
|
|
325
|
+
const tag = createHash('sha256').update(extra.url).digest('hex').slice(0, 8)
|
|
326
|
+
const archivePath = join(stagingDir, `extra-${tag}.${extra.format}`)
|
|
327
|
+
process.stderr.write(`[voice-runtime] fetching extra ${tag} (${(extra.size / 1024 / 1024).toFixed(0)} MB) ...\n`)
|
|
328
|
+
await downloadFile(extra.url, archivePath, {
|
|
329
|
+
onProgress: onProgress ? (p) => onProgress({ phase: 'extra', ...p }) : null,
|
|
330
|
+
})
|
|
331
|
+
if (!extra.sha256) throw new Error(`[voice-runtime] manifest extra entry missing required sha256: ${extra.url}`)
|
|
332
|
+
await verifySha256(archivePath, extra.sha256)
|
|
333
|
+
|
|
334
|
+
const extractDir = join(stagingDir, `.extra-${tag}`)
|
|
335
|
+
mkdirSync(extractDir, { recursive: true })
|
|
336
|
+
extractZip(archivePath, extractDir)
|
|
337
|
+
|
|
338
|
+
for (const f of extra.files) {
|
|
339
|
+
const src = join(extractDir, f.from)
|
|
340
|
+
const dst = join(stagingDir, f.to)
|
|
341
|
+
if (!existsSync(src)) {
|
|
342
|
+
throw new Error(`[voice-runtime] extra ${tag}: expected file ${f.from} not present after extract`)
|
|
343
|
+
}
|
|
344
|
+
mkdirSync(dirname(dst), { recursive: true })
|
|
345
|
+
renameWithRetrySync(src, dst)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Reclaim disk space — only the lifted files are kept.
|
|
349
|
+
try { rmSync(extractDir, { recursive: true, force: true }) } catch {}
|
|
350
|
+
try { rmSync(archivePath, { force: true }) } catch {}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function gcStaleVersions(rootDir, activeName, prefix) {
|
|
355
|
+
if (!existsSync(rootDir)) return
|
|
356
|
+
for (const entry of readdirSync(rootDir)) {
|
|
357
|
+
if (entry === activeName || entry === 'active-version' || entry === 'manifest.json') continue
|
|
358
|
+
if (!entry.startsWith(prefix) && !entry.startsWith('staging-')) continue
|
|
359
|
+
try { rmSync(join(rootDir, entry), { recursive: true, force: true }) } catch {}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function extractGz(gzPath, destPath) {
|
|
364
|
+
await pipeline(createReadStream(gzPath), createGunzip(), createWriteStream(destPath))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Sweep .staging-* partials left by killed/crashed install attempts.
|
|
368
|
+
function gcStagingPartials(dir) {
|
|
369
|
+
if (!existsSync(dir)) return
|
|
370
|
+
try {
|
|
371
|
+
for (const entry of readdirSync(dir)) {
|
|
372
|
+
if (!entry.startsWith('.staging-')) continue
|
|
373
|
+
try { rmSync(join(dir, entry), { force: true }) } catch {}
|
|
374
|
+
}
|
|
375
|
+
} catch {}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function downloadFile(url, destPath, { onProgress = null, timeoutMs = 180_000 } = {}) {
|
|
379
|
+
// Default 180s ceiling: voice runtime tarball (ffmpeg/whisper) is < 100MB.
|
|
380
|
+
// Callers may raise timeoutMs (e.g. the ~1.5GB model). On any failure path
|
|
381
|
+
// the destPath is unlinked so the next attempt does not see a corrupt
|
|
382
|
+
// half-written archive.
|
|
383
|
+
try {
|
|
384
|
+
const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(timeoutMs) })
|
|
385
|
+
if (!res.ok) throw new Error(`[voice-runtime] download failed ${res.status} ${res.statusText} (${url})`)
|
|
386
|
+
if (!res.body) throw new Error(`[voice-runtime] download has no body (${url})`)
|
|
387
|
+
if (onProgress) {
|
|
388
|
+
const total = Number(res.headers.get('content-length')) || 0
|
|
389
|
+
let downloaded = 0
|
|
390
|
+
let lastEmit = 0
|
|
391
|
+
const emit = (force = false) => {
|
|
392
|
+
const now = Date.now()
|
|
393
|
+
if (!force && now - lastEmit < 200) return
|
|
394
|
+
lastEmit = now
|
|
395
|
+
onProgress({ downloaded, total })
|
|
396
|
+
}
|
|
397
|
+
const counter = new Transform({
|
|
398
|
+
transform(chunk, _enc, cb) {
|
|
399
|
+
downloaded += chunk.length
|
|
400
|
+
emit()
|
|
401
|
+
if (total > 0 && downloaded >= total) emit(true)
|
|
402
|
+
cb(null, chunk)
|
|
403
|
+
},
|
|
404
|
+
flush(cb) {
|
|
405
|
+
emit(true)
|
|
406
|
+
cb()
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
await pipeline(Readable.fromWeb(res.body), counter, createWriteStream(destPath))
|
|
410
|
+
} else {
|
|
411
|
+
await pipeline(res.body, createWriteStream(destPath))
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
try { rmSync(destPath, { force: true }) } catch {}
|
|
415
|
+
throw e
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Cross-OS zip extraction. Windows 10+ and macOS ship bsdtar (handles zip via
|
|
420
|
+
// libarchive); Linux ships GNU tar which does NOT understand zip, so it uses
|
|
421
|
+
// the unzip command (preinstalled on every distro we support, apt-get on
|
|
422
|
+
// Ubuntu / dnf on Fedora). Platform decision is a single switch — no fallback
|
|
423
|
+
// chain, no probing.
|
|
424
|
+
function extractZip(zipPath, destDir) {
|
|
425
|
+
// Windows: bundled tar.exe (libarchive) misreads `C:` drive letter as
|
|
426
|
+
// host:path and tries DNS resolution. Use PowerShell Expand-Archive,
|
|
427
|
+
// which is Windows-native and path-safe.
|
|
428
|
+
if (process.platform === 'win32') {
|
|
429
|
+
const ps = `Expand-Archive -LiteralPath ${JSON.stringify(zipPath)} -DestinationPath ${JSON.stringify(destDir)} -Force`
|
|
430
|
+
const r = spawnSync('powershell', ['-NoProfile', '-Command', ps], { stdio: 'pipe', windowsHide: true })
|
|
431
|
+
if (r.status !== 0) {
|
|
432
|
+
throw new Error(`[voice-runtime] zip extract failed via Expand-Archive: ${r.stderr?.toString() || r.stdout?.toString() || 'unknown'}`)
|
|
433
|
+
}
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
const onLinux = process.platform === 'linux'
|
|
437
|
+
const cmd = onLinux ? 'unzip' : 'tar'
|
|
438
|
+
const args = onLinux ? ['-q', '-o', zipPath, '-d', destDir] : ['-xf', zipPath, '-C', destDir]
|
|
439
|
+
const r = spawnSync(cmd, args, { stdio: 'pipe', windowsHide: true })
|
|
440
|
+
if (r.status !== 0) {
|
|
441
|
+
const err = r.stderr?.toString() || r.stdout?.toString() || `status=${r.status}`
|
|
442
|
+
throw new Error(`[voice-runtime] zip extract failed via ${cmd}: ${err}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function ensureWhisperRuntime(dataDir, onProgress = null) {
|
|
447
|
+
const manifest = await loadManifest(dataDir)
|
|
448
|
+
const key = platformKey()
|
|
449
|
+
const platformEntry = manifest.platforms?.[key]
|
|
450
|
+
if (!platformEntry) {
|
|
451
|
+
throw new Error(`[voice-runtime] no manifest entry for ${key} — disable voice in mixdog-config.json or add a manifest entry for this platform`)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const variants = platformEntry.variants
|
|
455
|
+
if (!Array.isArray(variants) || variants.length === 0) {
|
|
456
|
+
throw new Error(`[voice-runtime] manifest for ${key} has no variants array`)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const env = {
|
|
460
|
+
cudaMajors: detectCudaMajors(),
|
|
461
|
+
hasNvidiaDriver: hasNvidiaDriver(),
|
|
462
|
+
}
|
|
463
|
+
const variant = pickVariant(variants, env)
|
|
464
|
+
if (!variant) {
|
|
465
|
+
throw new Error(`[voice-runtime] no variant matched on ${key} (cuda=${[...env.cudaMajors].join(',') || 'none'} nvidiaDriver=${env.hasNvidiaDriver}) and no requires:null fallback in manifest`)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const ver = manifest.version
|
|
469
|
+
const rootDir = join(dataDir, 'voice-runtime')
|
|
470
|
+
const activeName = `whisper-${ver}-${variant.id}`
|
|
471
|
+
const activeDir = join(rootDir, activeName)
|
|
472
|
+
const whisperCmd = join(activeDir, variant.executable)
|
|
473
|
+
|
|
474
|
+
if (existsSync(whisperCmd)) {
|
|
475
|
+
gcStaleVersions(rootDir, activeName, 'whisper-')
|
|
476
|
+
return { whisperCmd, version: ver, variantId: variant.id }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return _withInstallLock(rootDir, 'install', async () => {
|
|
480
|
+
if (existsSync(whisperCmd)) {
|
|
481
|
+
gcStaleVersions(rootDir, activeName, 'whisper-')
|
|
482
|
+
return { whisperCmd, version: ver, variantId: variant.id }
|
|
483
|
+
}
|
|
484
|
+
gcStagingPartials(rootDir)
|
|
485
|
+
const stagingDir = join(rootDir, `staging-${process.pid}-${Date.now()}`)
|
|
486
|
+
mkdirSync(stagingDir, { recursive: true })
|
|
487
|
+
try {
|
|
488
|
+
const archivePath = join(stagingDir, `whisper.${variant.format}`)
|
|
489
|
+
process.stderr.write(`[voice-runtime] fetching whisper-${ver} variant=${variant.id} for ${key} (${(variant.size / 1024 / 1024).toFixed(0)} MB; cuda=${[...env.cudaMajors].join(',') || 'none'} nvidiaDriver=${env.hasNvidiaDriver}) ...\n`)
|
|
490
|
+
await downloadFile(variant.url, archivePath, {
|
|
491
|
+
onProgress: onProgress ? (p) => onProgress({ phase: 'runtime', ...p }) : null,
|
|
492
|
+
})
|
|
493
|
+
if (!variant.sha256) throw new Error(`[voice-runtime] manifest variant ${variant.id} for ${key} missing required sha256`)
|
|
494
|
+
await verifySha256(archivePath, variant.sha256)
|
|
495
|
+
extractZip(archivePath, stagingDir)
|
|
496
|
+
rmSync(archivePath, { force: true })
|
|
497
|
+
const stagedExec = join(stagingDir, variant.executable)
|
|
498
|
+
if (!existsSync(stagedExec)) {
|
|
499
|
+
throw new Error(`[voice-runtime] expected executable ${variant.executable} not present after extract`)
|
|
500
|
+
}
|
|
501
|
+
// Process extras (e.g. NVIDIA cublas wheel) so the bundled runtime is
|
|
502
|
+
// self-contained — user does not need a separate CUDA Toolkit install.
|
|
503
|
+
await processExtras(variant.extras, stagingDir, onProgress)
|
|
504
|
+
// Bytes on disk first; publish ready flag only after rename completes.
|
|
505
|
+
renameWithRetrySync(stagingDir, activeDir)
|
|
506
|
+
writeFileAtomicSync(join(rootDir, 'active-version'), activeName, { fsyncDir: true })
|
|
507
|
+
process.stderr.write(`[voice-runtime] whisper-${ver} variant=${variant.id} ready at ${activeDir}\n`)
|
|
508
|
+
gcStaleVersions(rootDir, activeName, 'whisper-')
|
|
509
|
+
return { whisperCmd, version: ver, variantId: variant.id }
|
|
510
|
+
} catch (err) {
|
|
511
|
+
try { rmSync(stagingDir, { recursive: true, force: true }) } catch {}
|
|
512
|
+
throw err
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Read-only resolver: returns the cached binary path when the managed runtime
|
|
518
|
+
// is fully installed, null otherwise. Used by the transcribe hot path and the
|
|
519
|
+
// /cli-check endpoint to test installation state without triggering a fetch.
|
|
520
|
+
export function resolveManagedWhisperCmd(dataDir) {
|
|
521
|
+
const activeFile = join(dataDir, 'voice-runtime', 'active-version')
|
|
522
|
+
if (!existsSync(activeFile)) return null
|
|
523
|
+
const activeName = readFileSync(activeFile, 'utf8').trim()
|
|
524
|
+
if (!activeName) return null
|
|
525
|
+
const activeDir = join(dataDir, 'voice-runtime', activeName)
|
|
526
|
+
// Consult the bundled manifest for the platform/variant executable path
|
|
527
|
+
// instead of hard-coding two layout guesses. The active-version name is
|
|
528
|
+
// `whisper-<version>-<variantId>`, so we look up the matching variant and
|
|
529
|
+
// use its declared `executable`. Falls through to the legacy guesses only
|
|
530
|
+
// when the manifest is unreadable.
|
|
531
|
+
if (existsSync(BUNDLED_MANIFEST_PATH)) {
|
|
532
|
+
try {
|
|
533
|
+
const manifest = JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
|
|
534
|
+
const key = platformKey()
|
|
535
|
+
const variants = manifest.platforms?.[key]?.variants
|
|
536
|
+
if (Array.isArray(variants)) {
|
|
537
|
+
const prefix = `whisper-${manifest.version}-`
|
|
538
|
+
const variantId = activeName.startsWith(prefix) ? activeName.slice(prefix.length) : ''
|
|
539
|
+
const variant = variants.find(v => v.id === variantId)
|
|
540
|
+
if (variant?.executable) {
|
|
541
|
+
const p = join(activeDir, variant.executable)
|
|
542
|
+
if (existsSync(p)) return p
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch {}
|
|
546
|
+
}
|
|
547
|
+
for (const c of ['Release/whisper-cli.exe', 'Release/whisper-cli']) {
|
|
548
|
+
const p = join(activeDir, c)
|
|
549
|
+
if (existsSync(p)) return p
|
|
550
|
+
}
|
|
551
|
+
return null
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Read-only resolver matching the managed model layout. The bundled manifest
|
|
555
|
+
// path is read synchronously because this runs on the per-message hot path
|
|
556
|
+
// and an async fetch would add latency to every voice transcribe.
|
|
557
|
+
export function resolveManagedWhisperModel(dataDir) {
|
|
558
|
+
if (!existsSync(BUNDLED_MANIFEST_PATH)) return null
|
|
559
|
+
const manifest = JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
|
|
560
|
+
if (!manifest.model?.filename) return null
|
|
561
|
+
const p = join(dataDir, 'voice', 'models', manifest.model.filename)
|
|
562
|
+
return existsSync(p) ? p : null
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Single managed ffmpeg binary used by transcribe (ogg→wav). Layout mirrors
|
|
566
|
+
// whisper-runtime: one binary per OS×arch fetched once, sha256-verified, atomic
|
|
567
|
+
// stage→rename, GC of stale ffmpeg-* dirs. Source binaries are gz-compressed
|
|
568
|
+
// raw executables on the eugeneware/ffmpeg-static GitHub releases — no archive
|
|
569
|
+
// extraction. The package is never bundled into the marketplace cache; the
|
|
570
|
+
// manifest only carries url + sha256 + size + executable name.
|
|
571
|
+
export async function ensureFfmpegRuntime(dataDir, onProgress = null) {
|
|
572
|
+
const manifest = await loadManifest(dataDir)
|
|
573
|
+
if (!manifest.ffmpeg) {
|
|
574
|
+
throw new Error('[voice-runtime] manifest is missing the `ffmpeg` section — cannot resolve ffmpeg runtime')
|
|
575
|
+
}
|
|
576
|
+
const key = platformKey()
|
|
577
|
+
const platformEntry = manifest.ffmpeg.platforms?.[key]
|
|
578
|
+
if (!platformEntry) {
|
|
579
|
+
throw new Error(`[voice-runtime] no ffmpeg manifest entry for ${key} — disable voice in mixdog-config.json or add a manifest entry for this platform`)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const ver = manifest.ffmpeg.version
|
|
583
|
+
const rootDir = join(dataDir, 'ffmpeg-runtime')
|
|
584
|
+
const activeName = `ffmpeg-${ver}`
|
|
585
|
+
const activeDir = join(rootDir, activeName)
|
|
586
|
+
const ffmpegPath = join(activeDir, platformEntry.executable)
|
|
587
|
+
|
|
588
|
+
if (existsSync(ffmpegPath)) {
|
|
589
|
+
gcStaleVersions(rootDir, activeName, 'ffmpeg-')
|
|
590
|
+
return { ffmpegPath, version: ver }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return _withInstallLock(rootDir, 'install', async () => {
|
|
594
|
+
if (existsSync(ffmpegPath)) {
|
|
595
|
+
gcStaleVersions(rootDir, activeName, 'ffmpeg-')
|
|
596
|
+
return { ffmpegPath, version: ver }
|
|
597
|
+
}
|
|
598
|
+
gcStagingPartials(rootDir)
|
|
599
|
+
const stagingDir = join(rootDir, `staging-${process.pid}-${Date.now()}`)
|
|
600
|
+
mkdirSync(stagingDir, { recursive: true })
|
|
601
|
+
try {
|
|
602
|
+
const archivePath = join(stagingDir, `ffmpeg.${platformEntry.format}`)
|
|
603
|
+
process.stderr.write(`[voice-runtime] fetching ffmpeg-${ver} for ${key} (${(platformEntry.size / 1024 / 1024).toFixed(0)} MB) ...\n`)
|
|
604
|
+
await downloadFile(platformEntry.url, archivePath, {
|
|
605
|
+
onProgress: onProgress ? (p) => onProgress({ phase: 'ffmpeg', ...p }) : null,
|
|
606
|
+
})
|
|
607
|
+
if (!platformEntry.sha256) throw new Error(`[voice-runtime] manifest ffmpeg entry for ${key} missing required sha256`)
|
|
608
|
+
await verifySha256(archivePath, platformEntry.sha256)
|
|
609
|
+
const stagedExec = join(stagingDir, platformEntry.executable)
|
|
610
|
+
if (platformEntry.format === 'gz') {
|
|
611
|
+
await extractGz(archivePath, stagedExec)
|
|
612
|
+
} else if (platformEntry.format === 'zip') {
|
|
613
|
+
extractZip(archivePath, stagingDir)
|
|
614
|
+
} else {
|
|
615
|
+
throw new Error(`[voice-runtime] ffmpeg manifest format must be "gz" or "zip", got "${platformEntry.format}"`)
|
|
616
|
+
}
|
|
617
|
+
rmSync(archivePath, { force: true })
|
|
618
|
+
if (!existsSync(stagedExec)) {
|
|
619
|
+
throw new Error(`[voice-runtime] expected ffmpeg executable ${platformEntry.executable} not present after extract`)
|
|
620
|
+
}
|
|
621
|
+
if (process.platform !== 'win32') {
|
|
622
|
+
chmodSync(stagedExec, 0o755)
|
|
623
|
+
}
|
|
624
|
+
// Bytes on disk first; publish ready flag only after rename completes.
|
|
625
|
+
renameWithRetrySync(stagingDir, activeDir)
|
|
626
|
+
writeFileAtomicSync(join(rootDir, 'active-version'), activeName, { fsyncDir: true })
|
|
627
|
+
process.stderr.write(`[voice-runtime] ffmpeg-${ver} ready at ${activeDir}\n`)
|
|
628
|
+
gcStaleVersions(rootDir, activeName, 'ffmpeg-')
|
|
629
|
+
return { ffmpegPath, version: ver }
|
|
630
|
+
} catch (err) {
|
|
631
|
+
try { rmSync(stagingDir, { recursive: true, force: true }) } catch {}
|
|
632
|
+
throw err
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function resolveManagedFfmpegPath(dataDir) {
|
|
638
|
+
const activeFile = join(dataDir, 'ffmpeg-runtime', 'active-version')
|
|
639
|
+
if (!existsSync(activeFile)) return null
|
|
640
|
+
const activeName = readFileSync(activeFile, 'utf8').trim()
|
|
641
|
+
if (!activeName) return null
|
|
642
|
+
const activeDir = join(dataDir, 'ffmpeg-runtime', activeName)
|
|
643
|
+
// Consult the bundled manifest for the platform's declared executable
|
|
644
|
+
// instead of hard-coding two layout guesses. Falls through to legacy
|
|
645
|
+
// guesses only when the manifest is unreadable.
|
|
646
|
+
if (existsSync(BUNDLED_MANIFEST_PATH)) {
|
|
647
|
+
try {
|
|
648
|
+
const manifest = JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
|
|
649
|
+
const key = platformKey()
|
|
650
|
+
const platformEntry = manifest.ffmpeg?.platforms?.[key]
|
|
651
|
+
if (platformEntry?.executable) {
|
|
652
|
+
const p = join(activeDir, platformEntry.executable)
|
|
653
|
+
if (existsSync(p)) return p
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
}
|
|
657
|
+
for (const c of ['ffmpeg.exe', 'ffmpeg']) {
|
|
658
|
+
const p = join(activeDir, c)
|
|
659
|
+
if (existsSync(p)) return p
|
|
660
|
+
}
|
|
661
|
+
return null
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export function resolveVoiceRuntime(dataDir) {
|
|
665
|
+
const managedWhisperCmd = resolveManagedWhisperCmd(dataDir)
|
|
666
|
+
const managedModelPath = resolveManagedWhisperModel(dataDir)
|
|
667
|
+
const managedFfmpegPath = resolveManagedFfmpegPath(dataDir)
|
|
668
|
+
const ext = process.platform === 'win32' ? '.exe' : ''
|
|
669
|
+
const managedServerCmd = managedWhisperCmd
|
|
670
|
+
? join(dirname(managedWhisperCmd), `whisper-server${ext}`)
|
|
671
|
+
: null
|
|
672
|
+
const serverCmd = managedServerCmd && existsSync(managedServerCmd) ? managedServerCmd : null
|
|
673
|
+
return {
|
|
674
|
+
kind: 'managed-whisper.cpp',
|
|
675
|
+
label: 'whisper.cpp',
|
|
676
|
+
installed: !!(managedWhisperCmd && serverCmd && managedModelPath && managedFfmpegPath),
|
|
677
|
+
binary: !!managedWhisperCmd,
|
|
678
|
+
model: !!managedModelPath,
|
|
679
|
+
ffmpeg: !!managedFfmpegPath,
|
|
680
|
+
whisperCmd: managedWhisperCmd,
|
|
681
|
+
serverCmd,
|
|
682
|
+
modelPath: managedModelPath,
|
|
683
|
+
modelName: managedModelPath ? 'ggml-large-v3-turbo.bin' : '',
|
|
684
|
+
ffmpegPath: managedFfmpegPath,
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Single managed location for the whisper model weight file. Idempotent: if
|
|
689
|
+
// the resolved file exists and matches the manifest sha256, return without
|
|
690
|
+
// re-downloading. Atomic install via stage-then-rename so a partial download
|
|
691
|
+
// on a kill/crash never leaves the model dir holding a corrupted .bin.
|
|
692
|
+
export async function ensureWhisperModel(dataDir, onProgress = null) {
|
|
693
|
+
const manifest = await loadManifest(dataDir)
|
|
694
|
+
const model = manifest.model
|
|
695
|
+
if (!model) {
|
|
696
|
+
throw new Error('[voice-runtime] manifest is missing the `model` section — cannot resolve whisper model')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const modelDir = join(dataDir, 'voice', 'models')
|
|
700
|
+
const modelPath = join(modelDir, model.filename)
|
|
701
|
+
|
|
702
|
+
if (existsSync(modelPath)) {
|
|
703
|
+
const actual = await sha256File(modelPath)
|
|
704
|
+
if (actual === model.sha256) {
|
|
705
|
+
return { modelPath, modelId: model.id, size: model.size }
|
|
706
|
+
}
|
|
707
|
+
process.stderr.write(`[voice-runtime] model ${model.filename} sha256 mismatch (expected ${model.sha256}, got ${actual}) — re-fetching\n`)
|
|
708
|
+
try { rmSync(modelPath, { force: true }) } catch {}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return _withInstallLock(modelDir, 'install', async () => {
|
|
712
|
+
if (existsSync(modelPath)) {
|
|
713
|
+
const actual = await sha256File(modelPath)
|
|
714
|
+
if (actual === model.sha256) return { modelPath, modelId: model.id, size: model.size }
|
|
715
|
+
try { rmSync(modelPath, { force: true }) } catch {}
|
|
716
|
+
}
|
|
717
|
+
gcStagingPartials(modelDir)
|
|
718
|
+
const stagingPath = join(modelDir, `.staging-${process.pid}-${Date.now()}-${model.filename}`)
|
|
719
|
+
try {
|
|
720
|
+
process.stderr.write(`[voice-runtime] fetching model ${model.id} (${(model.size / 1024 / 1024).toFixed(0)} MB) from ${model.url} ...\n`)
|
|
721
|
+
await downloadFile(model.url, stagingPath, {
|
|
722
|
+
onProgress: onProgress ? (p) => onProgress({ phase: 'model', ...p }) : null,
|
|
723
|
+
timeoutMs: 1_200_000,
|
|
724
|
+
})
|
|
725
|
+
await verifySha256(stagingPath, model.sha256)
|
|
726
|
+
renameWithRetrySync(stagingPath, modelPath)
|
|
727
|
+
process.stderr.write(`[voice-runtime] model ${model.id} ready at ${modelPath}\n`)
|
|
728
|
+
return { modelPath, modelId: model.id, size: model.size }
|
|
729
|
+
} catch (err) {
|
|
730
|
+
try { rmSync(stagingPath, { force: true }) } catch {}
|
|
731
|
+
throw err
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
}
|