@zenithfoundry/slm-gate 1.2.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.
Files changed (106) hide show
  1. package/.env.example +669 -0
  2. package/LICENSE +21 -0
  3. package/README.md +317 -0
  4. package/configs/antigravity/.env.16gb.example +674 -0
  5. package/configs/antigravity/.env.24gb.example +674 -0
  6. package/configs/antigravity/.env.32gb.example +674 -0
  7. package/configs/antigravity/README.md +109 -0
  8. package/configs/claude-code/.env.16gb.example +674 -0
  9. package/configs/claude-code/.env.24gb.example +674 -0
  10. package/configs/claude-code/.env.32gb.example +674 -0
  11. package/configs/claude-code/README.md +52 -0
  12. package/configs/claude-desktop/.env.16gb.example +674 -0
  13. package/configs/claude-desktop/.env.24gb.example +674 -0
  14. package/configs/claude-desktop/.env.32gb.example +674 -0
  15. package/configs/claude-desktop/README.md +37 -0
  16. package/configs/cline-continue-opencode/.env.16gb.example +674 -0
  17. package/configs/cline-continue-opencode/.env.24gb.example +674 -0
  18. package/configs/cline-continue-opencode/.env.32gb.example +674 -0
  19. package/configs/cline-continue-opencode/README.md +34 -0
  20. package/configs/cursor/.env.16gb.example +674 -0
  21. package/configs/cursor/.env.24gb.example +674 -0
  22. package/configs/cursor/.env.32gb.example +674 -0
  23. package/configs/cursor/README.md +26 -0
  24. package/configs/generic-http/.env.16gb.example +674 -0
  25. package/configs/generic-http/.env.24gb.example +674 -0
  26. package/configs/generic-http/.env.32gb.example +674 -0
  27. package/configs/generic-http/README.md +20 -0
  28. package/configs/generic-stdio/.env.16gb.example +674 -0
  29. package/configs/generic-stdio/.env.24gb.example +674 -0
  30. package/configs/generic-stdio/.env.32gb.example +674 -0
  31. package/configs/generic-stdio/README.md +24 -0
  32. package/configs/preserve/README.md +26 -0
  33. package/configs/preserve/tls.json +61 -0
  34. package/dist/adapters/tech-lead-stack.js +38 -0
  35. package/dist/cache/index.js +173 -0
  36. package/dist/cli.js +256 -0
  37. package/dist/config.js +255 -0
  38. package/dist/dashboard/data.js +149 -0
  39. package/dist/dashboard/export.js +42 -0
  40. package/dist/dashboard/serve.js +63 -0
  41. package/dist/doctor.js +338 -0
  42. package/dist/hardware.js +126 -0
  43. package/dist/home-dir.js +39 -0
  44. package/dist/ledger/flush-lifecycle.js +50 -0
  45. package/dist/ledger/index.js +946 -0
  46. package/dist/ledger/report.js +69 -0
  47. package/dist/ledger/setup-dashboard.js +456 -0
  48. package/dist/ledger/smoke.js +37 -0
  49. package/dist/ledger/sync-config.js +177 -0
  50. package/dist/ledger/sync.js +307 -0
  51. package/dist/ledger/verify.js +185 -0
  52. package/dist/ledger/wipe-langfuse.js +130 -0
  53. package/dist/llm-gate/distill.js +239 -0
  54. package/dist/llm-gate/formats/anthropic.js +185 -0
  55. package/dist/llm-gate/formats/chat-completions.js +103 -0
  56. package/dist/llm-gate/formats/contract.js +29 -0
  57. package/dist/llm-gate/formats/gemini.js +84 -0
  58. package/dist/llm-gate/formats/internal.js +1 -0
  59. package/dist/llm-gate/formats/openai.js +77 -0
  60. package/dist/llm-gate/formats/responses.js +146 -0
  61. package/dist/llm-gate/forward.js +150 -0
  62. package/dist/llm-gate/index.js +40 -0
  63. package/dist/llm-gate/local-first.js +217 -0
  64. package/dist/llm-gate/pipeline.js +267 -0
  65. package/dist/llm-gate/server.js +289 -0
  66. package/dist/mcp-gate/ground.js +64 -0
  67. package/dist/mcp-gate/index.js +57 -0
  68. package/dist/mcp-gate/pipeline.js +252 -0
  69. package/dist/mcp-gate/server.js +302 -0
  70. package/dist/mcp-gate/tool-names.js +57 -0
  71. package/dist/models/check.js +26 -0
  72. package/dist/models/footprint.js +137 -0
  73. package/dist/models/helpers.js +91 -0
  74. package/dist/models/index.js +5 -0
  75. package/dist/models/reasoning.js +91 -0
  76. package/dist/models/roles.js +9 -0
  77. package/dist/models/slm.js +243 -0
  78. package/dist/models/types.js +1 -0
  79. package/dist/pricing/index.js +115 -0
  80. package/dist/pricing/plans.js +54 -0
  81. package/dist/pricing/providers.js +172 -0
  82. package/dist/resolver/index.js +277 -0
  83. package/dist/resolver/types.js +1 -0
  84. package/dist/setup/claim.js +41 -0
  85. package/dist/setup/gate-command.js +41 -0
  86. package/dist/setup/init.js +92 -0
  87. package/dist/setup/local-models.js +123 -0
  88. package/dist/setup/model-gate.js +220 -0
  89. package/dist/setup/notify.js +45 -0
  90. package/dist/setup/ollama-install.js +53 -0
  91. package/dist/setup/parent-watch.js +84 -0
  92. package/dist/setup/required-models.js +20 -0
  93. package/dist/setup/startup.js +132 -0
  94. package/dist/setup/tool-settings.js +101 -0
  95. package/dist/utils/backoff.js +47 -0
  96. package/dist/utils/compression.js +145 -0
  97. package/dist/utils/constants.js +22 -0
  98. package/dist/utils/duration.js +43 -0
  99. package/dist/utils/elision.js +556 -0
  100. package/dist/utils/embedding.js +32 -0
  101. package/dist/utils/entry-point.js +23 -0
  102. package/dist/utils/local-only.js +82 -0
  103. package/dist/utils/preserve-patterns.js +115 -0
  104. package/dist/utils/safety.js +30 -0
  105. package/dist/verifier/index.js +67 -0
  106. package/package.json +121 -0
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @fileoverview "Only programs on this computer": the one rule both slm-gate HTTP servers apply (the model
3
+ * gate on LLM_GATE_PORT and the MCP server's HTTP mode on MCP_GATE_PORT). Kept in one place because it is a
4
+ * security rule and two copies could drift.
5
+ *
6
+ * Two parts, because binding alone is not enough:
7
+ * - Listen on the loopback addresses only, so other machines on the network cannot connect.
8
+ * - Refuse requests addressed to another name (Host) or sent by a web page from another site (Origin).
9
+ * A page open in the browser runs on this computer too: through DNS rebinding it can reach 127.0.0.1
10
+ * under its own domain name, and any page can send cross-site requests to localhost.
11
+ */
12
+ import http from 'node:http';
13
+ /** Names for this computer. Other spellings (`localhost.`, `127.1`, …) are refused: fail closed. */
14
+ const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
15
+ /** A Host header's name without the port or IPv6 brackets, e.g. `[::1]:8787` → `::1`. */
16
+ function hostnameOf(host) {
17
+ const value = host.trim().toLowerCase();
18
+ if (value.startsWith('['))
19
+ return value.slice(1, value.indexOf(']'));
20
+ const colon = value.lastIndexOf(':');
21
+ return colon === -1 ? value : value.slice(0, colon);
22
+ }
23
+ function isLocalOrigin(origin) {
24
+ try {
25
+ const url = new URL(origin);
26
+ const hostname = url.hostname.replace(/^\[|\]$/g, '');
27
+ return (url.protocol === 'http:' || url.protocol === 'https:') && LOCAL_HOSTNAMES.has(hostname);
28
+ }
29
+ catch {
30
+ return false; // includes the literal `null` sent by sandboxed pages and file:// documents
31
+ }
32
+ }
33
+ /**
34
+ * True for a request addressed to this computer by name, and either sent by a program (no Origin) or by a
35
+ * web page served from this computer. Coding tools' model clients send no Origin.
36
+ */
37
+ export function isLocalRequest(headers) {
38
+ if (!headers.host || !LOCAL_HOSTNAMES.has(hostnameOf(headers.host)))
39
+ return false;
40
+ return headers.origin === undefined || isLocalOrigin(headers.origin);
41
+ }
42
+ function listenOn(params) {
43
+ return new Promise((resolve, reject) => {
44
+ params.server.once('error', reject);
45
+ params.server.listen({ host: params.host, port: params.port, ipv6Only: params.host === '::1' }, () => {
46
+ params.server.off('error', reject);
47
+ resolve();
48
+ });
49
+ });
50
+ }
51
+ /**
52
+ * Serves `handler` on 127.0.0.1 and ::1, so `localhost` works whichever address a client's resolver picks
53
+ * first (macOS answers ::1 first, and not every client falls back).
54
+ *
55
+ * @param params.port The port (0 picks a free one, the same for both addresses)
56
+ * @param params.onListening Called once both addresses listen
57
+ * @returns The listening servers (to close them)
58
+ * @throws When 127.0.0.1 cannot be used (e.g. the port is taken), or ::1 fails for any reason other than
59
+ * IPv6 being unavailable; nothing is left listening then
60
+ */
61
+ export async function listenOnThisComputer(params) {
62
+ const ipv4 = http.createServer(params.handler);
63
+ await listenOn({ server: ipv4, host: '127.0.0.1', port: params.port });
64
+ const port = ipv4.address().port;
65
+ const ipv6 = http.createServer(params.handler);
66
+ try {
67
+ await listenOn({ server: ipv6, host: '::1', port });
68
+ }
69
+ catch (err) {
70
+ const code = err.code;
71
+ if (code !== 'EADDRNOTAVAIL' && code !== 'EAFNOSUPPORT') {
72
+ // E.g. another program holds [::1]:port: `localhost` clients would reach it, not us.
73
+ await new Promise(resolve => ipv4.close(resolve));
74
+ throw err;
75
+ }
76
+ console.error(`[slm-gate] IPv6 is not available on this computer; listening on 127.0.0.1:${port} only.`);
77
+ params.onListening?.();
78
+ return [ipv4];
79
+ }
80
+ params.onListening?.();
81
+ return [ipv4, ipv6];
82
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'fs/promises';
2
+ import { CONFIG } from '../config.js';
3
+ /**
4
+ * A rigid set of baseline RegExp patterns used to identify lines that MUST NEVER be dropped
5
+ * during local offline Small Language Model (SLM) contextual compression.
6
+ *
7
+ * The system ensures that any line matching these patterns is explicitly preserved and shielded
8
+ * via placeholder substitution before handing the text to the SLM. This guarantees that
9
+ * critical structural integrity, hard requirements, and specific directives survive the
10
+ * lossy distillation process.
11
+ */
12
+ export const BUILTIN_PATTERNS = [
13
+ // requirement keywords
14
+ /.*(?:MUST|MUST NOT|SHALL|REQUIRED|SHOULD NOT|NEVER|ALWAYS|DO NOT|MANDATORY|PROHIBITED|IMPORTANT|CRITICAL|WARNING|CAUTION).*/i,
15
+ // markdown headings
16
+ /^#{1,6}\s/,
17
+ // numbered steps
18
+ /^\s*\d+[.)]\s/,
19
+ // checklist items
20
+ /^\s*[-*]\s+\[[ xX]\]/,
21
+ // fenced code blocks / inline code lines (contain backticks)
22
+ /`/,
23
+ // URLs
24
+ /https?:\/\//,
25
+ // path/KEY[:=]value config lines
26
+ /^[A-Za-z0-9_.\/-]+[:=]/,
27
+ // output/format directives
28
+ /(?:format:|respond with|output must|Answer:)/i,
29
+ // hard quantities
30
+ /(?:no more than \d|at most|exactly \d|maximum|minimum)/i,
31
+ // YAML frontmatter block markers
32
+ /^---/,
33
+ ];
34
+ /**
35
+ * Attempts to compile a regular expression string into an executable RegExp object.
36
+ * It first attempts to use `re2` (a fast, linear-time regex engine safe against ReDoS attacks)
37
+ * if installed. If `re2` is missing or rejects the pattern (e.g., due to unsupported features
38
+ * like complex lookaheads), it gracefully falls back to the native V8 RegExp engine.
39
+ *
40
+ * @param pattern - The raw regular expression string to compile
41
+ * @returns The compiled RegExp instance, or null if the pattern is fatally invalid
42
+ */
43
+ async function compilePattern(pattern) {
44
+ let Re2;
45
+ try {
46
+ // @ts-ignore - optional dependency
47
+ Re2 = (await import('re2')).default;
48
+ }
49
+ catch {
50
+ // re2 not installed, will fallback
51
+ }
52
+ if (Re2) {
53
+ try {
54
+ return new Re2(pattern);
55
+ }
56
+ catch (e) {
57
+ // Re2 threw (e.g. unsupported syntax like lookaheads), fallback to native RegExp
58
+ }
59
+ }
60
+ try {
61
+ return new RegExp(pattern);
62
+ }
63
+ catch (e) {
64
+ console.error(`[distill] Warning: Skipping invalid regex pattern: ${pattern}`);
65
+ return null;
66
+ }
67
+ }
68
+ /**
69
+ * Constructs the aggregate array of preservation Regex patterns for the current session.
70
+ * Depending on the configuration (`DISTILL_PRESERVE_MODE`), it will either:
71
+ * - 'extend': Combine `BUILTIN_PATTERNS`, user-defined JSON patterns, and adapter-specific patterns.
72
+ * - 'replace': Ignore `BUILTIN_PATTERNS` entirely, relying only on user/adapter specifications.
73
+ *
74
+ * @returns A promise resolving to the final array of compiled RegExp patterns
75
+ */
76
+ export async function buildPreserveList() {
77
+ let patterns = [];
78
+ const mode = CONFIG.DISTILL_PRESERVE_MODE || 'extend';
79
+ if (mode === 'extend') {
80
+ patterns = [...BUILTIN_PATTERNS];
81
+ }
82
+ // Load custom preservation patterns defined in an external JSON file by the user
83
+ if (CONFIG.DISTILL_PRESERVE_PATH) {
84
+ try {
85
+ const data = await fs.readFile(CONFIG.DISTILL_PRESERVE_PATH, 'utf-8');
86
+ const parsed = JSON.parse(data);
87
+ if (Array.isArray(parsed.patterns)) {
88
+ for (const p of parsed.patterns) {
89
+ const compiled = await compilePattern(p);
90
+ if (compiled) {
91
+ patterns.push(compiled);
92
+ }
93
+ }
94
+ }
95
+ }
96
+ catch (e) {
97
+ console.error(`[distill] Warning: Failed to read or parse user preserve patterns from ${CONFIG.DISTILL_PRESERVE_PATH}`);
98
+ }
99
+ }
100
+ // Optionally load tech-lead-stack patterns if running in decoupled integration mode
101
+ if (CONFIG.TLS_ADAPTER) {
102
+ try {
103
+ // Guarded dynamic import
104
+ // @ts-ignore - Decoupling: adapter may not exist in pure MCP-gate configurations
105
+ const adapter = await import('../adapters/tech-lead-stack.js');
106
+ if (adapter.tlsPreservePatterns) {
107
+ patterns.push(...adapter.tlsPreservePatterns);
108
+ }
109
+ }
110
+ catch (e) {
111
+ console.error(`[distill] Warning: Failed to load TLS adapter patterns`, e);
112
+ }
113
+ }
114
+ return patterns;
115
+ }
@@ -0,0 +1,30 @@
1
+ import { TOOL_RESULT_PREFIXES, CURSOR_TOOL_MARKERS } from './constants.js';
2
+ /**
3
+ * Heuristic to detect if the *last* message in the conversation actually represents
4
+ * a tool response or a file output, rather than the user's own turn.
5
+ *
6
+ * If it is a tool output, it's unsafe to locally defer since we shouldn't execute
7
+ * commands derived directly from data.
8
+ */
9
+ export function isLatestInstructionFromTool(messages) {
10
+ if (messages.length === 0)
11
+ return false;
12
+ const last = messages[messages.length - 1];
13
+ if (last.role === 'tool') {
14
+ return true;
15
+ }
16
+ // Some clients encode tool results as 'user' or 'assistant' messages.
17
+ const content = last.content.trim();
18
+ if (TOOL_RESULT_PREFIXES.some(prefix => content.startsWith(prefix))) {
19
+ return true;
20
+ }
21
+ if (CURSOR_TOOL_MARKERS.some(marker => content.includes(marker))) {
22
+ return true;
23
+ }
24
+ // A very common pattern: JSON objects embedded directly at the start that look like tool returns
25
+ if (content.startsWith('{"') && content.includes('}') && (content.includes('result') || content.includes('output'))) {
26
+ // Basic heuristic for raw JSON return
27
+ return true;
28
+ }
29
+ return false;
30
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The Verifier Module for small-language-model-gate.
3
+ *
4
+ * This module is responsible for analyzing the output of the local Small Language Model (SLM)
5
+ * and deciding whether the response is acceptable or if the request must be escalated to the
6
+ * downstream cloud LLM. It acts as a safety and quality gate.
7
+ *
8
+ * It is primarily used by the `llm-gate` routing layer to evaluate the confidence and
9
+ * structural validity of local inferences before deferring to them.
10
+ *
11
+ * The verification strictness is monotonic (levels 1-5), meaning each level is a superset
12
+ * of the one below it. As the strictness level increases, the likelihood of escalation also increases.
13
+ *
14
+ * Strictness Levels:
15
+ * - Level 1: Escalates if the answer is completely empty.
16
+ * - Level 2: Escalates if the answer fails a provided regex format constraint.
17
+ * - Level 3: Escalates if the answer contains hedging language (e.g., "I'm not sure").
18
+ * - Level 4: Escalates if multiple generated samples disagree with each other.
19
+ * - Level 5: Always escalates to the cloud model (bypasses local SLM entirely).
20
+ */
21
+ import { checkAgreement } from '../models/reasoning.js';
22
+ export const HEDGING_LEXICON = [
23
+ "i'm not sure",
24
+ "i am not sure",
25
+ "i think",
26
+ "possibly",
27
+ "cannot determine",
28
+ "unclear",
29
+ "it's unclear"
30
+ ];
31
+ const HEDGING_REGEX = new RegExp(`\\b(${HEDGING_LEXICON.join('|')})\\b`, 'i');
32
+ export function verify(answer, samples, constraints, level) {
33
+ const flags = [];
34
+ // Level 1: empty or unparseable
35
+ if (level >= 1) {
36
+ if (answer.trim() === '') {
37
+ flags.push('empty');
38
+ }
39
+ }
40
+ // Level 2: fails format constraint
41
+ if (level >= 2) {
42
+ if (constraints.formatRe && !constraints.formatRe.test(answer)) {
43
+ flags.push('format_mismatch');
44
+ }
45
+ }
46
+ // Level 3: hedging language
47
+ if (level >= 3) {
48
+ if (HEDGING_REGEX.test(answer)) {
49
+ flags.push('hedging');
50
+ }
51
+ }
52
+ // Level 4: samples disagree
53
+ if (level >= 4) {
54
+ if (samples.length > 0 && checkAgreement(samples) === null) {
55
+ flags.push('disagreement');
56
+ }
57
+ }
58
+ // Level 5: always escalate
59
+ if (level >= 5) {
60
+ flags.push('always_escalate');
61
+ }
62
+ return {
63
+ escalate: flags.length > 0,
64
+ level,
65
+ flags
66
+ };
67
+ }
package/package.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "name": "@zenithfoundry/slm-gate",
3
+ "version": "1.2.1",
4
+ "type": "module",
5
+ "description": "A decoupled local-SLM pre-processing and routing layer for MCP and cloud LLMs. Intercepts, distills, and routes prompts locally with Ollama to cut token costs and protect monthly quota.",
6
+ "license": "MIT",
7
+ "author": "bronz3beard",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/zenithfoundry/slm-gate.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/zenithfoundry/slm-gate/issues"
14
+ },
15
+ "homepage": "https://github.com/zenithfoundry/slm-gate#readme",
16
+ "keywords": [
17
+ "ai",
18
+ "slm",
19
+ "small-language-model",
20
+ "mcp",
21
+ "mcp-server",
22
+ "model-context-protocol",
23
+ "local-ai",
24
+ "ollama",
25
+ "qwen",
26
+ "prompt-compression",
27
+ "llm-routing",
28
+ "token-optimization",
29
+ "cost-reduction",
30
+ "agentic-workflows",
31
+ "ai-agents",
32
+ "antigravity",
33
+ "cursor",
34
+ "claude-code"
35
+ ],
36
+ "bin": {
37
+ "slm-gate": "./dist/cli.js"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "!dist/**/*.test.js",
42
+ "configs",
43
+ ".env.example"
44
+ ],
45
+ "engines": {
46
+ "node": ">=22",
47
+ "pnpm": ">=10"
48
+ },
49
+ "dependencies": {
50
+ "@langfuse/client": "^5.11.1",
51
+ "@langfuse/otel": "^5.11.1",
52
+ "@langfuse/tracing": "^5.11.1",
53
+ "@modelcontextprotocol/sdk": "^1.30.1",
54
+ "@opentelemetry/sdk-node": "^0.222.0",
55
+ "better-sqlite3": "^13.0.3",
56
+ "dotenv": "^16.4.5",
57
+ "markdown-it": "^15.0.2",
58
+ "ollama": "^0.6.3",
59
+ "zod": "^3.23.8",
60
+ "zod-to-json-schema": "^3.23.1"
61
+ },
62
+ "optionalDependencies": {
63
+ "re2": "^1.21.4"
64
+ },
65
+ "devDependencies": {
66
+ "@jest/globals": "^30.5.2",
67
+ "@types/better-sqlite3": "^7.6.11",
68
+ "@types/jest": "^29.5.12",
69
+ "@types/markdown-it": "^14.2.0",
70
+ "@types/node": "^20.14.9",
71
+ "fast-check": "^4.10.2",
72
+ "jest": "^29.7.0",
73
+ "ts-jest": "^29.4.13",
74
+ "tsx": "^4.23.15",
75
+ "typescript": "^5.5.3"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ },
80
+ "scripts": {
81
+ "slm-gate": "tsx src/cli.ts",
82
+ "build": "tsc",
83
+ "dev": "tsx src/cli.ts",
84
+ "config": "node --import tsx src/config.ts",
85
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests",
86
+ "test:summary": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests --reporters=\"summary\"",
87
+ "test:decoupling": "bash -c 'trap \"mv .src_adapters_tmp src/adapters 2>/dev/null\" EXIT; mv src/adapters .src_adapters_tmp; pnpm run build && pnpm run test'",
88
+ "models:check": "node --import tsx src/models/check.ts",
89
+ "ledger:smoke": "tsx src/ledger/smoke.ts",
90
+ "llm-gate:serve": "tsx src/llm-gate/index.ts",
91
+ "mcp-gate:stdio": "tsx src/mcp-gate/index.ts",
92
+ "mcp-gate:http": "tsx src/mcp-gate/index.ts",
93
+ "e2e:standalone-mcp": "tsx scripts/e2e-standalone-mcp.ts",
94
+ "e2e:llm-gate-routing": "tsx scripts/e2e-llm-gate-routing.ts",
95
+ "e2e:distill-fallback": "tsx scripts/e2e-distill-fallback.ts",
96
+ "e2e:resolver-ambiguities": "tsx scripts/e2e-resolver-ambiguities.ts",
97
+ "smoke:e2e": "tsx scripts/smoke-e2e.ts",
98
+ "e2e:tls-downstream": "tsx scripts/e2e-tls-downstream.ts",
99
+ "e2e:npm-install": "tsx scripts/e2e-npm-install.ts",
100
+ "test:e2e": "pnpm run build && pnpm run e2e:standalone-mcp && pnpm run e2e:llm-gate-routing && pnpm run e2e:distill-fallback && pnpm run e2e:resolver-ambiguities && pnpm run smoke:e2e",
101
+ "e2e": "pnpm run test:e2e",
102
+ "bench": "tsx harness/run.ts",
103
+ "bench:reset": "rm -rf harness/.cache && pnpm run bench",
104
+ "metrics": "tsx harness/metrics.ts",
105
+ "migrate:provider": "tsx scripts/migrations/001-add-provider.ts",
106
+ "ledger:sync": "tsx src/ledger/sync.ts",
107
+ "ledger:verify": "tsx src/ledger/verify.ts",
108
+ "ledger:report": "tsx src/ledger/report.ts",
109
+ "dashboard": "tsx src/dashboard/serve.ts",
110
+ "dashboard:export": "tsx src/dashboard/export.ts",
111
+ "ledger:reset": "rm -f output/ledger.sqlite output/ledger.sqlite-wal output/ledger.sqlite-shm output/deferral_curve.svg output/leaderboard.md",
112
+ "ledger:wipe-langfuse": "tsx src/ledger/wipe-langfuse.ts",
113
+ "langfuse:wipe": "tsx src/ledger/wipe-langfuse.ts",
114
+ "langfuse:setup-dashboard": "tsx src/ledger/setup-dashboard.ts",
115
+ "elision:stats": "node scripts/elision-clean.mjs --stats",
116
+ "elision:clean": "node scripts/elision-clean.mjs --interactive",
117
+ "elision:clean:dry": "node scripts/elision-clean.mjs --older-than 180d --dry-run",
118
+ "elision:clean:old": "node scripts/elision-clean.mjs --older-than 180d",
119
+ "elision:clean:all": "node scripts/elision-clean.mjs --all"
120
+ }
121
+ }