@ashlr/hub 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1158 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/bin/ashlr +8 -0
- package/dist/api/core.d.ts +28 -0
- package/dist/api/core.js +42 -0
- package/dist/api/core.js.map +1 -0
- package/dist/api/index.d.ts +9 -0
- package/dist/api/index.js +9 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/plugin.d.ts +12 -0
- package/dist/api/plugin.js +13 -0
- package/dist/api/plugin.js.map +1 -0
- package/dist/api/types.d.ts +9 -0
- package/dist/api/types.js +9 -0
- package/dist/api/types.js.map +1 -0
- package/dist/cli/args.d.ts +20 -0
- package/dist/cli/args.js +23 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/ask.d.ts +22 -0
- package/dist/cli/ask.js +240 -0
- package/dist/cli/ask.js.map +1 -0
- package/dist/cli/audit.d.ts +26 -0
- package/dist/cli/audit.js +199 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/backlog.d.ts +26 -0
- package/dist/cli/backlog.js +322 -0
- package/dist/cli/backlog.js.map +1 -0
- package/dist/cli/completions.d.ts +18 -0
- package/dist/cli/completions.js +156 -0
- package/dist/cli/completions.js.map +1 -0
- package/dist/cli/daemon.d.ts +31 -0
- package/dist/cli/daemon.js +334 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/demo-sandbox.d.ts +78 -0
- package/dist/cli/demo-sandbox.js +227 -0
- package/dist/cli/demo-sandbox.js.map +1 -0
- package/dist/cli/demo.d.ts +76 -0
- package/dist/cli/demo.js +285 -0
- package/dist/cli/demo.js.map +1 -0
- package/dist/cli/digest.d.ts +46 -0
- package/dist/cli/digest.js +352 -0
- package/dist/cli/digest.js.map +1 -0
- package/dist/cli/doctor-init.d.ts +28 -0
- package/dist/cli/doctor-init.js +335 -0
- package/dist/cli/doctor-init.js.map +1 -0
- package/dist/cli/genome.d.ts +62 -0
- package/dist/cli/genome.js +1061 -0
- package/dist/cli/genome.js.map +1 -0
- package/dist/cli/gh.d.ts +27 -0
- package/dist/cli/gh.js +322 -0
- package/dist/cli/gh.js.map +1 -0
- package/dist/cli/goals.d.ts +55 -0
- package/dist/cli/goals.js +667 -0
- package/dist/cli/goals.js.map +1 -0
- package/dist/cli/health.d.ts +56 -0
- package/dist/cli/health.js +407 -0
- package/dist/cli/health.js.map +1 -0
- package/dist/cli/help.d.ts +51 -0
- package/dist/cli/help.js +436 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/inbox.d.ts +28 -0
- package/dist/cli/inbox.js +589 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.js +1208 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/knowledge.d.ts +33 -0
- package/dist/cli/knowledge.js +575 -0
- package/dist/cli/knowledge.js.map +1 -0
- package/dist/cli/mcp.d.ts +26 -0
- package/dist/cli/mcp.js +487 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/models.d.ts +26 -0
- package/dist/cli/models.js +435 -0
- package/dist/cli/models.js.map +1 -0
- package/dist/cli/new.d.ts +32 -0
- package/dist/cli/new.js +565 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/notify.d.ts +20 -0
- package/dist/cli/notify.js +188 -0
- package/dist/cli/notify.js.map +1 -0
- package/dist/cli/onboard.d.ts +85 -0
- package/dist/cli/onboard.js +305 -0
- package/dist/cli/onboard.js.map +1 -0
- package/dist/cli/open.d.ts +38 -0
- package/dist/cli/open.js +86 -0
- package/dist/cli/open.js.map +1 -0
- package/dist/cli/orient.d.ts +14 -0
- package/dist/cli/orient.js +110 -0
- package/dist/cli/orient.js.map +1 -0
- package/dist/cli/picker.d.ts +21 -0
- package/dist/cli/picker.js +177 -0
- package/dist/cli/picker.js.map +1 -0
- package/dist/cli/plugins.d.ts +21 -0
- package/dist/cli/plugins.js +394 -0
- package/dist/cli/plugins.js.map +1 -0
- package/dist/cli/preflight.d.ts +29 -0
- package/dist/cli/preflight.js +131 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/pulse.d.ts +26 -0
- package/dist/cli/pulse.js +482 -0
- package/dist/cli/pulse.js.map +1 -0
- package/dist/cli/reflect.d.ts +42 -0
- package/dist/cli/reflect.js +381 -0
- package/dist/cli/reflect.js.map +1 -0
- package/dist/cli/run.d.ts +29 -0
- package/dist/cli/run.js +687 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/sandbox.d.ts +20 -0
- package/dist/cli/sandbox.js +345 -0
- package/dist/cli/sandbox.js.map +1 -0
- package/dist/cli/seams.d.ts +41 -0
- package/dist/cli/seams.js +167 -0
- package/dist/cli/seams.js.map +1 -0
- package/dist/cli/serve.d.ts +25 -0
- package/dist/cli/serve.js +249 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/ship.d.ts +33 -0
- package/dist/cli/ship.js +433 -0
- package/dist/cli/ship.js.map +1 -0
- package/dist/cli/spec.d.ts +19 -0
- package/dist/cli/spec.js +478 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/cli/swarm.d.ts +42 -0
- package/dist/cli/swarm.js +1365 -0
- package/dist/cli/swarm.js.map +1 -0
- package/dist/cli/telemetry.d.ts +26 -0
- package/dist/cli/telemetry.js +364 -0
- package/dist/cli/telemetry.js.map +1 -0
- package/dist/cli/tui.d.ts +17 -0
- package/dist/cli/tui.js +52 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/cli/ui.d.ts +53 -0
- package/dist/cli/ui.js +66 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli/update.d.ts +38 -0
- package/dist/cli/update.js +682 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/vercel.d.ts +20 -0
- package/dist/cli/vercel.js +193 -0
- package/dist/cli/vercel.js.map +1 -0
- package/dist/cli/verify-safety.d.ts +96 -0
- package/dist/cli/verify-safety.js +509 -0
- package/dist/cli/verify-safety.js.map +1 -0
- package/dist/cli/wire.d.ts +22 -0
- package/dist/cli/wire.js +205 -0
- package/dist/cli/wire.js.map +1 -0
- package/dist/core/classify.d.ts +51 -0
- package/dist/core/classify.js +450 -0
- package/dist/core/classify.js.map +1 -0
- package/dist/core/config.d.ts +63 -0
- package/dist/core/config.js +474 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/daemon/loop.d.ts +74 -0
- package/dist/core/daemon/loop.js +618 -0
- package/dist/core/daemon/loop.js.map +1 -0
- package/dist/core/daemon/state.d.ts +66 -0
- package/dist/core/daemon/state.js +197 -0
- package/dist/core/daemon/state.js.map +1 -0
- package/dist/core/dashboard.d.ts +40 -0
- package/dist/core/dashboard.js +463 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/digest/build.d.ts +51 -0
- package/dist/core/digest/build.js +230 -0
- package/dist/core/digest/build.js.map +1 -0
- package/dist/core/digest/deliver.d.ts +47 -0
- package/dist/core/digest/deliver.js +230 -0
- package/dist/core/digest/deliver.js.map +1 -0
- package/dist/core/digest/store.d.ts +57 -0
- package/dist/core/digest/store.js +223 -0
- package/dist/core/digest/store.js.map +1 -0
- package/dist/core/doctor-fix.d.ts +21 -0
- package/dist/core/doctor-fix.js +440 -0
- package/dist/core/doctor-fix.js.map +1 -0
- package/dist/core/doctor.d.ts +18 -0
- package/dist/core/doctor.js +806 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/env-bridge.d.ts +64 -0
- package/dist/core/env-bridge.js +103 -0
- package/dist/core/env-bridge.js.map +1 -0
- package/dist/core/genome/capture.d.ts +49 -0
- package/dist/core/genome/capture.js +352 -0
- package/dist/core/genome/capture.js.map +1 -0
- package/dist/core/genome/consolidate.d.ts +38 -0
- package/dist/core/genome/consolidate.js +426 -0
- package/dist/core/genome/consolidate.js.map +1 -0
- package/dist/core/genome/export.d.ts +29 -0
- package/dist/core/genome/export.js +102 -0
- package/dist/core/genome/export.js.map +1 -0
- package/dist/core/genome/playbook.d.ts +33 -0
- package/dist/core/genome/playbook.js +320 -0
- package/dist/core/genome/playbook.js.map +1 -0
- package/dist/core/genome/recall.d.ts +45 -0
- package/dist/core/genome/recall.js +293 -0
- package/dist/core/genome/recall.js.map +1 -0
- package/dist/core/genome/store.d.ts +62 -0
- package/dist/core/genome/store.js +711 -0
- package/dist/core/genome/store.js.map +1 -0
- package/dist/core/git.d.ts +34 -0
- package/dist/core/git.js +115 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/goals/advance.d.ts +82 -0
- package/dist/core/goals/advance.js +263 -0
- package/dist/core/goals/advance.js.map +1 -0
- package/dist/core/goals/planner.d.ts +58 -0
- package/dist/core/goals/planner.js +233 -0
- package/dist/core/goals/planner.js.map +1 -0
- package/dist/core/goals/store.d.ts +128 -0
- package/dist/core/goals/store.js +442 -0
- package/dist/core/goals/store.js.map +1 -0
- package/dist/core/inbox/apply.d.ts +36 -0
- package/dist/core/inbox/apply.js +407 -0
- package/dist/core/inbox/apply.js.map +1 -0
- package/dist/core/inbox/notify-proposal.d.ts +13 -0
- package/dist/core/inbox/notify-proposal.js +21 -0
- package/dist/core/inbox/notify-proposal.js.map +1 -0
- package/dist/core/inbox/store.d.ts +67 -0
- package/dist/core/inbox/store.js +258 -0
- package/dist/core/inbox/store.js.map +1 -0
- package/dist/core/index-engine.d.ts +62 -0
- package/dist/core/index-engine.js +486 -0
- package/dist/core/index-engine.js.map +1 -0
- package/dist/core/integrations/desktop-notify.d.ts +18 -0
- package/dist/core/integrations/desktop-notify.js +42 -0
- package/dist/core/integrations/desktop-notify.js.map +1 -0
- package/dist/core/integrations/editors.d.ts +50 -0
- package/dist/core/integrations/editors.js +216 -0
- package/dist/core/integrations/editors.js.map +1 -0
- package/dist/core/integrations/github.d.ts +66 -0
- package/dist/core/integrations/github.js +332 -0
- package/dist/core/integrations/github.js.map +1 -0
- package/dist/core/integrations/identity.d.ts +29 -0
- package/dist/core/integrations/identity.js +359 -0
- package/dist/core/integrations/identity.js.map +1 -0
- package/dist/core/integrations/notify.d.ts +23 -0
- package/dist/core/integrations/notify.js +96 -0
- package/dist/core/integrations/notify.js.map +1 -0
- package/dist/core/integrations/vercel.d.ts +37 -0
- package/dist/core/integrations/vercel.js +192 -0
- package/dist/core/integrations/vercel.js.map +1 -0
- package/dist/core/knowledge/ask.d.ts +34 -0
- package/dist/core/knowledge/ask.js +325 -0
- package/dist/core/knowledge/ask.js.map +1 -0
- package/dist/core/knowledge/graph.d.ts +51 -0
- package/dist/core/knowledge/graph.js +564 -0
- package/dist/core/knowledge/graph.js.map +1 -0
- package/dist/core/knowledge/index.d.ts +74 -0
- package/dist/core/knowledge/index.js +586 -0
- package/dist/core/knowledge/index.js.map +1 -0
- package/dist/core/learn/playbooks.d.ts +84 -0
- package/dist/core/learn/playbooks.js +241 -0
- package/dist/core/learn/playbooks.js.map +1 -0
- package/dist/core/learn/reflect.d.ts +87 -0
- package/dist/core/learn/reflect.js +435 -0
- package/dist/core/learn/reflect.js.map +1 -0
- package/dist/core/learn/store.d.ts +51 -0
- package/dist/core/learn/store.js +165 -0
- package/dist/core/learn/store.js.map +1 -0
- package/dist/core/learn/tuning.d.ts +48 -0
- package/dist/core/learn/tuning.js +201 -0
- package/dist/core/learn/tuning.js.map +1 -0
- package/dist/core/lifecycle/scaffold.d.ts +43 -0
- package/dist/core/lifecycle/scaffold.js +260 -0
- package/dist/core/lifecycle/scaffold.js.map +1 -0
- package/dist/core/lifecycle/ship.d.ts +48 -0
- package/dist/core/lifecycle/ship.js +513 -0
- package/dist/core/lifecycle/ship.js.map +1 -0
- package/dist/core/lifecycle/templates.d.ts +20 -0
- package/dist/core/lifecycle/templates.js +605 -0
- package/dist/core/lifecycle/templates.js.map +1 -0
- package/dist/core/mcp-gateway.d.ts +59 -0
- package/dist/core/mcp-gateway.js +385 -0
- package/dist/core/mcp-gateway.js.map +1 -0
- package/dist/core/mcp-native.d.ts +53 -0
- package/dist/core/mcp-native.js +507 -0
- package/dist/core/mcp-native.js.map +1 -0
- package/dist/core/mcp-registry.d.ts +36 -0
- package/dist/core/mcp-registry.js +180 -0
- package/dist/core/mcp-registry.js.map +1 -0
- package/dist/core/observability/budget-alert.d.ts +18 -0
- package/dist/core/observability/budget-alert.js +90 -0
- package/dist/core/observability/budget-alert.js.map +1 -0
- package/dist/core/observability/estimate.d.ts +27 -0
- package/dist/core/observability/estimate.js +188 -0
- package/dist/core/observability/estimate.js.map +1 -0
- package/dist/core/observability/forecast.d.ts +19 -0
- package/dist/core/observability/forecast.js +101 -0
- package/dist/core/observability/forecast.js.map +1 -0
- package/dist/core/observability/governance.d.ts +28 -0
- package/dist/core/observability/governance.js +101 -0
- package/dist/core/observability/governance.js.map +1 -0
- package/dist/core/observability/otlp.d.ts +88 -0
- package/dist/core/observability/otlp.js +217 -0
- package/dist/core/observability/otlp.js.map +1 -0
- package/dist/core/observability/rollup.d.ts +35 -0
- package/dist/core/observability/rollup.js +311 -0
- package/dist/core/observability/rollup.js.map +1 -0
- package/dist/core/observability/telemetry-sink.d.ts +63 -0
- package/dist/core/observability/telemetry-sink.js +350 -0
- package/dist/core/observability/telemetry-sink.js.map +1 -0
- package/dist/core/observability/usage-source.d.ts +60 -0
- package/dist/core/observability/usage-source.js +347 -0
- package/dist/core/observability/usage-source.js.map +1 -0
- package/dist/core/onboard.d.ts +27 -0
- package/dist/core/onboard.js +288 -0
- package/dist/core/onboard.js.map +1 -0
- package/dist/core/orient.d.ts +23 -0
- package/dist/core/orient.js +135 -0
- package/dist/core/orient.js.map +1 -0
- package/dist/core/phantom.d.ts +23 -0
- package/dist/core/phantom.js +279 -0
- package/dist/core/phantom.js.map +1 -0
- package/dist/core/plugins/host-api.d.ts +29 -0
- package/dist/core/plugins/host-api.js +113 -0
- package/dist/core/plugins/host-api.js.map +1 -0
- package/dist/core/plugins/integrity.d.ts +36 -0
- package/dist/core/plugins/integrity.js +73 -0
- package/dist/core/plugins/integrity.js.map +1 -0
- package/dist/core/plugins/manifest.d.ts +31 -0
- package/dist/core/plugins/manifest.js +316 -0
- package/dist/core/plugins/manifest.js.map +1 -0
- package/dist/core/plugins/registry.d.ts +87 -0
- package/dist/core/plugins/registry.js +415 -0
- package/dist/core/plugins/registry.js.map +1 -0
- package/dist/core/plugins/types.d.ts +182 -0
- package/dist/core/plugins/types.js +40 -0
- package/dist/core/plugins/types.js.map +1 -0
- package/dist/core/plugins/wrappers.d.ts +40 -0
- package/dist/core/plugins/wrappers.js +229 -0
- package/dist/core/plugins/wrappers.js.map +1 -0
- package/dist/core/portfolio/backlog.d.ts +40 -0
- package/dist/core/portfolio/backlog.js +177 -0
- package/dist/core/portfolio/backlog.js.map +1 -0
- package/dist/core/portfolio/scanners.d.ts +21 -0
- package/dist/core/portfolio/scanners.js +600 -0
- package/dist/core/portfolio/scanners.js.map +1 -0
- package/dist/core/providers.d.ts +34 -0
- package/dist/core/providers.js +250 -0
- package/dist/core/providers.js.map +1 -0
- package/dist/core/quality/conventions.d.ts +35 -0
- package/dist/core/quality/conventions.js +267 -0
- package/dist/core/quality/conventions.js.map +1 -0
- package/dist/core/quality/fixes.d.ts +56 -0
- package/dist/core/quality/fixes.js +209 -0
- package/dist/core/quality/fixes.js.map +1 -0
- package/dist/core/quality/health.d.ts +69 -0
- package/dist/core/quality/health.js +350 -0
- package/dist/core/quality/health.js.map +1 -0
- package/dist/core/quality/store.d.ts +56 -0
- package/dist/core/quality/store.js +195 -0
- package/dist/core/quality/store.js.map +1 -0
- package/dist/core/readiness.d.ts +112 -0
- package/dist/core/readiness.js +431 -0
- package/dist/core/readiness.js.map +1 -0
- package/dist/core/run/agent-loop.d.ts +40 -0
- package/dist/core/run/agent-loop.js +291 -0
- package/dist/core/run/agent-loop.js.map +1 -0
- package/dist/core/run/budget.d.ts +47 -0
- package/dist/core/run/budget.js +113 -0
- package/dist/core/run/budget.js.map +1 -0
- package/dist/core/run/engines.d.ts +75 -0
- package/dist/core/run/engines.js +199 -0
- package/dist/core/run/engines.js.map +1 -0
- package/dist/core/run/model-manager.d.ts +64 -0
- package/dist/core/run/model-manager.js +339 -0
- package/dist/core/run/model-manager.js.map +1 -0
- package/dist/core/run/orchestrator.d.ts +100 -0
- package/dist/core/run/orchestrator.js +1515 -0
- package/dist/core/run/orchestrator.js.map +1 -0
- package/dist/core/run/provider-client.d.ts +46 -0
- package/dist/core/run/provider-client.js +796 -0
- package/dist/core/run/provider-client.js.map +1 -0
- package/dist/core/run/retry.d.ts +19 -0
- package/dist/core/run/retry.js +68 -0
- package/dist/core/run/retry.js.map +1 -0
- package/dist/core/run/router.d.ts +50 -0
- package/dist/core/run/router.js +257 -0
- package/dist/core/run/router.js.map +1 -0
- package/dist/core/run/self-heal.d.ts +52 -0
- package/dist/core/run/self-heal.js +181 -0
- package/dist/core/run/self-heal.js.map +1 -0
- package/dist/core/run/streaming.d.ts +31 -0
- package/dist/core/run/streaming.js +122 -0
- package/dist/core/run/streaming.js.map +1 -0
- package/dist/core/run/verify.d.ts +30 -0
- package/dist/core/run/verify.js +204 -0
- package/dist/core/run/verify.js.map +1 -0
- package/dist/core/sandbox/audit.d.ts +31 -0
- package/dist/core/sandbox/audit.js +169 -0
- package/dist/core/sandbox/audit.js.map +1 -0
- package/dist/core/sandbox/policy.d.ts +61 -0
- package/dist/core/sandbox/policy.js +211 -0
- package/dist/core/sandbox/policy.js.map +1 -0
- package/dist/core/sandbox/worktree.d.ts +175 -0
- package/dist/core/sandbox/worktree.js +673 -0
- package/dist/core/sandbox/worktree.js.map +1 -0
- package/dist/core/seams/backlog.d.ts +47 -0
- package/dist/core/seams/backlog.js +47 -0
- package/dist/core/seams/backlog.js.map +1 -0
- package/dist/core/seams/daemon-coordinator.d.ts +72 -0
- package/dist/core/seams/daemon-coordinator.js +76 -0
- package/dist/core/seams/daemon-coordinator.js.map +1 -0
- package/dist/core/seams/genome.d.ts +45 -0
- package/dist/core/seams/genome.js +53 -0
- package/dist/core/seams/genome.js.map +1 -0
- package/dist/core/seams/identity.d.ts +40 -0
- package/dist/core/seams/identity.js +44 -0
- package/dist/core/seams/identity.js.map +1 -0
- package/dist/core/seams/inbox.d.ts +60 -0
- package/dist/core/seams/inbox.js +66 -0
- package/dist/core/seams/inbox.js.map +1 -0
- package/dist/core/seams/index.d.ts +20 -0
- package/dist/core/seams/index.js +21 -0
- package/dist/core/seams/index.js.map +1 -0
- package/dist/core/seams/portfolio.d.ts +50 -0
- package/dist/core/seams/portfolio.js +61 -0
- package/dist/core/seams/portfolio.js.map +1 -0
- package/dist/core/seams/registry.d.ts +42 -0
- package/dist/core/seams/registry.js +128 -0
- package/dist/core/seams/registry.js.map +1 -0
- package/dist/core/seams/run-swarm.d.ts +66 -0
- package/dist/core/seams/run-swarm.js +74 -0
- package/dist/core/seams/run-swarm.js.map +1 -0
- package/dist/core/seams/types.d.ts +123 -0
- package/dist/core/seams/types.js +35 -0
- package/dist/core/seams/types.js.map +1 -0
- package/dist/core/spec/spec-store.d.ts +62 -0
- package/dist/core/spec/spec-store.js +359 -0
- package/dist/core/spec/spec-store.js.map +1 -0
- package/dist/core/swarm/gate.d.ts +45 -0
- package/dist/core/swarm/gate.js +114 -0
- package/dist/core/swarm/gate.js.map +1 -0
- package/dist/core/swarm/planner.d.ts +32 -0
- package/dist/core/swarm/planner.js +293 -0
- package/dist/core/swarm/planner.js.map +1 -0
- package/dist/core/swarm/rollback.d.ts +56 -0
- package/dist/core/swarm/rollback.js +266 -0
- package/dist/core/swarm/rollback.js.map +1 -0
- package/dist/core/swarm/runner.d.ts +62 -0
- package/dist/core/swarm/runner.js +1263 -0
- package/dist/core/swarm/runner.js.map +1 -0
- package/dist/core/swarm/sign.d.ts +71 -0
- package/dist/core/swarm/sign.js +362 -0
- package/dist/core/swarm/sign.js.map +1 -0
- package/dist/core/swarm/store.d.ts +52 -0
- package/dist/core/swarm/store.js +195 -0
- package/dist/core/swarm/store.js.map +1 -0
- package/dist/core/tidy.d.ts +32 -0
- package/dist/core/tidy.js +354 -0
- package/dist/core/tidy.js.map +1 -0
- package/dist/core/tools-registry.d.ts +16 -0
- package/dist/core/tools-registry.js +308 -0
- package/dist/core/tools-registry.js.map +1 -0
- package/dist/core/types.d.ts +2545 -0
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/web/api.d.ts +53 -0
- package/dist/core/web/api.js +698 -0
- package/dist/core/web/api.js.map +1 -0
- package/dist/core/web/public/app.js +1906 -0
- package/dist/core/web/public/index.html +721 -0
- package/dist/core/web/public/styles.css +2007 -0
- package/dist/core/web/server.d.ts +18 -0
- package/dist/core/web/server.js +122 -0
- package/dist/core/web/server.js.map +1 -0
- package/dist/core/web/static.d.ts +18 -0
- package/dist/core/web/static.js +122 -0
- package/dist/core/web/static.js.map +1 -0
- package/dist/tui/app.d.ts +33 -0
- package/dist/tui/app.js +350 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/render.d.ts +20 -0
- package/dist/tui/render.js +558 -0
- package/dist/tui/render.js.map +1 -0
- package/package.json +80 -0
- package/schema/config.schema.json +223 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-client.ts — thin chat client over the ACTIVE local provider.
|
|
3
|
+
*
|
|
4
|
+
* LOCAL-FIRST: refuses cloud providers unless --allow-cloud is passed (and the
|
|
5
|
+
* relevant API key is present). Supports Ollama (/api/chat native) and LM
|
|
6
|
+
* Studio (/v1/chat/completions OpenAI shape). Detects tool-call capability and
|
|
7
|
+
* degrades to plain chat when unsupported.
|
|
8
|
+
*/
|
|
9
|
+
import { getProviderRegistry } from '../providers.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Known cloud provider identifiers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const CLOUD_PROVIDERS = new Set([
|
|
14
|
+
'anthropic',
|
|
15
|
+
'openai',
|
|
16
|
+
'gemini',
|
|
17
|
+
'cohere',
|
|
18
|
+
'groq',
|
|
19
|
+
'mistral',
|
|
20
|
+
'azure',
|
|
21
|
+
]);
|
|
22
|
+
const CLOUD_PROVIDER_ENV = {
|
|
23
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
24
|
+
openai: 'OPENAI_API_KEY',
|
|
25
|
+
gemini: 'GEMINI_API_KEY',
|
|
26
|
+
cohere: 'COHERE_API_KEY',
|
|
27
|
+
groq: 'GROQ_API_KEY',
|
|
28
|
+
mistral: 'MISTRAL_API_KEY',
|
|
29
|
+
azure: 'AZURE_OPENAI_API_KEY',
|
|
30
|
+
};
|
|
31
|
+
function isCloudProvider(id) {
|
|
32
|
+
return CLOUD_PROVIDERS.has(id.toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Token estimation
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* Approximate token count for a string (~4 chars/token heuristic).
|
|
39
|
+
*/
|
|
40
|
+
export function estimateTokens(text) {
|
|
41
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Models that reliably support tool calls via Ollama
|
|
45
|
+
// (additive heuristic — safe to extend; false negatives just degrade to chat)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
const TOOL_CAPABLE_MODEL_PATTERNS = [
|
|
48
|
+
/llama3/i,
|
|
49
|
+
/llama-3/i,
|
|
50
|
+
/mistral/i,
|
|
51
|
+
/mixtral/i,
|
|
52
|
+
/qwen/i,
|
|
53
|
+
/deepseek/i,
|
|
54
|
+
/command-r/i,
|
|
55
|
+
/hermes/i,
|
|
56
|
+
/functionary/i,
|
|
57
|
+
/nexusraven/i,
|
|
58
|
+
/tool/i,
|
|
59
|
+
];
|
|
60
|
+
function modelSupportsTools(modelName) {
|
|
61
|
+
return TOOL_CAPABLE_MODEL_PATTERNS.some((pat) => pat.test(modelName));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Pick the best model from a list of available model names.
|
|
65
|
+
* Prefers a coder/capable model, falls back to first available.
|
|
66
|
+
*/
|
|
67
|
+
function pickModel(models, explicit) {
|
|
68
|
+
if (models.length === 0)
|
|
69
|
+
return explicit?.trim() || 'default';
|
|
70
|
+
// 1. Explicit override. An EXPLICIT argument (passed in by a provider-aware
|
|
71
|
+
// caller such as the M15 router) takes precedence over the ASHLR_MODEL env
|
|
72
|
+
// var; the env var remains supported for the --model flag / global override.
|
|
73
|
+
// Passing the model explicitly avoids mutating shared process.env under
|
|
74
|
+
// concurrent tasks (no env race). Exact match wins, else first substring
|
|
75
|
+
// match, else trust the caller's string verbatim.
|
|
76
|
+
const override = (explicit?.trim() || process.env.ASHLR_MODEL?.trim()) || undefined;
|
|
77
|
+
if (override) {
|
|
78
|
+
return (models.find((m) => m === override) ??
|
|
79
|
+
models.find((m) => m.toLowerCase().includes(override.toLowerCase())) ??
|
|
80
|
+
override);
|
|
81
|
+
}
|
|
82
|
+
// 2. Local-first & cost-efficient default: prefer the SMALLEST/fastest chat model so runs start
|
|
83
|
+
// instantly instead of cold-loading a 70B (which times out). Skip embedding-only models;
|
|
84
|
+
// bias slightly toward coder models among equal sizes.
|
|
85
|
+
const isEmbed = (m) => /embed|bge|e5|nomic/i.test(m);
|
|
86
|
+
const pool = models.filter((m) => !isEmbed(m));
|
|
87
|
+
const ranked = (pool.length ? pool : models).slice();
|
|
88
|
+
const sizeOf = (m) => {
|
|
89
|
+
const b = m.match(/(\d+(?:\.\d+)?)\s*b\b/i); // "3b" / "7b" / "72b"
|
|
90
|
+
if (b)
|
|
91
|
+
return parseFloat(b[1]);
|
|
92
|
+
if (/mini|small|tiny|nano|phi/i.test(m))
|
|
93
|
+
return 3; // unlabeled small models
|
|
94
|
+
return 999; // unknown size -> deprioritize
|
|
95
|
+
};
|
|
96
|
+
const score = (m) => sizeOf(m) - (/coder|code/i.test(m) ? 0.5 : 0);
|
|
97
|
+
ranked.sort((a, b) => score(a) - score(b));
|
|
98
|
+
return ranked[0];
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Fetch with timeout
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
const FETCH_TIMEOUT_MS = 30_000; // 30s for chat completions
|
|
104
|
+
async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
107
|
+
try {
|
|
108
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Open a streaming connection whose AbortController stays alive AFTER the
|
|
116
|
+
* response headers arrive, so the caller can keep it as an idle-watchdog
|
|
117
|
+
* over the body read loop (fetchWithTimeout clears its timer the instant
|
|
118
|
+
* headers resolve, leaving the body read unbounded — see the streaming loops).
|
|
119
|
+
*
|
|
120
|
+
* The connect timer bounds time-to-headers; the returned controller lets the
|
|
121
|
+
* read loop abort a stalled/trickling body. Caller MUST clear the connect timer
|
|
122
|
+
* (clearConnectTimer) once headers are in, then arm its own idle watchdog.
|
|
123
|
+
*/
|
|
124
|
+
async function fetchStream(url, init, connectTimeoutMs) {
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
127
|
+
let cleared = false;
|
|
128
|
+
const clearConnectTimer = () => {
|
|
129
|
+
if (!cleared) {
|
|
130
|
+
cleared = true;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
136
|
+
return { response, controller, clearConnectTimer };
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
clearConnectTimer();
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Read one chunk from a stream reader under a per-read idle deadline.
|
|
145
|
+
* If no bytes arrive within idleMs, aborts the underlying request (so the
|
|
146
|
+
* read rejects) and surfaces a timeout error. This bounds the ONLY otherwise
|
|
147
|
+
* unbounded wait in M11: a provider that returns 200 headers then stalls
|
|
148
|
+
* mid-stream (or never sends done). On timeout the caller's catch falls back
|
|
149
|
+
* to non-streaming chat() (itself FETCH_TIMEOUT-bounded).
|
|
150
|
+
*/
|
|
151
|
+
async function readWithIdleTimeout(reader, controller, idleMs) {
|
|
152
|
+
let timer;
|
|
153
|
+
const idle = new Promise((_, reject) => {
|
|
154
|
+
timer = setTimeout(() => {
|
|
155
|
+
controller.abort();
|
|
156
|
+
reject(new Error(`stream idle timeout after ${idleMs}ms`));
|
|
157
|
+
}, idleMs);
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
return await Promise.race([reader.read(), idle]);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (timer)
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Ollama chat client
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Parse tool calls from an Ollama response message.
|
|
172
|
+
* Ollama returns tool_calls as an array on the message object.
|
|
173
|
+
*/
|
|
174
|
+
function parseOllamaToolCalls(message) {
|
|
175
|
+
const rawCalls = message['tool_calls'];
|
|
176
|
+
if (!Array.isArray(rawCalls) || rawCalls.length === 0)
|
|
177
|
+
return undefined;
|
|
178
|
+
const parsed = [];
|
|
179
|
+
for (const tc of rawCalls) {
|
|
180
|
+
if (typeof tc !== 'object' || tc === null)
|
|
181
|
+
continue;
|
|
182
|
+
const call = tc;
|
|
183
|
+
const fn = call['function'];
|
|
184
|
+
if (!fn)
|
|
185
|
+
continue;
|
|
186
|
+
const name = typeof fn['name'] === 'string' ? fn['name'] : 'unknown';
|
|
187
|
+
const args = fn['arguments'] ?? {};
|
|
188
|
+
// Generate a stable id from index (Ollama doesn't assign ids)
|
|
189
|
+
const id = `call_${parsed.length}`;
|
|
190
|
+
parsed.push({ id, name, arguments: args });
|
|
191
|
+
}
|
|
192
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Build Ollama messages array from ChatMessage[].
|
|
196
|
+
* Reused by both chat() and chatStream().
|
|
197
|
+
*/
|
|
198
|
+
function toOllamaMessages(messages) {
|
|
199
|
+
return messages.map((m) => {
|
|
200
|
+
if (m.role === 'tool') {
|
|
201
|
+
return {
|
|
202
|
+
role: 'tool',
|
|
203
|
+
content: m.content,
|
|
204
|
+
tool_call_id: m.toolCallId,
|
|
205
|
+
name: m.name,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { role: m.role, content: m.content };
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Streaming timeout is longer — the first token may take a few seconds.
|
|
212
|
+
const STREAM_TIMEOUT_MS = 60_000; // 60s for streaming requests
|
|
213
|
+
function buildOllamaClient(baseUrl, model, supportsTools) {
|
|
214
|
+
const chatUrl = baseUrl.replace(/\/+$/, '') + '/api/chat';
|
|
215
|
+
return {
|
|
216
|
+
id: 'ollama',
|
|
217
|
+
supportsTools,
|
|
218
|
+
async chat(messages, tools) {
|
|
219
|
+
// Map ChatMessage roles to Ollama format
|
|
220
|
+
// Ollama uses role: 'system'|'user'|'assistant'|'tool'
|
|
221
|
+
const ollamaMessages = toOllamaMessages(messages);
|
|
222
|
+
const body = {
|
|
223
|
+
model,
|
|
224
|
+
messages: ollamaMessages,
|
|
225
|
+
stream: false,
|
|
226
|
+
};
|
|
227
|
+
// Only send tools if supported and tools are provided
|
|
228
|
+
if (supportsTools && tools && tools.length > 0) {
|
|
229
|
+
body['tools'] = tools;
|
|
230
|
+
}
|
|
231
|
+
let response;
|
|
232
|
+
try {
|
|
233
|
+
response = await fetchWithTimeout(chatUrl, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify(body),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
241
|
+
throw new Error(`Ollama fetch failed: ${msg}`);
|
|
242
|
+
}
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
const errText = await response.text().catch(() => '');
|
|
245
|
+
throw new Error(`Ollama HTTP ${response.status}: ${errText}`);
|
|
246
|
+
}
|
|
247
|
+
let data;
|
|
248
|
+
try {
|
|
249
|
+
data = (await response.json());
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
throw new Error('Ollama returned non-JSON response');
|
|
253
|
+
}
|
|
254
|
+
const d = data;
|
|
255
|
+
const message = (d['message'] ?? {});
|
|
256
|
+
const content = typeof message['content'] === 'string' ? message['content'] : '';
|
|
257
|
+
// Parse usage from response
|
|
258
|
+
const promptEval = typeof d['prompt_eval_count'] === 'number' ? d['prompt_eval_count'] : 0;
|
|
259
|
+
const evalCount = typeof d['eval_count'] === 'number' ? d['eval_count'] : 0;
|
|
260
|
+
const tokensIn = promptEval > 0
|
|
261
|
+
? promptEval
|
|
262
|
+
: estimateTokens(messages.map((m) => m.content).join(' '));
|
|
263
|
+
const tokensOut = evalCount > 0 ? evalCount : estimateTokens(content);
|
|
264
|
+
const toolCalls = parseOllamaToolCalls(message);
|
|
265
|
+
return {
|
|
266
|
+
content,
|
|
267
|
+
toolCalls,
|
|
268
|
+
usage: { tokensIn, tokensOut },
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
async chatStream(messages, tools, onDelta) {
|
|
272
|
+
// Attempt streaming; fall back to chat() on any error.
|
|
273
|
+
try {
|
|
274
|
+
const ollamaMessages = toOllamaMessages(messages);
|
|
275
|
+
const body = {
|
|
276
|
+
model,
|
|
277
|
+
messages: ollamaMessages,
|
|
278
|
+
stream: true,
|
|
279
|
+
};
|
|
280
|
+
if (supportsTools && tools && tools.length > 0) {
|
|
281
|
+
body['tools'] = tools;
|
|
282
|
+
}
|
|
283
|
+
let response;
|
|
284
|
+
let streamController;
|
|
285
|
+
try {
|
|
286
|
+
const opened = await fetchStream(chatUrl, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: { 'Content-Type': 'application/json' },
|
|
289
|
+
body: JSON.stringify(body),
|
|
290
|
+
}, STREAM_TIMEOUT_MS);
|
|
291
|
+
response = opened.response;
|
|
292
|
+
streamController = opened.controller;
|
|
293
|
+
// Headers are in; stop the connect timer. The read loop below arms its
|
|
294
|
+
// own per-read idle watchdog via streamController so a stalled body
|
|
295
|
+
// can't hang indefinitely.
|
|
296
|
+
opened.clearConnectTimer();
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
300
|
+
throw new Error(`Ollama stream fetch failed: ${msg}`);
|
|
301
|
+
}
|
|
302
|
+
if (!response.ok) {
|
|
303
|
+
const errText = await response.text().catch(() => '');
|
|
304
|
+
throw new Error(`Ollama stream HTTP ${response.status}: ${errText}`);
|
|
305
|
+
}
|
|
306
|
+
if (!response.body) {
|
|
307
|
+
throw new Error('Ollama stream response has no body');
|
|
308
|
+
}
|
|
309
|
+
// Parse NDJSON: each line is a complete JSON object.
|
|
310
|
+
// Accumulate content, tool_calls from the last message chunk, and usage
|
|
311
|
+
// from the final done=true line.
|
|
312
|
+
let accContent = '';
|
|
313
|
+
let finalMessage = {};
|
|
314
|
+
let tokensIn = 0;
|
|
315
|
+
let tokensOut = 0;
|
|
316
|
+
const reader = response.body.getReader();
|
|
317
|
+
const decoder = new TextDecoder();
|
|
318
|
+
let lineBuffer = '';
|
|
319
|
+
try {
|
|
320
|
+
while (true) {
|
|
321
|
+
const { done, value } = await readWithIdleTimeout(reader, streamController, STREAM_TIMEOUT_MS);
|
|
322
|
+
if (done)
|
|
323
|
+
break;
|
|
324
|
+
lineBuffer += decoder.decode(value, { stream: true });
|
|
325
|
+
// Process all complete lines (delimited by '\n')
|
|
326
|
+
let nlIdx;
|
|
327
|
+
while ((nlIdx = lineBuffer.indexOf('\n')) !== -1) {
|
|
328
|
+
const line = lineBuffer.slice(0, nlIdx).trim();
|
|
329
|
+
lineBuffer = lineBuffer.slice(nlIdx + 1);
|
|
330
|
+
if (!line)
|
|
331
|
+
continue;
|
|
332
|
+
let chunk;
|
|
333
|
+
try {
|
|
334
|
+
chunk = JSON.parse(line);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Skip malformed lines
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const chunkMsg = (chunk['message'] ?? {});
|
|
341
|
+
const delta = typeof chunkMsg['content'] === 'string' ? chunkMsg['content'] : '';
|
|
342
|
+
if (delta) {
|
|
343
|
+
accContent += delta;
|
|
344
|
+
onDelta(delta);
|
|
345
|
+
}
|
|
346
|
+
// The final chunk (done:true) carries usage counters and the
|
|
347
|
+
// complete message (including any tool_calls).
|
|
348
|
+
if (chunk['done'] === true) {
|
|
349
|
+
finalMessage = chunkMsg;
|
|
350
|
+
const promptEval = typeof chunk['prompt_eval_count'] === 'number'
|
|
351
|
+
? chunk['prompt_eval_count']
|
|
352
|
+
: 0;
|
|
353
|
+
const evalCount = typeof chunk['eval_count'] === 'number' ? chunk['eval_count'] : 0;
|
|
354
|
+
tokensIn =
|
|
355
|
+
promptEval > 0
|
|
356
|
+
? promptEval
|
|
357
|
+
: estimateTokens(messages.map((m) => m.content).join(' '));
|
|
358
|
+
tokensOut = evalCount > 0 ? evalCount : estimateTokens(accContent);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
reader.releaseLock();
|
|
365
|
+
}
|
|
366
|
+
// Estimate usage if we never saw a done line
|
|
367
|
+
if (tokensIn === 0) {
|
|
368
|
+
tokensIn = estimateTokens(messages.map((m) => m.content).join(' '));
|
|
369
|
+
}
|
|
370
|
+
if (tokensOut === 0) {
|
|
371
|
+
tokensOut = estimateTokens(accContent);
|
|
372
|
+
}
|
|
373
|
+
const toolCalls = parseOllamaToolCalls(finalMessage);
|
|
374
|
+
return {
|
|
375
|
+
content: accContent,
|
|
376
|
+
toolCalls,
|
|
377
|
+
usage: { tokensIn, tokensOut },
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Streaming failed — fall back to non-streaming chat() and emit as one delta.
|
|
382
|
+
const result = await this.chat(messages, tools);
|
|
383
|
+
if (result.content) {
|
|
384
|
+
onDelta(result.content);
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// LM Studio / OpenAI-compat client
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
/**
|
|
395
|
+
* Parse tool calls from an OpenAI-compat response choice message.
|
|
396
|
+
*/
|
|
397
|
+
function parseOpenAIToolCalls(message) {
|
|
398
|
+
const rawCalls = message['tool_calls'];
|
|
399
|
+
if (!Array.isArray(rawCalls) || rawCalls.length === 0)
|
|
400
|
+
return undefined;
|
|
401
|
+
const parsed = [];
|
|
402
|
+
for (const tc of rawCalls) {
|
|
403
|
+
if (typeof tc !== 'object' || tc === null)
|
|
404
|
+
continue;
|
|
405
|
+
const call = tc;
|
|
406
|
+
const id = typeof call['id'] === 'string' ? call['id'] : `call_${parsed.length}`;
|
|
407
|
+
const fn = call['function'];
|
|
408
|
+
if (!fn)
|
|
409
|
+
continue;
|
|
410
|
+
const name = typeof fn['name'] === 'string' ? fn['name'] : 'unknown';
|
|
411
|
+
let args = fn['arguments'] ?? {};
|
|
412
|
+
// Arguments may be a JSON string — parse it
|
|
413
|
+
if (typeof args === 'string') {
|
|
414
|
+
try {
|
|
415
|
+
args = JSON.parse(args);
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// Keep as string if parse fails
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
parsed.push({ id, name, arguments: args });
|
|
422
|
+
}
|
|
423
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Build OpenAI-compat messages array from ChatMessage[].
|
|
427
|
+
* Reused by both chat() and chatStream() in the LM Studio client.
|
|
428
|
+
*/
|
|
429
|
+
function toOpenAIMessages(messages) {
|
|
430
|
+
return messages.map((m) => {
|
|
431
|
+
if (m.role === 'tool') {
|
|
432
|
+
return {
|
|
433
|
+
role: 'tool',
|
|
434
|
+
content: m.content,
|
|
435
|
+
tool_call_id: m.toolCallId ?? '',
|
|
436
|
+
name: m.name,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return { role: m.role, content: m.content };
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function buildLmStudioClient(baseUrl, model, supportsTools) {
|
|
443
|
+
const chatUrl = baseUrl.replace(/\/+$/, '') + '/v1/chat/completions';
|
|
444
|
+
return {
|
|
445
|
+
id: 'lmstudio',
|
|
446
|
+
supportsTools,
|
|
447
|
+
async chat(messages, tools) {
|
|
448
|
+
// Map ChatMessage roles to OpenAI shape
|
|
449
|
+
const openaiMessages = toOpenAIMessages(messages);
|
|
450
|
+
const body = {
|
|
451
|
+
model,
|
|
452
|
+
messages: openaiMessages,
|
|
453
|
+
stream: false,
|
|
454
|
+
};
|
|
455
|
+
if (supportsTools && tools && tools.length > 0) {
|
|
456
|
+
body['tools'] = tools;
|
|
457
|
+
body['tool_choice'] = 'auto';
|
|
458
|
+
}
|
|
459
|
+
let response;
|
|
460
|
+
try {
|
|
461
|
+
response = await fetchWithTimeout(chatUrl, {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
464
|
+
body: JSON.stringify(body),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
469
|
+
throw new Error(`LM Studio fetch failed: ${msg}`);
|
|
470
|
+
}
|
|
471
|
+
if (!response.ok) {
|
|
472
|
+
const errText = await response.text().catch(() => '');
|
|
473
|
+
throw new Error(`LM Studio HTTP ${response.status}: ${errText}`);
|
|
474
|
+
}
|
|
475
|
+
let data;
|
|
476
|
+
try {
|
|
477
|
+
data = (await response.json());
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
throw new Error('LM Studio returned non-JSON response');
|
|
481
|
+
}
|
|
482
|
+
const d = data;
|
|
483
|
+
const choices = Array.isArray(d['choices']) ? d['choices'] : [];
|
|
484
|
+
const firstChoice = (choices[0] ?? {});
|
|
485
|
+
const message = (firstChoice['message'] ?? {});
|
|
486
|
+
const content = typeof message['content'] === 'string' ? message['content'] : '';
|
|
487
|
+
// Parse usage
|
|
488
|
+
const usageRaw = (d['usage'] ?? {});
|
|
489
|
+
const promptTokens = typeof usageRaw['prompt_tokens'] === 'number' ? usageRaw['prompt_tokens'] : 0;
|
|
490
|
+
const completionTokens = typeof usageRaw['completion_tokens'] === 'number' ? usageRaw['completion_tokens'] : 0;
|
|
491
|
+
const tokensIn = promptTokens > 0
|
|
492
|
+
? promptTokens
|
|
493
|
+
: estimateTokens(messages.map((m) => m.content).join(' '));
|
|
494
|
+
const tokensOut = completionTokens > 0 ? completionTokens : estimateTokens(content);
|
|
495
|
+
const toolCalls = parseOpenAIToolCalls(message);
|
|
496
|
+
return {
|
|
497
|
+
content,
|
|
498
|
+
toolCalls,
|
|
499
|
+
usage: { tokensIn, tokensOut },
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
async chatStream(messages, tools, onDelta) {
|
|
503
|
+
// Attempt SSE streaming; fall back to chat() on any error.
|
|
504
|
+
try {
|
|
505
|
+
const openaiMessages = toOpenAIMessages(messages);
|
|
506
|
+
const body = {
|
|
507
|
+
model,
|
|
508
|
+
messages: openaiMessages,
|
|
509
|
+
stream: true,
|
|
510
|
+
};
|
|
511
|
+
if (supportsTools && tools && tools.length > 0) {
|
|
512
|
+
body['tools'] = tools;
|
|
513
|
+
body['tool_choice'] = 'auto';
|
|
514
|
+
}
|
|
515
|
+
let response;
|
|
516
|
+
let streamController;
|
|
517
|
+
try {
|
|
518
|
+
const opened = await fetchStream(chatUrl, {
|
|
519
|
+
method: 'POST',
|
|
520
|
+
headers: { 'Content-Type': 'application/json' },
|
|
521
|
+
body: JSON.stringify(body),
|
|
522
|
+
}, STREAM_TIMEOUT_MS);
|
|
523
|
+
response = opened.response;
|
|
524
|
+
streamController = opened.controller;
|
|
525
|
+
// Headers are in; stop the connect timer. The read loop below arms its
|
|
526
|
+
// own per-read idle watchdog via streamController so a stalled body
|
|
527
|
+
// can't hang indefinitely.
|
|
528
|
+
opened.clearConnectTimer();
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
532
|
+
throw new Error(`LM Studio stream fetch failed: ${msg}`);
|
|
533
|
+
}
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
const errText = await response.text().catch(() => '');
|
|
536
|
+
throw new Error(`LM Studio stream HTTP ${response.status}: ${errText}`);
|
|
537
|
+
}
|
|
538
|
+
if (!response.body) {
|
|
539
|
+
throw new Error('LM Studio stream response has no body');
|
|
540
|
+
}
|
|
541
|
+
// Parse SSE: lines prefixed with "data: "; "[DONE]" marks end.
|
|
542
|
+
// Accumulate content deltas; collect tool_call fragments from the last
|
|
543
|
+
// non-[DONE] chunk; extract usage from the final chunk when present.
|
|
544
|
+
let accContent = '';
|
|
545
|
+
// tool_calls may arrive as fragments across SSE chunks; accumulate index-keyed.
|
|
546
|
+
const toolCallFragments = {};
|
|
547
|
+
let tokensIn = 0;
|
|
548
|
+
let tokensOut = 0;
|
|
549
|
+
const reader = response.body.getReader();
|
|
550
|
+
const decoder = new TextDecoder();
|
|
551
|
+
let lineBuffer = '';
|
|
552
|
+
try {
|
|
553
|
+
while (true) {
|
|
554
|
+
const { done, value } = await readWithIdleTimeout(reader, streamController, STREAM_TIMEOUT_MS);
|
|
555
|
+
if (done)
|
|
556
|
+
break;
|
|
557
|
+
lineBuffer += decoder.decode(value, { stream: true });
|
|
558
|
+
let nlIdx;
|
|
559
|
+
while ((nlIdx = lineBuffer.indexOf('\n')) !== -1) {
|
|
560
|
+
const raw = lineBuffer.slice(0, nlIdx).trim();
|
|
561
|
+
lineBuffer = lineBuffer.slice(nlIdx + 1);
|
|
562
|
+
// SSE lines either start with "data: " or are blank/comment lines.
|
|
563
|
+
if (!raw.startsWith('data:'))
|
|
564
|
+
continue;
|
|
565
|
+
const payload = raw.slice(5).trim(); // strip "data:" prefix
|
|
566
|
+
if (payload === '[DONE]')
|
|
567
|
+
continue;
|
|
568
|
+
let chunk;
|
|
569
|
+
try {
|
|
570
|
+
chunk = JSON.parse(payload);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
// Extract usage when present (some providers send on the last chunk).
|
|
576
|
+
const usageRaw = chunk['usage'];
|
|
577
|
+
if (usageRaw) {
|
|
578
|
+
const pt = typeof usageRaw['prompt_tokens'] === 'number'
|
|
579
|
+
? usageRaw['prompt_tokens']
|
|
580
|
+
: 0;
|
|
581
|
+
const ct = typeof usageRaw['completion_tokens'] === 'number'
|
|
582
|
+
? usageRaw['completion_tokens']
|
|
583
|
+
: 0;
|
|
584
|
+
if (pt > 0)
|
|
585
|
+
tokensIn = pt;
|
|
586
|
+
if (ct > 0)
|
|
587
|
+
tokensOut = ct;
|
|
588
|
+
}
|
|
589
|
+
const choices = Array.isArray(chunk['choices']) ? chunk['choices'] : [];
|
|
590
|
+
const firstChoice = (choices[0] ?? {});
|
|
591
|
+
const delta = (firstChoice['delta'] ?? {});
|
|
592
|
+
// Accumulate content delta
|
|
593
|
+
const contentChunk = typeof delta['content'] === 'string' ? delta['content'] : '';
|
|
594
|
+
if (contentChunk) {
|
|
595
|
+
accContent += contentChunk;
|
|
596
|
+
onDelta(contentChunk);
|
|
597
|
+
}
|
|
598
|
+
// Accumulate tool_call fragments (OpenAI streaming splits them across chunks)
|
|
599
|
+
if (Array.isArray(delta['tool_calls'])) {
|
|
600
|
+
for (const tc of delta['tool_calls']) {
|
|
601
|
+
if (typeof tc !== 'object' || tc === null)
|
|
602
|
+
continue;
|
|
603
|
+
const t = tc;
|
|
604
|
+
const idx = typeof t['index'] === 'number' ? t['index'] : 0;
|
|
605
|
+
if (!toolCallFragments[idx]) {
|
|
606
|
+
toolCallFragments[idx] = { id: '', name: '', argsStr: '' };
|
|
607
|
+
}
|
|
608
|
+
if (typeof t['id'] === 'string') {
|
|
609
|
+
toolCallFragments[idx].id = t['id'];
|
|
610
|
+
}
|
|
611
|
+
const fn = t['function'];
|
|
612
|
+
if (fn) {
|
|
613
|
+
if (typeof fn['name'] === 'string') {
|
|
614
|
+
toolCallFragments[idx].name += fn['name'];
|
|
615
|
+
}
|
|
616
|
+
if (typeof fn['arguments'] === 'string') {
|
|
617
|
+
toolCallFragments[idx].argsStr += fn['arguments'];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
finally {
|
|
626
|
+
reader.releaseLock();
|
|
627
|
+
}
|
|
628
|
+
// Fill in usage estimates if not reported by the server
|
|
629
|
+
if (tokensIn === 0) {
|
|
630
|
+
tokensIn = estimateTokens(messages.map((m) => m.content).join(' '));
|
|
631
|
+
}
|
|
632
|
+
if (tokensOut === 0) {
|
|
633
|
+
tokensOut = estimateTokens(accContent);
|
|
634
|
+
}
|
|
635
|
+
// Reconstruct tool calls from accumulated fragments
|
|
636
|
+
const toolCalls = Object.keys(toolCallFragments).length > 0
|
|
637
|
+
? Object.entries(toolCallFragments).map(([, frag]) => {
|
|
638
|
+
let args = frag.argsStr;
|
|
639
|
+
if (frag.argsStr) {
|
|
640
|
+
try {
|
|
641
|
+
args = JSON.parse(frag.argsStr);
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
// keep as raw string if parse fails
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
id: frag.id || `call_${Math.random().toString(36).slice(2)}`,
|
|
649
|
+
name: frag.name,
|
|
650
|
+
arguments: args,
|
|
651
|
+
};
|
|
652
|
+
})
|
|
653
|
+
: undefined;
|
|
654
|
+
return {
|
|
655
|
+
content: accContent,
|
|
656
|
+
toolCalls,
|
|
657
|
+
usage: { tokensIn, tokensOut },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
// Streaming failed — fall back to non-streaming chat() and emit as one delta.
|
|
662
|
+
const result = await this.chat(messages, tools);
|
|
663
|
+
if (result.content) {
|
|
664
|
+
onDelta(result.content);
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
// Public API
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
/**
|
|
675
|
+
* Build a chat client over the ACTIVE LOCAL provider for `cfg`.
|
|
676
|
+
*
|
|
677
|
+
* LOCAL-FIRST guardrail:
|
|
678
|
+
* - If the active provider is a cloud provider and !allowCloud → throws.
|
|
679
|
+
* - If allowCloud is true but the cloud provider's API key is absent → throws.
|
|
680
|
+
* - If no provider is active → throws.
|
|
681
|
+
*
|
|
682
|
+
* Provider detection:
|
|
683
|
+
* - 'ollama' → POST <ollamaUrl>/api/chat (native Ollama API)
|
|
684
|
+
* - 'lmstudio' → POST <lmstudioUrl>/v1/chat/completions (OpenAI shape)
|
|
685
|
+
*
|
|
686
|
+
* Tool-call detection:
|
|
687
|
+
* - Ollama: enabled for models whose names match known tool-capable patterns.
|
|
688
|
+
* - LM Studio: enabled by default (model capability not reliably detectable
|
|
689
|
+
* without a live call; degrades gracefully on failure).
|
|
690
|
+
*
|
|
691
|
+
* Provider-aware routing (M15):
|
|
692
|
+
* - `opts.provider` forces a specific provider id (e.g. the cloud provider a
|
|
693
|
+
* RouteDecision escalated to) instead of defaulting to registry.activeProvider.
|
|
694
|
+
* This is REQUIRED for cloud escalation to actually target the routed cloud
|
|
695
|
+
* provider rather than silently re-running on the local active provider.
|
|
696
|
+
* The same local-first guards apply: a cloud provider id still throws unless
|
|
697
|
+
* allowCloud AND its key is present (and cloud completions are implemented).
|
|
698
|
+
* - `opts.model` forces the model name (threaded into pickModel) without
|
|
699
|
+
* mutating the shared process.env.ASHLR_MODEL — avoiding the env race under
|
|
700
|
+
* concurrent tasks. ASHLR_MODEL is still honored when opts.model is absent.
|
|
701
|
+
*/
|
|
702
|
+
export async function getActiveClient(cfg, opts) {
|
|
703
|
+
const registry = await getProviderRegistry(cfg);
|
|
704
|
+
// Honor an explicitly-requested provider (router decision) over the chain's
|
|
705
|
+
// first-up provider. This is what makes a cloud escalation actually route to
|
|
706
|
+
// the cloud provider instead of falling through to the local active one.
|
|
707
|
+
const activeId = opts.provider ?? registry.activeProvider;
|
|
708
|
+
if (activeId === null) {
|
|
709
|
+
throw new Error('local-first: no provider is reachable. Start Ollama or LM Studio, or pass --allow-cloud to use a cloud provider.');
|
|
710
|
+
}
|
|
711
|
+
// ---- Cloud-provider guard ----
|
|
712
|
+
if (isCloudProvider(activeId)) {
|
|
713
|
+
if (!opts.allowCloud) {
|
|
714
|
+
throw new Error(`local-first: no local model available; the active provider is '${activeId}' (cloud). ` +
|
|
715
|
+
`Pass --allow-cloud to use it.`);
|
|
716
|
+
}
|
|
717
|
+
// allowCloud is true — verify the API key is present
|
|
718
|
+
const envVar = CLOUD_PROVIDER_ENV[activeId.toLowerCase()];
|
|
719
|
+
if (envVar) {
|
|
720
|
+
const key = process.env[envVar];
|
|
721
|
+
if (!key || key.trim().length === 0) {
|
|
722
|
+
throw new Error(`local-first: --allow-cloud passed but ${envVar} is not set. ` +
|
|
723
|
+
`Set the API key for '${activeId}' and retry.`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Cloud provider with key — not yet implemented in this client (local-first focus).
|
|
727
|
+
// Throw a clear informational error rather than silently doing nothing.
|
|
728
|
+
throw new Error(`local-first: cloud provider '${activeId}' was requested (--allow-cloud) but ` +
|
|
729
|
+
`this client does not yet implement cloud completions. ` +
|
|
730
|
+
`Configure Ollama or LM Studio for local inference.`);
|
|
731
|
+
}
|
|
732
|
+
// ---- Local provider: Ollama ----
|
|
733
|
+
if (activeId === 'ollama') {
|
|
734
|
+
const endpoint = registry.providers.find((p) => p.id === 'ollama');
|
|
735
|
+
if (!endpoint?.up) {
|
|
736
|
+
throw new Error(`local-first: Ollama was selected as the active provider but is not reachable at ${cfg.models.ollama}. ` +
|
|
737
|
+
`Ensure Ollama is running ('ollama serve').`);
|
|
738
|
+
}
|
|
739
|
+
const model = pickModel(endpoint.models, opts.model);
|
|
740
|
+
const supportsTools = modelSupportsTools(model);
|
|
741
|
+
const baseUrl = cfg.models.ollama.replace(/\/+$/, '');
|
|
742
|
+
return buildOllamaClient(baseUrl, model, supportsTools);
|
|
743
|
+
}
|
|
744
|
+
// ---- Local provider: LM Studio ----
|
|
745
|
+
if (activeId === 'lmstudio') {
|
|
746
|
+
const endpoint = registry.providers.find((p) => p.id === 'lmstudio');
|
|
747
|
+
if (!endpoint?.up) {
|
|
748
|
+
throw new Error(`local-first: LM Studio was selected as the active provider but is not reachable at ${cfg.models.lmstudio}. ` +
|
|
749
|
+
`Ensure LM Studio is running with the local server enabled.`);
|
|
750
|
+
}
|
|
751
|
+
const model = pickModel(endpoint.models, opts.model);
|
|
752
|
+
// LM Studio (OpenAI compat) — assume tool support; degrade on error in chat()
|
|
753
|
+
const supportsTools = true;
|
|
754
|
+
const baseUrl = cfg.models.lmstudio.replace(/\/+$/, '');
|
|
755
|
+
return buildLmStudioClient(baseUrl, model, supportsTools);
|
|
756
|
+
}
|
|
757
|
+
// ---- M33: plugin-contributed provider ----
|
|
758
|
+
// Strictly additive: this branch is reachable ONLY when the resolved id
|
|
759
|
+
// matches an enabled plugin provider — builtin routing above is untouched.
|
|
760
|
+
// Cloud-tier plugin providers sit behind the SAME local-first gates as
|
|
761
|
+
// builtin cloud providers (--allow-cloud + declared key env present).
|
|
762
|
+
try {
|
|
763
|
+
const { getPluginProviders } = await import('../plugins/registry.js');
|
|
764
|
+
const pluginProviders = await getPluginProviders(cfg);
|
|
765
|
+
const spec = pluginProviders.find((p) => p.id === activeId);
|
|
766
|
+
if (spec) {
|
|
767
|
+
if (spec.tier === 'cloud') {
|
|
768
|
+
if (!opts.allowCloud) {
|
|
769
|
+
throw new Error(`local-first: provider '${activeId}' is a cloud-tier plugin provider. Pass --allow-cloud to use it.`);
|
|
770
|
+
}
|
|
771
|
+
for (const envKey of spec.envKeys ?? []) {
|
|
772
|
+
const v = process.env[envKey];
|
|
773
|
+
if (!v || v.trim().length === 0) {
|
|
774
|
+
throw new Error(`local-first: --allow-cloud passed but ${envKey} is not set for plugin provider '${activeId}'.`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const client = (await spec.createClient({ model: opts.model }));
|
|
779
|
+
// Runtime shape check — a plugin returning garbage must fail loudly here,
|
|
780
|
+
// not deep inside the agent loop.
|
|
781
|
+
if (!client || typeof client.chat !== 'function' || typeof client.id !== 'string') {
|
|
782
|
+
throw new Error(`plugin provider '${activeId}' returned an invalid client (missing id/chat).`);
|
|
783
|
+
}
|
|
784
|
+
return client;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
// A real gate/validation error must surface; only a missing plugin layer
|
|
789
|
+
// falls through to the unknown-provider error below.
|
|
790
|
+
if (err instanceof Error && !err.message.includes('Cannot find module'))
|
|
791
|
+
throw err;
|
|
792
|
+
}
|
|
793
|
+
// ---- Unknown local provider ----
|
|
794
|
+
throw new Error(`local-first: unknown provider '${activeId}'. Only 'ollama', 'lmstudio', and enabled plugin providers are supported.`);
|
|
795
|
+
}
|
|
796
|
+
//# sourceMappingURL=provider-client.js.map
|