@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,24 @@
1
+ # Generic Stdio MCP Configuration
2
+
3
+ **File Location:** Depends on your MCP client's configuration schema.
4
+
5
+ ```json
6
+ {
7
+ "mcpServers": {
8
+ "slm-gate": {
9
+ "command": "node",
10
+ "args": [
11
+ "<ABS_PATH>/dist/mcp-gate/index.js"
12
+ ],
13
+ "env": {
14
+ "TLS_ADAPTER": "on",
15
+ "DOWNSTREAM_MCP": "{\"command\":\"node\",\"args\":[\"<ABS_PATH_TO_TLS>/dist/mcp-server.mjs\"]}"
16
+ }
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ After adding this, make sure to build TLS first by running `pnpm run mcp:build` in your TLS directory, then restart/refresh MCP servers and verify with `slm-gate doctor`.
23
+
24
+ When your client starts this MCP server, it also starts the model gate (Layer 2) if it isn't running. If your client has a setting for the model's address, `slm-gate doctor` prints the line for the tools it knows; the `env` values above never reach the model gate, which reads only `slm-gate`'s `.env`.
@@ -0,0 +1,26 @@
1
+ # Preserved Patterns Configuration
2
+
3
+ This directory contains JSON files specifying regular expression patterns that the distillation engine will protect.
4
+
5
+ ## The Contract
6
+ Files must follow the `{ "patterns": string[] }` schema:
7
+ ```json
8
+ {
9
+ "patterns": [
10
+ "^\\s*\\|.*\\|\\s*$",
11
+ "<!--\\s*slm-gate:verbatim-start\\s*-->"
12
+ ]
13
+ }
14
+ ```
15
+
16
+ ## Extend vs Replace
17
+ Controlled by `DISTILL_PRESERVE_MODE` in your `.env`:
18
+ - `extend`: Your patterns are appended to the built-in defaults.
19
+ - `replace`: Only your patterns (plus any adapter patterns like TLS) are used.
20
+
21
+ ## RE2 Safety
22
+ Patterns are executed in Node.js using V8's regex engine. Avoid pathological backtracking by keeping patterns simple and bounded.
23
+
24
+ ## The Line-Based Caveat
25
+ The regex preservation engine operates **line-by-line**. It cannot inherently protect multi-line blocks (like a fenced code block) unless every internal line independently matches a pattern.
26
+ For atomic structural protection, `slm-gate` employs an AST-based tokenizer (Phase 1) and adaptive DB-driven policies (Phase 2 & 3). Regexes serve as the fallback floor.
@@ -0,0 +1,61 @@
1
+ {
2
+ "patterns": [
3
+ "^name:",
4
+ "^description:",
5
+ "^phase:",
6
+ "^kind:",
7
+ "^domain:",
8
+ "^spans:",
9
+ "^targets:",
10
+ "^minModelClass:",
11
+ "^ownership:",
12
+ "^drive:",
13
+ "^approve:",
14
+ "cost: ~\\d+ tokens",
15
+ "^modes:",
16
+ "^surface:",
17
+ "^category:",
18
+ "^policies:",
19
+
20
+ "G-Stack / Diagnosis-First",
21
+ "MinimumCD",
22
+ "^## MinimumCD",
23
+ "Production-Grade Ethos",
24
+ "Modern Web Guidance",
25
+ "^## Four Pillars",
26
+
27
+ "^Phase \\d",
28
+ "^## Phase",
29
+ "^### Step \\d",
30
+
31
+ "^// turbo-all",
32
+ "^\\*\\*CRITICAL: PHASE 0",
33
+ "^> \\[!(IMPORTANT|WARNING|CAUTION|NOTE|TIP)\\]",
34
+
35
+ "^## Commands",
36
+ "^## Stack quirks",
37
+ "^## MCP tool naming",
38
+ "^## Policies & Execution",
39
+ "^## Git discipline",
40
+ "^## Code style",
41
+ "^## Scope",
42
+
43
+ "^## Objective",
44
+ "^## ⛔️ STRICT GUARDRAILS",
45
+ "^## Guardrails",
46
+ "^## 🛠 Execution Steps",
47
+ "^## Execution Steps",
48
+ "^## Code Modification Convention",
49
+ "^## Runtime modes",
50
+ "^## Pre-Flight Model Contract",
51
+ "^## Telemetry",
52
+ "^## Three Mandatory End-State Disclosures",
53
+ "^## 🎯",
54
+ "^## 🔍 Validation Gates",
55
+ "^## 🛠 Outcome Actions",
56
+ "^## 🔒",
57
+ "^# Lane:",
58
+ "^## Quota Discipline",
59
+ "^## Quality Verification"
60
+ ]
61
+ }
@@ -0,0 +1,38 @@
1
+ export const tlsPreservePatterns = [
2
+ /cost: ~\d+ tokens/i,
3
+ /^modes:/i,
4
+ /^## MinimumCD/i,
5
+ /^## Quality Verification/i,
6
+ /^Phase \d/i
7
+ ];
8
+ export function mapLedgerEventToTlsAnalytics(event) {
9
+ return {
10
+ eventId: event.request_id,
11
+ timestamp: event.ts,
12
+ sessionId: event.session_id,
13
+ toolName: event.skill || 'unknown',
14
+ routingMode: event.route,
15
+ localModel: event.slm_model,
16
+ cloudModel: event.api_model,
17
+ tokens: {
18
+ input: event.api_in_tok,
19
+ output: event.api_out_tok,
20
+ localInput: event.in_tok,
21
+ localOutput: event.out_tok
22
+ },
23
+ latency: {
24
+ local: event.slm_latency_s,
25
+ cloud: event.api_latency_s
26
+ },
27
+ tags: [
28
+ `slm_gate=${event.slm_gate}`
29
+ ]
30
+ };
31
+ }
32
+ export function buildTlsDownstreamConfig(tlsRepoPath) {
33
+ return {
34
+ command: 'node',
35
+ args: [`${tlsRepoPath}/dist/mcp-server.mjs`],
36
+ env: {}
37
+ };
38
+ }
@@ -0,0 +1,173 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { CONFIG } from '../config.js';
5
+ import { getDb } from '../ledger/index.js';
6
+ import { cosineSimilarity, embedText } from '../utils/embedding.js';
7
+ let dbInitialized = false;
8
+ /**
9
+ * Initializes the semantic cache table in the local SQLite ledger.
10
+ * This is called lazily before cache operations to ensure the `semcache`
11
+ * table exists without blocking the main application boot.
12
+ */
13
+ export function initCacheDb() {
14
+ if (dbInitialized)
15
+ return;
16
+ const db = getDb();
17
+ db.exec(`
18
+ CREATE TABLE IF NOT EXISTS semcache (
19
+ id TEXT PRIMARY KEY,
20
+ embedding_blob BLOB,
21
+ response TEXT,
22
+ file_hashes TEXT,
23
+ ts TEXT
24
+ );
25
+ `);
26
+ dbInitialized = true;
27
+ }
28
+ /**
29
+ * Calculates the cosine similarity between two high-dimensional vectors.
30
+ * A score of 1.0 means perfectly identical directions (identical meaning),
31
+ * while 0 means orthogonal (no semantic overlap).
32
+ *
33
+ * @param a The first vector (e.g., the current prompt's embedding)
34
+ * @param b The second vector (e.g., a stored prompt's embedding)
35
+ * @returns A similarity score between -1.0 and 1.0
36
+ */
37
+ /**
38
+ * Scans a prompt text for potential file paths, reads those files from disk,
39
+ * and returns a map of their SHA-256 hashes. This acts as a stale-context guard:
40
+ * if a referenced file is edited, its hash changes, invalidating previous cache hits.
41
+ *
42
+ * @param text The raw prompt or task text containing potential file paths.
43
+ * @returns A dictionary mapping absolute file paths to their SHA-256 hashes.
44
+ */
45
+ export async function getReferencedFilesHashes(text) {
46
+ const hashes = {};
47
+ // Regex to extract standard file paths or file:// URIs
48
+ const regex = /(?:file:\/\/)?(\/?(?:[a-zA-Z0-9_\-\.]+\/)+[a-zA-Z0-9_\-\.]+)/g;
49
+ let match;
50
+ const potentialPaths = new Set();
51
+ while ((match = regex.exec(text)) !== null) {
52
+ potentialPaths.add(match[1]);
53
+ }
54
+ // Secondary regex for isolated relative paths like "src/index.ts"
55
+ const wordRegex = /([a-zA-Z0-9_\-\.\/]+)/g;
56
+ while ((match = wordRegex.exec(text)) !== null) {
57
+ if (match[1].includes('.') && match[1].includes('/')) {
58
+ potentialPaths.add(match[1]);
59
+ }
60
+ }
61
+ for (let p of potentialPaths) {
62
+ // Strip URI protocol if present
63
+ if (p.startsWith('file://'))
64
+ p = p.substring(7);
65
+ // Resolve relative paths against the project root
66
+ let fullPath = p;
67
+ if (!path.isAbsolute(fullPath)) {
68
+ fullPath = path.join(CONFIG.ROOT_DIR, fullPath);
69
+ }
70
+ try {
71
+ // Hash the file contents if it exists and is a valid file
72
+ const stat = await fs.stat(fullPath);
73
+ if (stat.isFile()) {
74
+ const content = await fs.readFile(fullPath);
75
+ hashes[fullPath] = crypto.createHash('sha256').update(content).digest('hex');
76
+ }
77
+ }
78
+ catch {
79
+ // Safely ignore missing files or invalid paths
80
+ }
81
+ }
82
+ return hashes;
83
+ }
84
+ /**
85
+ * Checks the semantic cache for a sufficiently similar previous request.
86
+ *
87
+ * Flow:
88
+ * 1. Generates an embedding for the current request using the local model.
89
+ * 2. Extracts and hashes any files referenced in the current request.
90
+ * 3. Scans stored cache entries:
91
+ * a. Rejects if the stored referenced file hashes do not match exactly.
92
+ * b. Calculates cosine similarity of the prompt embeddings.
93
+ * c. Accepts and returns the stored response if similarity >= threshold.
94
+ *
95
+ * @param text The current task or prompt text to evaluate.
96
+ * @returns The cached response object/string if a match is found, otherwise null.
97
+ */
98
+ export async function checkSemanticCache(text) {
99
+ if (!CONFIG.SEMCACHE)
100
+ return null;
101
+ initCacheDb();
102
+ let currentEmbedding;
103
+ try {
104
+ currentEmbedding = await embedText(text);
105
+ if (!currentEmbedding)
106
+ return null;
107
+ }
108
+ catch (e) {
109
+ console.error(`[cache] Failed to generate embedding: ${e}`);
110
+ return null;
111
+ }
112
+ const currentHashes = await getReferencedFilesHashes(text);
113
+ const db = getDb();
114
+ const rows = db.prepare('SELECT id, embedding_blob, response, file_hashes FROM semcache').all();
115
+ for (const row of rows) {
116
+ const storedHashes = JSON.parse(row.file_hashes);
117
+ // Validate that the context files haven't changed (stale-context guard)
118
+ const keys1 = Object.keys(currentHashes);
119
+ const keys2 = Object.keys(storedHashes);
120
+ let match = keys1.length === keys2.length;
121
+ if (match) {
122
+ for (const k of keys1) {
123
+ if (currentHashes[k] !== storedHashes[k]) {
124
+ match = false;
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ // Skip this cache entry if file contexts differ
130
+ if (!match)
131
+ continue;
132
+ // Convert the stored SQLite BLOB back into a Float64Array for math operations
133
+ const buf = row.embedding_blob;
134
+ const storedEmbedding = new Float64Array(buf.buffer, buf.byteOffset, buf.byteLength / 8);
135
+ const sim = cosineSimilarity(currentEmbedding, Array.from(storedEmbedding));
136
+ if (sim >= CONFIG.SEMCACHE_THRESHOLD) {
137
+ console.error(`[cache] HIT! Similarity: ${sim.toFixed(4)}`);
138
+ return JSON.parse(row.response);
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * Stores a new entry in the semantic cache.
145
+ *
146
+ * Generates the vector embedding for the prompt, hashes its referenced files,
147
+ * and stores the original response payload in the SQLite ledger.
148
+ *
149
+ * @param text The original prompt or task text.
150
+ * @param responseObj The response payload or string to store.
151
+ */
152
+ export async function setSemanticCache(text, responseObj) {
153
+ if (!CONFIG.SEMCACHE)
154
+ return;
155
+ initCacheDb();
156
+ let embedding;
157
+ try {
158
+ embedding = await embedText(text);
159
+ if (!embedding)
160
+ return;
161
+ }
162
+ catch (e) {
163
+ console.error(`[cache] Failed to generate embedding for storage: ${e}`);
164
+ return;
165
+ }
166
+ // Convert the array of floats into a binary buffer for SQLite BLOB storage
167
+ const blob = Buffer.from(new Float64Array(embedding).buffer);
168
+ const currentHashes = await getReferencedFilesHashes(text);
169
+ const db = getDb();
170
+ const stmt = db.prepare('INSERT INTO semcache (id, embedding_blob, response, file_hashes, ts) VALUES (?, ?, ?, ?, ?)');
171
+ // Store the request embedding alongside the serialized response and the file context state
172
+ stmt.run(crypto.randomUUID(), blob, JSON.stringify(responseObj), JSON.stringify(currentHashes), new Date().toISOString());
173
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point for the `slm-gate` CLI.
4
+ *
5
+ * Why it is written this way:
6
+ * 1. Dual Execution Environments: We need to support running via `tsx src/cli.ts` in development,
7
+ * and `node dist/cli.js` when installed globally or built for production.
8
+ * 2. Process Spawning: Instead of heavily coupling the CLI directly to the module logic (which might
9
+ * carry complex dependencies or require TS execution), this script acts purely as an orchestrator.
10
+ * It determines whether to use Node or tsx and spawns child processes for the actual commands.
11
+ * 3. Environment Overrides: It allows injecting temporary environment variables (like RAM presets)
12
+ * into the spawned processes without permanently mutating the parent process environment.
13
+ */
14
+ import { spawn } from 'node:child_process';
15
+ import path from 'node:path';
16
+ import fs from 'node:fs';
17
+ import os from 'node:os';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { isPackageInstall, resolveHomeDir } from './home-dir.js';
20
+ const filename = fileURLToPath(import.meta.url);
21
+ const dirname = path.dirname(filename);
22
+ // ROOT_DIR ensures that no matter where the command is executed from, it executes relative to the project root.
23
+ const ROOT_DIR = path.resolve(dirname, '..');
24
+ /**
25
+ * Spawns a child process for a given command.
26
+ *
27
+ * @param command - The executable to run (e.g., 'node' or 'tsx')
28
+ * @param args - Arguments to pass to the executable
29
+ * @param env - Additional environment variables to overlay on top of process.env
30
+ * @returns The spawned ChildProcess instance
31
+ */
32
+ function runCommand(command, args, env = {}) {
33
+ const child = spawn(command, args, {
34
+ stdio: 'inherit', // Connects child's stdout/stderr directly to the terminal
35
+ cwd: ROOT_DIR,
36
+ env: { ...process.env, ...env }
37
+ });
38
+ child.on('error', (err) => {
39
+ console.error(`Failed to start process: ${err.message}`);
40
+ process.exit(1);
41
+ });
42
+ child.on('exit', (code) => {
43
+ if (code !== 0) {
44
+ process.exit(code || 1);
45
+ }
46
+ });
47
+ return child;
48
+ }
49
+ // Determine if we are running the compiled JavaScript (from dist/) or TypeScript (from src/)
50
+ const isCompiled = filename.endsWith('.js');
51
+ const tsxPath = path.join(ROOT_DIR, 'node_modules', '.bin', 'tsx');
52
+ /**
53
+ * Resolves the correct execution path and command for a given source script.
54
+ *
55
+ * @param srcPath - The relative path to the TypeScript source file (e.g., 'src/config.ts')
56
+ * @returns An object containing the executable ('node' or 'tsx') and the absolute path to the target file.
57
+ */
58
+ function getRunPath(srcPath) {
59
+ // If we're executing the compiled CLI (dist/cli.js), we map 'src/*.ts' to 'dist/*.js' and use 'node'
60
+ if (isCompiled && srcPath.startsWith('src/')) {
61
+ const distPath = srcPath.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
62
+ return { command: 'node', args: [path.join(ROOT_DIR, distPath)] };
63
+ }
64
+ // If running in development, we use 'tsx' to execute the TypeScript files directly
65
+ return { command: tsxPath, args: [path.join(ROOT_DIR, srcPath)] };
66
+ }
67
+ /** `bench` and `metrics` run the evaluation harness with tsx; only a git checkout has either. */
68
+ function requireCheckout(params) {
69
+ if (fs.existsSync(path.join(ROOT_DIR, params.script)) && fs.existsSync(tsxPath))
70
+ return;
71
+ console.error(`\`slm-gate ${params.command}\` needs a git checkout of slm-gate (https://github.com/zenithfoundry/slm-gate): it runs the evaluation harness, which an npm install does not include.`);
72
+ process.exit(1);
73
+ }
74
+ async function main() {
75
+ const args = process.argv.slice(2);
76
+ const command = args[0];
77
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
78
+ console.log(`
79
+ Usage: slm-gate <command> [options]
80
+
81
+ Commands:
82
+ init Create your settings file (.env), set up for this computer's RAM. Never replaces one
83
+ that exists
84
+ Options:
85
+ --ram <GB> (set up for a computer with this much RAM instead)
86
+ mcp Run the MCP server for a coding tool. This is what you register in the tool:
87
+ command \`slm-gate\`, argument \`mcp\`. It starts the model gate by itself
88
+ serve Run the model gate in this terminal (normally not needed: it starts by itself
89
+ whenever a coding tool starts slm-gate's MCP server)
90
+ Options:
91
+ --layer llm|mcp|both (default: llm; your coding tools start the MCP server)
92
+ --transport stdio|http (MCP server transport, default: stdio)
93
+ --preset <preset> (override RAM preset, e.g., ram-24)
94
+ start Start the model gate in the background (if it is not running)
95
+ stop Stop the model gate; it stays stopped until start, restart or a reboot
96
+ restart Stop and start the model gate (e.g. after updating slm-gate)
97
+ bench Run the offline evaluation harness
98
+ Options:
99
+ --n <number> (number of tasks to run)
100
+ metrics Show performance and cost metrics from the local ledger
101
+ ledger:sync Sync local SQLite ledger traces and scores to Langfuse
102
+ Options:
103
+ --all (sync all historical events)
104
+ --limit <number> (limit number of events to sync)
105
+ --dry-run (simulate without sending network requests)
106
+ ledger:reset Nuke local SQLite database and start fresh
107
+ setup-dashboard Setup Langfuse dashboard and widgets
108
+ config Print the current resolved configuration
109
+ models:check Check if required models are pulled and fit in RAM
110
+ doctor Run preflight readiness checks
111
+ `);
112
+ process.exit(0);
113
+ }
114
+ if (command === 'init') {
115
+ const ramAt = args.indexOf('--ram');
116
+ const ramGb = ramAt >= 0 ? Number(args[ramAt + 1]) : undefined;
117
+ if (ramGb !== undefined && !(Number.isInteger(ramGb) && ramGb > 0)) {
118
+ console.error('--ram takes a whole number of gigabytes, e.g. --ram 64');
119
+ process.exit(1);
120
+ }
121
+ const { initSettings } = await import('./setup/init.js');
122
+ const { detectHardware } = await import('./hardware.js');
123
+ const setUpFor = ramGb ?? detectHardware().totalRamGB;
124
+ let result;
125
+ try {
126
+ result = initSettings({
127
+ installDir: ROOT_DIR,
128
+ homeDir: resolveHomeDir({ installDir: ROOT_DIR, env: process.env, userHome: os.homedir() }),
129
+ ramGb: setUpFor,
130
+ });
131
+ }
132
+ catch (err) {
133
+ console.error(err instanceof Error ? err.message : String(err));
134
+ process.exit(1);
135
+ }
136
+ const self = isPackageInstall(ROOT_DIR) ? 'slm-gate' : `node ${path.join(ROOT_DIR, 'dist', 'cli.js')}`;
137
+ if (!result.created) {
138
+ console.log(`Your settings file already exists, so nothing was changed: ${result.envPath}`);
139
+ console.log(`Edit it directly, or delete it and run \`${self} init\` again.`);
140
+ return;
141
+ }
142
+ console.log(`Created your settings file: ${result.envPath}`);
143
+ console.log(`It is set up for ${setUpFor} GB of RAM (RAM_PRESET=${result.preset}); every other setting is explained in the file.`);
144
+ if (result.preset === 'custom') {
145
+ console.log('That is more than 128 GB, so it starts from the 128 GB models; you can choose bigger ones (step 4).');
146
+ }
147
+ console.log('\nNext:');
148
+ console.log(` 1. Download the local models: ${result.models.map(model => `ollama pull ${model}`).join(' && ')}`);
149
+ console.log(` 2. Check everything: ${self} doctor`);
150
+ console.log(` 3. Connect your coding tool: add an MCP server that runs \`${self} mcp\`.`);
151
+ console.log(` \`${self} doctor\` also prints the address for tools that can send their AI requests through slm-gate.`);
152
+ console.log(' 4. To pick models that fit this computer best, use llmfit: https://github.com/AlexsJones/llmfit');
153
+ console.log(' Then set SLM_BRAIN_MODEL and SLM_GATE_MODEL in the settings file.');
154
+ return;
155
+ }
156
+ if (command === 'mcp') {
157
+ // In this process, not a child: the coding tool talks to it over this process's stdin/stdout and
158
+ // stops it by stopping this process. The server sends all logging to stderr and starts on import.
159
+ await import('./mcp-gate/index.js');
160
+ return;
161
+ }
162
+ if (command === 'serve') {
163
+ // The MCP server is started by the coding tools themselves; by hand you usually want the model gate.
164
+ let layer = 'llm';
165
+ let transport = 'stdio';
166
+ let preset = '';
167
+ for (let i = 1; i < args.length; i++) {
168
+ if (args[i] === '--layer')
169
+ layer = args[++i];
170
+ else if (args[i] === '--transport')
171
+ transport = args[++i];
172
+ else if (args[i] === '--preset')
173
+ preset = args[++i];
174
+ }
175
+ if (!layer || !['mcp', 'llm', 'both'].includes(layer)) {
176
+ console.error('Error: --layer must be mcp, llm, or both');
177
+ process.exit(1);
178
+ }
179
+ const envOverride = {};
180
+ if (preset)
181
+ envOverride['RAM_PRESET'] = preset;
182
+ if (transport)
183
+ envOverride['MCP_GATE_TRANSPORT'] = transport;
184
+ const layersToStart = [];
185
+ if (layer === 'mcp' || layer === 'both')
186
+ layersToStart.push('src/mcp-gate/index.ts');
187
+ if (layer === 'llm' || layer === 'both')
188
+ layersToStart.push('src/llm-gate/index.ts');
189
+ for (const src of layersToStart) {
190
+ const { command: cmd, args: cmdArgs } = getRunPath(src);
191
+ runCommand(cmd, cmdArgs, envOverride);
192
+ }
193
+ }
194
+ else if (command === 'bench') {
195
+ requireCheckout({ command: 'bench', script: 'harness/run.ts' });
196
+ // Pass all args after bench to the run script
197
+ const benchArgs = args.slice(1);
198
+ runCommand(tsxPath, [path.join(ROOT_DIR, 'harness/run.ts'), ...benchArgs]);
199
+ }
200
+ else if (command === 'metrics') {
201
+ requireCheckout({ command: 'metrics', script: 'harness/metrics.ts' });
202
+ runCommand(tsxPath, [path.join(ROOT_DIR, 'harness/metrics.ts')]);
203
+ }
204
+ else if (command === 'ledger:sync' || command === 'sync') {
205
+ const syncArgs = args.slice(1);
206
+ const { command: cmd, args: cmdArgs } = getRunPath('src/ledger/sync.ts');
207
+ runCommand(cmd, [...cmdArgs, ...syncArgs]);
208
+ }
209
+ else if (command === 'setup-dashboard') {
210
+ const { command: cmd, args: cmdArgs } = getRunPath('src/ledger/setup-dashboard.ts');
211
+ runCommand(cmd, cmdArgs);
212
+ }
213
+ else if (command === 'ledger:reset') {
214
+ const outputDir = path.join(resolveHomeDir({ installDir: ROOT_DIR, env: process.env, userHome: os.homedir() }), 'output');
215
+ const filesToNuke = [
216
+ path.join(outputDir, 'ledger.sqlite'),
217
+ path.join(outputDir, 'ledger.sqlite-wal'),
218
+ path.join(outputDir, 'ledger.sqlite-shm'),
219
+ path.join(outputDir, 'deferral_curve.svg'),
220
+ path.join(outputDir, 'leaderboard.md')
221
+ ];
222
+ for (const f of filesToNuke) {
223
+ if (fs.existsSync(f)) {
224
+ fs.unlinkSync(f);
225
+ }
226
+ }
227
+ console.log('Local SQLite ledger and benchmark outputs nuked successfully.');
228
+ process.exit(0);
229
+ }
230
+ else if (command === 'config') {
231
+ const { command: cmd, args: cmdArgs } = getRunPath('src/config.ts');
232
+ // For config, if using node, we need to make sure we don't just import it but run it.
233
+ // The config.ts has a block that checks if it's the main module.
234
+ runCommand(cmd, cmdArgs);
235
+ }
236
+ else if (command === 'models:check') {
237
+ const { command: cmd, args: cmdArgs } = getRunPath('src/models/check.ts');
238
+ runCommand(cmd, cmdArgs);
239
+ }
240
+ else if (command === 'doctor') {
241
+ const { command: cmd, args: cmdArgs } = getRunPath('src/doctor.ts');
242
+ runCommand(cmd, cmdArgs);
243
+ }
244
+ else if (command === 'start' || command === 'stop' || command === 'restart') {
245
+ const { command: cmd, args: cmdArgs } = getRunPath('src/setup/gate-command.ts');
246
+ runCommand(cmd, [...cmdArgs, command]);
247
+ }
248
+ else {
249
+ console.error(`Unknown command: ${command}`);
250
+ process.exit(1);
251
+ }
252
+ }
253
+ main().catch(err => {
254
+ console.error(err);
255
+ process.exit(1);
256
+ });