lynkr 9.7.0 → 9.7.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lynkr
2
2
 
3
- ### The AI coding proxy that compresses tokens before they hit the model.
3
+ ### An LLM Gateway which optimises your token usage.
4
4
 
5
5
  **87.6% fewer tokens on JSON tool results. 53% fewer tokens on tool-heavy requests. 171ms semantic cache hits. Zero code changes.**
6
6
 
@@ -31,20 +31,11 @@
31
31
  ```bash
32
32
  npm install -g lynkr
33
33
 
34
+
34
35
  # Claude Code Pro/Max
35
36
  lynkr wrap claude
36
37
 
37
- # GitHub Copilot
38
- lynkr wrap copilot
39
-
40
- # Aider
41
- lynkr wrap aider
42
38
 
43
- # Cursor
44
- lynkr wrap cursor
45
-
46
- # OpenAI Codex
47
- lynkr wrap codex
48
39
  ```
49
40
 
50
41
  **Wrapping gives you:**
@@ -68,96 +59,31 @@ npm install -g lynkr
68
59
 
69
60
  ### 2. Configure Lynkr
70
61
 
71
- First run creates a `.env` file. Edit it with your provider settings.
72
-
73
- **Option A: Free & Local (Ollama) - Recommended for Testing**
62
+ The fastest path is the interactive wizard:
74
63
 
75
64
  ```bash
76
- # Install Ollama first: https://ollama.com
77
- ollama pull qwen2.5-coder:latest
65
+ lynkr init
78
66
  ```
79
67
 
80
- Create/edit `.env` in your project directory:
81
- ```bash
82
- # Provider
83
- MODEL_PROVIDER=ollama
84
- FALLBACK_ENABLED=false
85
-
86
- # Ollama Configuration
87
- OLLAMA_ENDPOINT=http://localhost:11434
88
- OLLAMA_MODEL=qwen2.5-coder:latest
89
-
90
- # Server
91
- PORT=8081
92
-
93
- # Optional: Limits (remove for unlimited)
94
- POLICY_MAX_STEPS=50
95
- POLICY_MAX_TOOL_CALLS=100
96
-
97
- # Disable overly strict command filtering
98
- POLICY_SAFE_COMMANDS_ENABLED=false
99
- ```
68
+ It asks four questions — usage mode (Claude Pro/Max via wrap, or direct API keys), tier picks for SIMPLE/MEDIUM/COMPLEX/REASONING across the 12 supported providers, credentials for what you chose, and a few routing-intelligence knobs — then writes a fully-populated `.env` with sensible production defaults for everything else (caching, compression, policy budgets, MCP sandbox, agents, rate limiting).
100
69
 
101
- **Option B: Cloud (OpenRouter) - Recommended for Production**
70
+ Useful flags:
102
71
 
103
72
  ```bash
104
- # Get API key from https://openrouter.ai
73
+ lynkr init --force # overwrite an existing .env
105
74
  ```
106
75
 
107
- Create/edit `.env`:
108
- ```bash
109
- # Provider
110
- MODEL_PROVIDER=openrouter
111
- OPENROUTER_API_KEY=sk-or-v1-your-key-here
112
- FALLBACK_ENABLED=false
113
-
114
- # Server
115
- PORT=8081
76
+ See [`docs/init.md`](docs/init.md) for the full wizard reference.
116
77
 
117
- # Optional: Limits (remove for unlimited)
118
- POLICY_MAX_STEPS=50
119
- POLICY_MAX_TOOL_CALLS=100
78
+ If you'd rather configure by hand, the manual options below still work — copy `.env.example` to `.env` and edit it directly:
120
79
 
121
- # Optional: Enable caching
122
- PROMPT_CACHE_ENABLED=true
123
- SEMANTIC_CACHE_ENABLED=true
124
- ```
125
-
126
- **Option C: Enterprise (AWS Bedrock)**
80
+ **Option A: Free & Local (Ollama) - Recommended for Testing**
127
81
 
128
- Create/edit `.env`:
129
82
  ```bash
130
- # Provider
131
- MODEL_PROVIDER=bedrock
132
- AWS_BEDROCK_API_KEY=your-aws-key
133
- AWS_BEDROCK_MODEL_ID=anthropic.claude-3-5-sonnet-20241022-v2:0
134
- FALLBACK_ENABLED=false
135
-
136
- # Server
137
- PORT=8081
138
-
139
- # Optional: Limits (remove for unlimited)
140
- POLICY_MAX_STEPS=50
141
- POLICY_MAX_TOOL_CALLS=100
83
+ # Install Ollama first: https://ollama.com
84
+ ollama pull qwen2.5-coder:latest
142
85
  ```
143
86
 
144
- **Option D: Enterprise (Databricks)**
145
-
146
- Create/edit `.env`:
147
- ```bash
148
- # Provider
149
- MODEL_PROVIDER=databricks
150
- DATABRICKS_API_BASE=https://your-workspace.cloud.databricks.com
151
- DATABRICKS_API_KEY=your-token
152
- FALLBACK_ENABLED=false
153
-
154
- # Server
155
- PORT=8081
156
-
157
- # Optional: Limits (remove for unlimited)
158
- POLICY_MAX_STEPS=50
159
- POLICY_MAX_TOOL_CALLS=100
160
- ```
161
87
 
162
88
  Then start Lynkr:
163
89
 
@@ -167,21 +93,6 @@ lynkr start
167
93
 
168
94
  ### 3. Connect Your Tool
169
95
 
170
- **Claude Code**
171
-
172
- **Windows (Command Prompt):**
173
- ```cmd
174
- set ANTHROPIC_BASE_URL=http://localhost:8081
175
- set ANTHROPIC_API_KEY=dummy
176
- claude "write a hello world in python"
177
- ```
178
-
179
- **Linux/macOS:**
180
- ```bash
181
- export ANTHROPIC_BASE_URL=http://localhost:8081
182
- export ANTHROPIC_API_KEY=dummy
183
- claude "write a hello world in python"
184
- ```
185
96
 
186
97
  **Cursor IDE**
187
98
  - Settings → Models → Override Base URL
@@ -338,223 +249,6 @@ Tier configuration is strictly authoritative — bandit exploration is constrain
338
249
 
339
250
  ---
340
251
 
341
- ## Complete .env Examples
342
-
343
- ### MVP: Minimal Working Setup (Ollama)
344
-
345
- Copy-paste ready configuration for immediate use:
346
-
347
- ```bash
348
- # .env - Minimal Ollama Setup
349
-
350
- # ============================================
351
- # REQUIRED: Provider Configuration
352
- # ============================================
353
- MODEL_PROVIDER=ollama
354
- FALLBACK_ENABLED=false
355
-
356
- # ============================================
357
- # REQUIRED: Ollama Settings
358
- # ============================================
359
- OLLAMA_ENDPOINT=http://localhost:11434
360
- OLLAMA_MODEL=qwen2.5-coder:latest
361
-
362
- # ============================================
363
- # REQUIRED: Server Configuration
364
- # ============================================
365
- PORT=8081
366
- HOST=0.0.0.0
367
-
368
- # ============================================
369
- # REQUIRED: Claude Code/Cursor Compatibility
370
- # ============================================
371
- POLICY_MAX_STEPS=50
372
- POLICY_MAX_TOOL_CALLS=100
373
- POLICY_SAFE_COMMANDS_ENABLED=false
374
-
375
- # ============================================
376
- # OPTIONAL: Performance (Recommended)
377
- # ============================================
378
- LOG_LEVEL=warn
379
- LOAD_SHEDDING_ENABLED=true
380
- LOAD_SHEDDING_HEAP_THRESHOLD=0.85
381
- ```
382
-
383
- **Steps:**
384
- 1. Install Ollama: `curl -fsSL https://ollama.com/install.sh | sh`
385
- 2. Pull model: `ollama pull qwen2.5-coder:latest`
386
- 3. Copy above to `.env` in your project directory
387
- 4. Run: `lynkr start`
388
-
389
- ---
390
-
391
- ### Production: Cloud with Tier Routing (OpenRouter)
392
-
393
- Optimized for cost savings with smart routing:
394
-
395
- ```bash
396
- # .env - Production OpenRouter Setup
397
-
398
- # ============================================
399
- # REQUIRED: Provider Configuration
400
- # ============================================
401
- MODEL_PROVIDER=openrouter
402
- OPENROUTER_API_KEY=sk-or-v1-your-key-here
403
- FALLBACK_ENABLED=false
404
-
405
- # ============================================
406
- # REQUIRED: Server Configuration
407
- # ============================================
408
- PORT=8081
409
- HOST=0.0.0.0
410
-
411
- # ============================================
412
- # TIER ROUTING: Smart Cost Optimization
413
- # ============================================
414
- # Simple queries → Cheap/fast model
415
- TIER_SIMPLE=openrouter:google/gemini-flash-1.5
416
-
417
- # Normal coding → Balanced model
418
- TIER_MEDIUM=openrouter:anthropic/claude-3.5-sonnet
419
-
420
- # Complex refactoring → Powerful model
421
- TIER_COMPLEX=openrouter:anthropic/claude-opus-4
422
-
423
- # Deep reasoning → Most capable model
424
- TIER_REASONING=openrouter:anthropic/claude-opus-4
425
-
426
- # ============================================
427
- # REQUIRED: Claude Code/Cursor Compatibility
428
- # ============================================
429
- POLICY_MAX_STEPS=50
430
- POLICY_MAX_TOOL_CALLS=100
431
- POLICY_SAFE_COMMANDS_ENABLED=false
432
-
433
- # ============================================
434
- # OPTIONAL: Token Optimization (60-80% savings)
435
- # ============================================
436
- PROMPT_CACHE_ENABLED=true
437
- SEMANTIC_CACHE_ENABLED=true
438
- SEMANTIC_CACHE_THRESHOLD=0.95
439
- TOOL_INJECTION_ENABLED=false
440
-
441
- # ============================================
442
- # OPTIONAL: Performance Tuning
443
- # ============================================
444
- LOG_LEVEL=warn
445
- LOAD_SHEDDING_ENABLED=true
446
- LOAD_SHEDDING_HEAP_THRESHOLD=0.85
447
- ```
448
-
449
- **Expected savings:** 70-90% of requests use Gemini Flash ($). Only 10-30% use Claude Opus ($$$).
450
-
451
- ---
452
-
453
- ### Enterprise: Databricks Foundation Models
454
-
455
- For teams using Databricks Model Serving:
456
-
457
- ```bash
458
- # .env - Enterprise Databricks Setup
459
-
460
- # ============================================
461
- # REQUIRED: Provider Configuration
462
- # ============================================
463
- MODEL_PROVIDER=databricks
464
- DATABRICKS_API_BASE=https://your-workspace.cloud.databricks.com
465
- DATABRICKS_API_KEY=dapi1234567890abcdef
466
- FALLBACK_ENABLED=false
467
-
468
- # ============================================
469
- # REQUIRED: Model Configuration
470
- # ============================================
471
- # Option 1: Single model (no tier routing)
472
- DATABRICKS_MODEL=databricks-meta-llama-3-1-405b-instruct
473
-
474
- # Option 2: Tier routing (comment out above, uncomment below)
475
- # TIER_SIMPLE=databricks:databricks-meta-llama-3-1-70b-instruct
476
- # TIER_MEDIUM=databricks:databricks-claude-sonnet-4-5
477
- # TIER_COMPLEX=databricks:databricks-claude-opus-4-6
478
- # TIER_REASONING=databricks:databricks-claude-opus-4-6
479
-
480
- # ============================================
481
- # REQUIRED: Server Configuration
482
- # ============================================
483
- PORT=8081
484
- HOST=0.0.0.0
485
-
486
- # ============================================
487
- # REQUIRED: Claude Code/Cursor Compatibility
488
- # ============================================
489
- POLICY_MAX_STEPS=50
490
- POLICY_MAX_TOOL_CALLS=100
491
- POLICY_SAFE_COMMANDS_ENABLED=false
492
-
493
- # ============================================
494
- # OPTIONAL: Enterprise Features
495
- # ============================================
496
- LOG_LEVEL=info
497
- LOAD_SHEDDING_ENABLED=true
498
- LOAD_SHEDDING_HEAP_THRESHOLD=0.85
499
-
500
- # Optional: Metrics for monitoring
501
- # PROMETHEUS_METRICS_ENABLED=true
502
- ```
503
-
504
- ---
505
-
506
- ### Hybrid: Local + Cloud Fallback
507
-
508
- Use free Ollama, fallback to cloud when needed:
509
-
510
- ```bash
511
- # .env - Hybrid Setup (Advanced)
512
-
513
- # ============================================
514
- # PRIMARY: Local Ollama
515
- # ============================================
516
- MODEL_PROVIDER=ollama
517
- OLLAMA_ENDPOINT=http://localhost:11434
518
- OLLAMA_MODEL=qwen2.5-coder:latest
519
-
520
- # ============================================
521
- # FALLBACK: Cloud Provider
522
- # ============================================
523
- FALLBACK_ENABLED=true
524
- FALLBACK_PROVIDER=openrouter
525
- OPENROUTER_API_KEY=sk-or-v1-your-key-here
526
-
527
- # ============================================
528
- # TIER ROUTING: Mix Local + Cloud
529
- # ============================================
530
- TIER_SIMPLE=ollama:qwen2.5:3b
531
- TIER_MEDIUM=ollama:qwen2.5:7b
532
- TIER_COMPLEX=openrouter:anthropic/claude-3.5-sonnet
533
- TIER_REASONING=openrouter:anthropic/claude-opus-4
534
-
535
- # ============================================
536
- # REQUIRED: Server Configuration
537
- # ============================================
538
- PORT=8081
539
- HOST=0.0.0.0
540
-
541
- # ============================================
542
- # REQUIRED: Claude Code/Cursor Compatibility
543
- # ============================================
544
- POLICY_MAX_STEPS=50
545
- POLICY_MAX_TOOL_CALLS=100
546
- POLICY_SAFE_COMMANDS_ENABLED=false
547
-
548
- # ============================================
549
- # OPTIONAL: Performance
550
- # ============================================
551
- LOG_LEVEL=warn
552
- LOAD_SHEDDING_ENABLED=true
553
- ```
554
-
555
- **Best of both worlds:** 80% of requests stay local (free). Complex tasks use cloud (paid).
556
-
557
- ---
558
252
 
559
253
  ## Common Issues & Fixes
560
254
 
package/bin/cli.js CHANGED
@@ -9,11 +9,15 @@ const SUBCOMMANDS = {
9
9
  usage: path.join(__dirname, "lynkr-usage.js"),
10
10
  trajectory: path.join(__dirname, "lynkr-trajectory.js"),
11
11
  wrap: path.join(__dirname, "wrap.js"),
12
+ init: path.join(__dirname, "lynkr-init.js"),
12
13
  };
13
14
 
14
15
  const sub = process.argv[2];
15
16
  if (sub && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, sub)) {
16
17
  process.argv.splice(2, 1); // drop the subcommand token so the script's own arg parser is happy
18
+ // Subcommand scripts check this to decide whether to invoke their main()
19
+ // when they're require()'d (vs being loaded by a test for unit-checking).
20
+ process.env._LYNKR_SUBCMD = sub;
17
21
  require(SUBCOMMANDS[sub]);
18
22
  return;
19
23
  }
@@ -0,0 +1,685 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `lynkr init` — interactive setup wizard that produces a working .env.
4
+ *
5
+ * Walks the user through:
6
+ * 1. Usage mode (Claude Pro/Max subscription via wrap, or API-key direct).
7
+ * 2. Per-tier model selection across all supported providers.
8
+ * 3. Routing-intelligence knobs (visible badge, intent window, decay).
9
+ * 4. Credential collection (re-uses values across tiers, never asks twice).
10
+ *
11
+ * Usage:
12
+ * lynkr init # interactive
13
+ * lynkr init --force # overwrite existing .env
14
+ * lynkr init --output=<path> # write to <path> instead of .env
15
+ * lynkr init --dry-run # print to stdout, don't write
16
+ * lynkr init --help
17
+ *
18
+ * @module bin/lynkr-init
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const readline = require('readline');
24
+
25
+ // ──────────────────────────────────────────────────────────────────────────────
26
+ // Provider schema
27
+ // ──────────────────────────────────────────────────────────────────────────────
28
+ //
29
+ // Canonical list pulled from src/config/index.js SUPPORTED_MODEL_PROVIDERS.
30
+ // Each entry lists the credential env vars the user needs to supply and any
31
+ // model/endpoint extras with sensible defaults. Local providers (no creds) are
32
+ // flagged so the wizard skips the credential prompt.
33
+
34
+ const PROVIDERS = {
35
+ ollama: {
36
+ label: 'Ollama (local, free)',
37
+ local: true,
38
+ creds: [],
39
+ extras: [
40
+ { key: 'OLLAMA_ENDPOINT', label: 'endpoint', default: 'http://localhost:11434' },
41
+ ],
42
+ defaultModel: 'qwen2.5-coder:latest',
43
+ },
44
+ llamacpp: {
45
+ label: 'llama.cpp (local)',
46
+ local: true,
47
+ creds: [],
48
+ extras: [
49
+ { key: 'LLAMACPP_ENDPOINT', label: 'endpoint', default: 'http://localhost:8080' },
50
+ ],
51
+ defaultModel: 'qwen2.5-coder',
52
+ },
53
+ lmstudio: {
54
+ label: 'LM Studio (local)',
55
+ local: true,
56
+ creds: [],
57
+ extras: [
58
+ { key: 'LMSTUDIO_ENDPOINT', label: 'endpoint', default: 'http://localhost:1234' },
59
+ ],
60
+ defaultModel: 'qwen2.5-coder',
61
+ },
62
+ 'azure-anthropic': {
63
+ label: 'Azure Anthropic (Claude via Azure)',
64
+ local: false,
65
+ creds: [
66
+ { key: 'AZURE_ANTHROPIC_ENDPOINT', label: 'Azure Anthropic endpoint URL' },
67
+ { key: 'AZURE_ANTHROPIC_API_KEY', label: 'Azure Anthropic API key', secret: true },
68
+ ],
69
+ extras: [],
70
+ defaultModel: 'claude-sonnet-4-6',
71
+ },
72
+ 'azure-openai': {
73
+ label: 'Azure OpenAI (GPT family via Azure)',
74
+ local: false,
75
+ creds: [
76
+ { key: 'AZURE_OPENAI_ENDPOINT', label: 'Azure OpenAI endpoint URL' },
77
+ { key: 'AZURE_OPENAI_API_KEY', label: 'Azure OpenAI API key', secret: true },
78
+ { key: 'AZURE_OPENAI_DEPLOYMENT', label: 'Deployment name', default: 'gpt-5.2-chat' },
79
+ ],
80
+ extras: [],
81
+ defaultModel: 'gpt-5.2-chat',
82
+ },
83
+ openai: {
84
+ label: 'OpenAI (direct)',
85
+ local: false,
86
+ creds: [
87
+ { key: 'OPENAI_API_KEY', label: 'OpenAI API key', secret: true },
88
+ ],
89
+ extras: [],
90
+ defaultModel: 'gpt-4o',
91
+ },
92
+ openrouter: {
93
+ label: 'OpenRouter (100+ models, one key)',
94
+ local: false,
95
+ creds: [
96
+ { key: 'OPENROUTER_API_KEY', label: 'OpenRouter API key', secret: true },
97
+ ],
98
+ extras: [],
99
+ defaultModel: 'anthropic/claude-sonnet-4',
100
+ },
101
+ databricks: {
102
+ label: 'Databricks Foundation Models',
103
+ local: false,
104
+ creds: [
105
+ { key: 'DATABRICKS_API_BASE', label: 'Databricks workspace URL' },
106
+ { key: 'DATABRICKS_API_KEY', label: 'Databricks API token', secret: true },
107
+ ],
108
+ extras: [],
109
+ defaultModel: 'databricks-claude-sonnet-4',
110
+ },
111
+ bedrock: {
112
+ label: 'AWS Bedrock',
113
+ local: false,
114
+ creds: [
115
+ // Bearer token from AWS Console → Bedrock → API Keys. The Bedrock client
116
+ // (src/clients/databricks.js:1450) requires this key and does NOT fall
117
+ // back to IAM/SigV4 — common misconception worth being explicit about.
118
+ { key: 'AWS_BEDROCK_API_KEY', label: 'AWS Bedrock API key (Bearer token from Bedrock console)', secret: true },
119
+ ],
120
+ extras: [
121
+ { key: 'AWS_BEDROCK_REGION', label: 'AWS region', default: 'us-east-1' },
122
+ { key: 'AWS_BEDROCK_MODEL_ID', label: 'Default model ID', default: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' },
123
+ ],
124
+ defaultModel: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
125
+ },
126
+ vertex: {
127
+ label: 'Google Vertex AI',
128
+ local: false,
129
+ creds: [
130
+ { key: 'VERTEX_API_KEY', label: 'Vertex API key (or use ADC)', secret: true },
131
+ ],
132
+ extras: [],
133
+ defaultModel: 'gemini-2.0-flash',
134
+ },
135
+ zai: {
136
+ label: 'Z.ai (GLM family)',
137
+ local: false,
138
+ creds: [
139
+ { key: 'ZAI_API_KEY', label: 'Z.ai API key', secret: true },
140
+ ],
141
+ extras: [],
142
+ defaultModel: 'GLM-4.7',
143
+ },
144
+ moonshot: {
145
+ label: 'Moonshot (Kimi family)',
146
+ local: false,
147
+ creds: [
148
+ { key: 'MOONSHOT_API_KEY', label: 'Moonshot API key', secret: true },
149
+ ],
150
+ extras: [],
151
+ defaultModel: 'kimi-k2-turbo-preview',
152
+ },
153
+ };
154
+
155
+ const PROVIDER_ORDER = [
156
+ 'ollama', 'llamacpp', 'lmstudio',
157
+ 'azure-anthropic', 'azure-openai', 'openai', 'openrouter',
158
+ 'databricks', 'bedrock', 'vertex', 'zai', 'moonshot',
159
+ ];
160
+ const TIERS = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
161
+
162
+ // Always-emitted baseline. Mirrors the production-grade config the maintainer
163
+ // runs locally: caching/compression on, generous policy budgets, sandboxed
164
+ // agents, MCP/web defaults, etc. Users can edit any of these post-generation;
165
+ // the wizard prompts only for tier picks, credentials, and a handful of
166
+ // intelligence knobs. Everything else is opinionated default.
167
+ //
168
+ // Categorised inline so future contributors know which group a key lives in.
169
+ const BASELINE_ENV = {
170
+ // ── Databricks placeholders (satisfy startup validator) ───────────────
171
+ DATABRICKS_API_BASE: 'http://localhost:8081',
172
+ DATABRICKS_API_KEY: 'tier-routing-active',
173
+ DATABRICKS_ENDPOINT_PATH: '/unused',
174
+
175
+ // ── Server ────────────────────────────────────────────────────────────
176
+ PORT: '8081',
177
+ NODE_ENV: 'production',
178
+ REQUEST_JSON_LIMIT: '1gb',
179
+ SESSION_DB_PATH: './data/sessions.db',
180
+ ENABLE_TOOL_SEARCH: 'true',
181
+ LOG_LEVEL: 'silent',
182
+
183
+ // ── Routing intelligence (tuned defaults) ─────────────────────────────
184
+ LYNKR_PREFLIGHT_ENABLED: 'false',
185
+ LYNKR_PREFLIGHT_TIMEOUT_MS: '120000',
186
+ LYNKR_CASCADE_ENABLED: 'true',
187
+ LYNKR_KNN_MIN_INDEX_SIZE: '200',
188
+ LYNKR_KNN_CONFIDENCE_HIGH: '0.55',
189
+ LYNKR_KNN_CONFIDENCE_LOW: '0.30',
190
+
191
+ // ── Tool execution ────────────────────────────────────────────────────
192
+ TOOL_EXECUTION_MODE: 'client',
193
+ TOOL_INJECTION_ENABLED: 'false',
194
+ SMART_TOOL_SELECTION_MODE: 'disabled',
195
+ SMART_TOOL_SELECTION_TOKEN_BUDGET: '2500',
196
+ CODE_MODE_ENABLED: 'true',
197
+
198
+ // ── Caching ───────────────────────────────────────────────────────────
199
+ PROMPT_CACHE_ENABLED: 'true',
200
+ PROMPT_CACHE_MAX_ENTRIES: '1000',
201
+ PROMPT_CACHE_TTL_MS: '300000',
202
+ SEMANTIC_CACHE_ENABLED: 'true',
203
+ SEMANTIC_CACHE_THRESHOLD: '0.85',
204
+ SEMANTIC_CACHE_MAX_ENTRIES: '50',
205
+ SEMANTIC_CACHE_TTL_MS: '300000',
206
+
207
+ // ── Compression: TOON + Headroom sidecar ──────────────────────────────
208
+ TOON_ENABLED: 'true',
209
+ TOON_MIN_BYTES: '4096',
210
+ TOON_FAIL_OPEN: 'true',
211
+ TOON_LOG_STATS: 'true',
212
+ HEADROOM_ENABLED: 'true',
213
+ HEADROOM_ENDPOINT: 'http://localhost:8787',
214
+ HEADROOM_TIMEOUT_MS: '5000',
215
+ HEADROOM_MIN_TOKENS: '100',
216
+ HEADROOM_MODE: 'optimize',
217
+ HEADROOM_PROVIDER: 'anthropic',
218
+ HEADROOM_DOCKER_ENABLED: 'true',
219
+ HEADROOM_DOCKER_IMAGE: 'lynkr/headroom-sidecar:latest',
220
+ HEADROOM_DOCKER_CONTAINER_NAME: 'lynkr-headroom',
221
+ HEADROOM_DOCKER_PORT: '8787',
222
+ HEADROOM_DOCKER_AUTO_BUILD: 'true',
223
+ HEADROOM_SMART_CRUSHER: 'true',
224
+ HEADROOM_SMART_CRUSHER_MIN_TOKENS: '200',
225
+ HEADROOM_SMART_CRUSHER_MAX_ITEMS: '15',
226
+ HEADROOM_TOOL_CRUSHER: 'true',
227
+ HEADROOM_CACHE_ALIGNER: 'true',
228
+ HEADROOM_ROLLING_WINDOW: 'true',
229
+ HEADROOM_KEEP_TURNS: '10',
230
+ HEADROOM_CCR: 'true',
231
+ HEADROOM_CCR_TTL: '300',
232
+
233
+ // ── Memory + token tracking ───────────────────────────────────────────
234
+ MEMORY_ENABLED: 'true',
235
+ MEMORY_RETRIEVAL_LIMIT: '5',
236
+ MEMORY_SURPRISE_THRESHOLD: '0.3',
237
+ MEMORY_MAX_AGE_DAYS: '90',
238
+ MEMORY_MAX_COUNT: '10000',
239
+ MEMORY_EXTRACTION_ENABLED: 'true',
240
+ MEMORY_DECAY_ENABLED: 'true',
241
+ MEMORY_DECAY_HALF_LIFE: '30',
242
+ MEMORY_FORMAT: 'compact',
243
+ MEMORY_DEDUP_ENABLED: 'true',
244
+ MEMORY_DEDUP_LOOKBACK: '5',
245
+ MEMORY_TTL: '3600000',
246
+ TOKEN_TRACKING_ENABLED: 'true',
247
+ TOOL_TRUNCATION_ENABLED: 'true',
248
+
249
+ // ── Prompt/output shaping ─────────────────────────────────────────────
250
+ SYSTEM_PROMPT_MODE: 'dynamic',
251
+ TOOL_DESCRIPTIONS: 'minimal',
252
+ HISTORY_COMPRESSION_ENABLED: 'true',
253
+ HISTORY_KEEP_RECENT_TURNS: '10',
254
+ HISTORY_SUMMARIZE_OLDER: 'true',
255
+ TOKEN_BUDGET_WARNING: '100000',
256
+ TOKEN_BUDGET_MAX: '180000',
257
+ TOKEN_BUDGET_ENFORCEMENT: 'true',
258
+ CAVEMAN_ENABLED: 'false',
259
+ CAVEMAN_LEVEL: 'full',
260
+ MARKDOWN_RENDER_ANSI: 'false',
261
+
262
+ // ── Policy & budgets ──────────────────────────────────────────────────
263
+ POLICY_MAX_STEPS: '2000',
264
+ POLICY_MAX_TOOL_CALLS: '2000',
265
+ POLICY_TOOL_LOOP_THRESHOLD: '100',
266
+ POLICY_GIT_ALLOW_PUSH: 'false',
267
+ POLICY_GIT_ALLOW_PULL: 'true',
268
+ POLICY_GIT_ALLOW_COMMIT: 'true',
269
+ POLICY_GIT_REQUIRE_TESTS: 'false',
270
+ POLICY_GIT_AUTOSTASH: 'false',
271
+ POLICY_FILE_BLOCKED_PATHS: '/.env,.env,/etc/passwd,/etc/shadow',
272
+ POLICY_SAFE_COMMANDS_ENABLED: 'true',
273
+
274
+ // ── Agents ────────────────────────────────────────────────────────────
275
+ AGENTS_ENABLED: 'true',
276
+ AGENTS_MAX_CONCURRENT: '10',
277
+ AGENTS_DEFAULT_MODEL: 'haiku',
278
+ AGENTS_MAX_STEPS: '15',
279
+ AGENTS_TIMEOUT: '300000',
280
+
281
+ // ── Rate limiting ─────────────────────────────────────────────────────
282
+ RATE_LIMIT_ENABLED: 'true',
283
+ RATE_LIMIT_WINDOW_MS: '60000',
284
+ RATE_LIMIT_MAX: '100',
285
+ RATE_LIMIT_KEY_BY: 'session',
286
+
287
+ // ── Hot reload + load shedding ────────────────────────────────────────
288
+ HOT_RELOAD_ENABLED: 'true',
289
+ HOT_RELOAD_DEBOUNCE_MS: '1000',
290
+ LOAD_SHEDDING_ENABLED: 'true',
291
+ LOAD_SHEDDING_HEAP_THRESHOLD: '0.85',
292
+ LOAD_SHEDDING_MEMORY_THRESHOLD: '0.95',
293
+
294
+ // ── Per-provider extras (secrets stay empty; wizard or user fills in) ─
295
+ AZURE_ANTHROPIC_ENDPOINT: 'https://api.anthropic.com/v1/messages',
296
+ AZURE_ANTHROPIC_VERSION: '2023-06-01',
297
+ AZURE_OPENAI_API_VERSION: '2024-08-01-preview',
298
+ OLLAMA_MODEL: 'minimax-m2.5:cloud',
299
+ OLLAMA_TIMEOUT_MS: '120000',
300
+ OLLAMA_EMBEDDINGS_MODEL: 'nomic-embed-text',
301
+ OLLAMA_EMBEDDINGS_ENDPOINT: 'http://localhost:11434/api/embeddings',
302
+ OPENROUTER_API_KEY: '',
303
+ OPENROUTER_MODEL: 'openai/gpt-4o-mini',
304
+ OPENROUTER_EMBEDDINGS_MODEL: 'openai/text-embedding-ada-002',
305
+ OPENROUTER_ENDPOINT: 'https://openrouter.ai/api/v1/chat/completions',
306
+ OPENROUTER_MAX_TOOLS_FOR_ROUTING: '15',
307
+ MOONSHOT_API_KEY: '',
308
+ MOONSHOT_ENDPOINT: 'https://api.moonshot.ai/v1/chat/completions',
309
+ MOONSHOT_MODEL: 'kimi-k2.6',
310
+ LLAMACPP_ENDPOINT: 'http://localhost:8080',
311
+ LLAMACPP_MODEL: 'default',
312
+ LLAMACPP_TIMEOUT_MS: '120000',
313
+ LLAMACPP_EMBEDDINGS_ENDPOINT: 'http://localhost:8080/embeddings',
314
+ LMSTUDIO_ENDPOINT: 'http://localhost:1234',
315
+ LMSTUDIO_MODEL: 'default',
316
+ LMSTUDIO_TIMEOUT_MS: '120000',
317
+
318
+ // ── MCP sandbox (Docker-isolated MCP tool execution) ──────────────────
319
+ MCP_SANDBOX_ENABLED: 'true',
320
+ MCP_SANDBOX_RUNTIME: 'docker',
321
+ MCP_SANDBOX_CONTAINER_WORKSPACE: '/workspace',
322
+ MCP_SANDBOX_MOUNT_WORKSPACE: 'true',
323
+ MCP_SANDBOX_ALLOW_NETWORKING: 'false',
324
+ MCP_SANDBOX_NETWORK_MODE: 'none',
325
+ MCP_SANDBOX_PASSTHROUGH_ENV: 'PATH,LANG,LC_ALL,TERM,HOME',
326
+ MCP_SANDBOX_TIMEOUT_MS: '20000',
327
+ MCP_SANDBOX_REUSE_SESSION: 'true',
328
+ MCP_SANDBOX_READ_ONLY_ROOT: 'false',
329
+ MCP_SANDBOX_NO_NEW_PRIVILEGES: 'true',
330
+ MCP_SANDBOX_DROP_CAPABILITIES: 'ALL',
331
+ MCP_SANDBOX_MEMORY_LIMIT: '512m',
332
+ MCP_SANDBOX_CPU_LIMIT: '1.0',
333
+ MCP_SANDBOX_PIDS_LIMIT: '100',
334
+ MCP_SANDBOX_PERMISSION_MODE: 'auto',
335
+ MCP_MANIFEST_DIRS: '~/.claude/mcp',
336
+
337
+ // ── Web tools (search + fetch) ────────────────────────────────────────
338
+ WEB_SEARCH_ENDPOINT: 'http://localhost:8888/search',
339
+ WEB_SEARCH_ALLOW_ALL: 'true',
340
+ WEB_SEARCH_TIMEOUT_MS: '10000',
341
+ WEB_FETCH_BODY_PREVIEW_MAX: '10000',
342
+ WEB_SEARCH_RETRY_ENABLED: 'true',
343
+ WEB_SEARCH_MAX_RETRIES: '2',
344
+
345
+ // ── TinyFish (web automation) ─────────────────────────────────────────
346
+ TINYFISH_API_KEY: '',
347
+ TINYFISH_ENDPOINT: 'https://agent.tinyfish.ai/v1/automation/run-sse',
348
+ TINYFISH_BROWSER_PROFILE: 'lite',
349
+ TINYFISH_TIMEOUT_MS: '120000',
350
+ TINYFISH_PROXY_ENABLED: 'false',
351
+ TINYFISH_PROXY_COUNTRY: 'US',
352
+
353
+ // ── Workspace test runner ─────────────────────────────────────────────
354
+ WORKSPACE_TEST_TIMEOUT_MS: '600000',
355
+ WORKSPACE_TEST_SANDBOX: 'auto',
356
+ WORKSPACE_TEST_COVERAGE_FILES: 'coverage/coverage-summary.json',
357
+ };
358
+
359
+ // ──────────────────────────────────────────────────────────────────────────────
360
+ // Args
361
+ // ──────────────────────────────────────────────────────────────────────────────
362
+
363
+ function parseArgs(argv) {
364
+ const opts = { force: false, dryRun: false, output: null, help: false };
365
+ for (let i = 0; i < argv.length; i++) {
366
+ const a = argv[i];
367
+ if (a === '--help' || a === '-h') opts.help = true;
368
+ else if (a === '--force' || a === '-f') opts.force = true;
369
+ else if (a === '--dry-run') opts.dryRun = true;
370
+ else if (a.startsWith('--output=')) opts.output = a.slice('--output='.length);
371
+ else if (a === '--output' || a === '-o') opts.output = argv[++i];
372
+ }
373
+ return opts;
374
+ }
375
+
376
+ function showHelp() {
377
+ console.log(`lynkr init — interactive setup wizard
378
+
379
+ Usage:
380
+ lynkr init Interactive wizard
381
+ lynkr init --force Overwrite existing .env
382
+ lynkr init --output=<path> Write to <path> instead of .env
383
+ lynkr init --dry-run Print to stdout, don't write
384
+ lynkr init --help
385
+
386
+ The wizard asks for:
387
+ 1. Usage mode (Claude Pro/Max via wrap, or direct API keys)
388
+ 2. Provider + model for each tier (SIMPLE / MEDIUM / COMPLEX / REASONING)
389
+ 3. Credentials for each picked provider (re-used across tiers)
390
+ 4. Routing intelligence (visible badge, intent window, decay)
391
+
392
+ Providers covered: ${PROVIDER_ORDER.join(', ')}.
393
+ `);
394
+ }
395
+
396
+ // ──────────────────────────────────────────────────────────────────────────────
397
+ // Prompt helpers
398
+ // ──────────────────────────────────────────────────────────────────────────────
399
+
400
+ function makeAsker() {
401
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
402
+ const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
403
+ const close = () => rl.close();
404
+ return { ask, close };
405
+ }
406
+
407
+ async function pickFromList(ask, label, choices, defaultIdx = 0) {
408
+ console.log(`\n${label}`);
409
+ choices.forEach((c, i) => {
410
+ const marker = i === defaultIdx ? '>' : ' ';
411
+ console.log(` ${marker} ${i + 1}) ${c}`);
412
+ });
413
+ const raw = await ask(`Choice [1-${choices.length}] (default ${defaultIdx + 1}): `);
414
+ if (!raw) return defaultIdx;
415
+ const n = parseInt(raw, 10);
416
+ if (Number.isNaN(n) || n < 1 || n > choices.length) {
417
+ console.log(` → invalid, using default (${choices[defaultIdx]})`);
418
+ return defaultIdx;
419
+ }
420
+ return n - 1;
421
+ }
422
+
423
+ async function askWithDefault(ask, label, defaultValue) {
424
+ const v = await ask(`${label}${defaultValue ? ` [${defaultValue}]` : ''}: `);
425
+ return v || defaultValue || '';
426
+ }
427
+
428
+ async function askYesNo(ask, label, defaultYes = true) {
429
+ const v = await ask(`${label} [${defaultYes ? 'Y/n' : 'y/N'}]: `);
430
+ if (!v) return defaultYes;
431
+ return /^y(es)?$/i.test(v);
432
+ }
433
+
434
+ // ──────────────────────────────────────────────────────────────────────────────
435
+ // Wizard
436
+ // ──────────────────────────────────────────────────────────────────────────────
437
+
438
+ async function runInteractive(opts) {
439
+ console.log('lynkr init — interactive setup\n');
440
+ const { ask, close } = makeAsker();
441
+ const env = {};
442
+ const credsCollected = {}; // dedupe per env key
443
+
444
+ try {
445
+ // ── 1. Usage mode ──
446
+ const modeIdx = await pickFromList(ask,
447
+ 'Usage mode:',
448
+ [
449
+ 'Claude Pro/Max subscription (via `lynkr wrap claude`, OAuth passthrough)',
450
+ 'Direct API usage (pay-as-you-go with API keys)',
451
+ ],
452
+ 0,
453
+ );
454
+ const isWrap = modeIdx === 0;
455
+
456
+ if (isWrap) {
457
+ env.LYNKR_OAUTH_PASSTHROUGH = 'true';
458
+ console.log('\n → OAuth passthrough enabled. COMPLEX/REASONING tiers will be sent');
459
+ console.log(' byte-for-byte to api.anthropic.com against your subscription.');
460
+ console.log(' You only need to configure a local model for SIMPLE/MEDIUM.\n');
461
+ }
462
+
463
+ // ── 2. Per-tier provider + model ──
464
+ const tierConfig = {};
465
+ const collectCreds = async (providerKey) => {
466
+ const p = PROVIDERS[providerKey];
467
+ for (const c of p.creds) {
468
+ if (credsCollected[c.key]) continue;
469
+ const existing = process.env[c.key];
470
+ const def = existing || c.default || '';
471
+ const prompt = ` ${c.label}${c.secret ? ' (hidden output not supported; paste anyway)' : ''}`;
472
+ const v = await askWithDefault(ask, prompt, def);
473
+ if (v) {
474
+ env[c.key] = v;
475
+ credsCollected[c.key] = true;
476
+ }
477
+ }
478
+ for (const ex of p.extras) {
479
+ if (env[ex.key]) continue;
480
+ const v = await askWithDefault(ask, ` ${ex.label}`, ex.default);
481
+ if (v) env[ex.key] = v;
482
+ }
483
+ };
484
+
485
+ const providerChoices = PROVIDER_ORDER.map((k) => PROVIDERS[k].label);
486
+
487
+ for (const tier of TIERS) {
488
+ const headline = isWrap && (tier === 'COMPLEX' || tier === 'REASONING')
489
+ ? `Tier ${tier} — covered by Pro/Max subscription, but you can override:`
490
+ : `Tier ${tier} — pick a provider:`;
491
+ const defaultIdx = isWrap && (tier === 'COMPLEX' || tier === 'REASONING')
492
+ ? PROVIDER_ORDER.indexOf('azure-anthropic')
493
+ : 0;
494
+
495
+ const skipOpt = isWrap && (tier === 'COMPLEX' || tier === 'REASONING')
496
+ ? [...providerChoices, 'Skip — let subscription passthrough handle it']
497
+ : providerChoices;
498
+
499
+ const idx = await pickFromList(ask, headline, skipOpt, defaultIdx);
500
+
501
+ if (idx === providerChoices.length) {
502
+ // Skip selected — leave TIER_<tier> unset
503
+ continue;
504
+ }
505
+
506
+ const providerKey = PROVIDER_ORDER[idx];
507
+ const p = PROVIDERS[providerKey];
508
+ const model = await askWithDefault(ask, ` Model for ${tier}`, p.defaultModel);
509
+ tierConfig[tier] = { provider: providerKey, model };
510
+ await collectCreds(providerKey);
511
+ }
512
+
513
+ for (const tier of TIERS) {
514
+ if (tierConfig[tier]) {
515
+ env[`TIER_${tier}`] = `${tierConfig[tier].provider}:${tierConfig[tier].model}`;
516
+ }
517
+ }
518
+
519
+ // Primary provider hint for legacy code paths
520
+ const firstTier = TIERS.map((t) => tierConfig[t]).find(Boolean);
521
+ if (firstTier) env.MODEL_PROVIDER = firstTier.provider;
522
+
523
+ // ── 3. Routing intelligence ──
524
+ console.log('\nRouting intelligence:');
525
+ if (await askYesNo(ask, 'Show the routing badge in your TUI (`*[Lynkr] …*`)?', isWrap)) {
526
+ env.LYNKR_VISIBLE_ROUTING = 'true';
527
+ }
528
+
529
+ const windowRaw = await askWithDefault(ask, 'Intent-scoring window size (1 = latest message only)', '5');
530
+ const windowN = parseInt(windowRaw, 10);
531
+ if (!Number.isNaN(windowN) && windowN >= 1) env.LYNKR_INTENT_WINDOW_N = String(windowN);
532
+
533
+ const decayRaw = await askWithDefault(ask, 'Intent-scoring per-turn decay (0.1-1.0)', '0.7');
534
+ const decay = parseFloat(decayRaw);
535
+ if (!Number.isNaN(decay) && decay > 0 && decay <= 1) env.LYNKR_INTENT_DECAY = String(decay);
536
+
537
+ close();
538
+ console.log('');
539
+ writeEnvFile(buildEnvContent(env, isWrap, tierConfig), opts);
540
+ } catch (err) {
541
+ close();
542
+ throw err;
543
+ }
544
+ }
545
+
546
+ // ──────────────────────────────────────────────────────────────────────────────
547
+ // Output
548
+ // ──────────────────────────────────────────────────────────────────────────────
549
+
550
+ function buildEnvContent(env, isWrap, tierConfig) {
551
+ // Baseline first, user choices on top — so user input always wins for keys
552
+ // they explicitly answered (e.g. LOG_LEVEL if the wizard ever asks for it).
553
+ const merged = { ...BASELINE_ENV, ...env };
554
+
555
+ const lines = [
556
+ '# Lynkr configuration',
557
+ `# Generated by 'lynkr init' at ${new Date().toISOString()}`,
558
+ `# Mode: ${isWrap ? 'wrap (Claude Pro/Max subscription)' : 'direct API'}`,
559
+ '# Edit directly to tweak; full reference in .env.example',
560
+ '',
561
+ ];
562
+
563
+ // Group output by section in the order it appears in the generated file.
564
+ // Mirrors the layout of the .env.example reference doc.
565
+ const SERVER_KEYS = new Set(['PORT', 'NODE_ENV', 'REQUEST_JSON_LIMIT', 'SESSION_DB_PATH', 'WORKSPACE_ROOT', 'ENABLE_TOOL_SEARCH']);
566
+ const TOOL_EXEC_KEYS = new Set(['TOOL_EXECUTION_MODE', 'TOOL_INJECTION_ENABLED', 'SMART_TOOL_SELECTION_MODE', 'SMART_TOOL_SELECTION_TOKEN_BUDGET', 'CODE_MODE_ENABLED']);
567
+ const CACHE_KEYS = new Set([
568
+ 'PROMPT_CACHE_ENABLED', 'PROMPT_CACHE_MAX_ENTRIES', 'PROMPT_CACHE_TTL_MS',
569
+ 'SEMANTIC_CACHE_ENABLED', 'SEMANTIC_CACHE_THRESHOLD', 'SEMANTIC_CACHE_MAX_ENTRIES', 'SEMANTIC_CACHE_TTL_MS',
570
+ ]);
571
+ const MEMORY_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('MEMORY_') || k === 'TOKEN_TRACKING_ENABLED' || k === 'TOOL_TRUNCATION_ENABLED'));
572
+ const SHAPING_KEYS = new Set([
573
+ 'SYSTEM_PROMPT_MODE', 'TOOL_DESCRIPTIONS',
574
+ 'HISTORY_COMPRESSION_ENABLED', 'HISTORY_KEEP_RECENT_TURNS', 'HISTORY_SUMMARIZE_OLDER',
575
+ 'TOKEN_BUDGET_WARNING', 'TOKEN_BUDGET_MAX', 'TOKEN_BUDGET_ENFORCEMENT',
576
+ 'CAVEMAN_ENABLED', 'CAVEMAN_LEVEL', 'MARKDOWN_RENDER_ANSI',
577
+ ]);
578
+ const POLICY_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('POLICY_')));
579
+ const AGENT_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('AGENTS_')));
580
+ const RATE_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('RATE_LIMIT_')));
581
+ const OPS_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('HOT_RELOAD_') || k.startsWith('LOAD_SHEDDING_')));
582
+ const COMPRESSION_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('TOON_') || k.startsWith('HEADROOM_')));
583
+ const MCP_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('MCP_')));
584
+ const WEB_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('WEB_SEARCH_') || k.startsWith('WEB_FETCH_')));
585
+ const TINYFISH_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('TINYFISH_')));
586
+ const WORKSPACE_TEST_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('WORKSPACE_TEST_')));
587
+
588
+ const groups = [
589
+ { heading: '# Tier routing', keys: Object.keys(merged).filter((k) => k.startsWith('TIER_') || k === 'MODEL_PROVIDER') },
590
+ { heading: '# Server', keys: Object.keys(merged).filter((k) => SERVER_KEYS.has(k)) },
591
+ { heading: '# Provider credentials', keys: Object.keys(merged).filter((k) =>
592
+ /(_API_KEY|_ENDPOINT|_API_BASE|_DEPLOYMENT|_MODEL|_ENDPOINT_PATH|_API_VERSION|_VERSION|_TIMEOUT_MS|_EMBEDDINGS_MODEL|_EMBEDDINGS_ENDPOINT|_MAX_TOOLS_FOR_ROUTING)$/.test(k) &&
593
+ !k.startsWith('LYNKR_') && !k.startsWith('HEADROOM_') && !k.startsWith('RATE_LIMIT_') &&
594
+ !k.startsWith('HOT_RELOAD_') && !k.startsWith('LOAD_SHEDDING_') && !k.startsWith('AGENTS_') &&
595
+ !k.startsWith('MCP_') && !k.startsWith('WEB_') && !k.startsWith('TINYFISH_') && !k.startsWith('WORKSPACE_TEST_') &&
596
+ !k.startsWith('NODE_') && !k.startsWith('TOON_')
597
+ ) },
598
+ { heading: '# Routing intelligence', keys: Object.keys(merged).filter((k) => k.startsWith('LYNKR_')) },
599
+ { heading: '# Tool execution', keys: Object.keys(merged).filter((k) => TOOL_EXEC_KEYS.has(k)) },
600
+ { heading: '# Caching', keys: Object.keys(merged).filter((k) => CACHE_KEYS.has(k)) },
601
+ { heading: '# Compression & context', keys: Object.keys(merged).filter((k) => COMPRESSION_KEYS.has(k)) },
602
+ { heading: '# Memory & tracking', keys: Object.keys(merged).filter((k) => MEMORY_KEYS.has(k)) },
603
+ { heading: '# Prompt & output shaping', keys: Object.keys(merged).filter((k) => SHAPING_KEYS.has(k)) },
604
+ { heading: '# Policy & budgets', keys: Object.keys(merged).filter((k) => POLICY_KEYS.has(k)) },
605
+ { heading: '# Agents', keys: Object.keys(merged).filter((k) => AGENT_KEYS.has(k)) },
606
+ { heading: '# Rate limiting', keys: Object.keys(merged).filter((k) => RATE_KEYS.has(k)) },
607
+ { heading: '# MCP sandbox', keys: Object.keys(merged).filter((k) => MCP_KEYS.has(k)) },
608
+ { heading: '# Web tools', keys: Object.keys(merged).filter((k) => WEB_KEYS.has(k)) },
609
+ { heading: '# TinyFish (web automation)', keys: Object.keys(merged).filter((k) => TINYFISH_KEYS.has(k)) },
610
+ { heading: '# Workspace test runner', keys: Object.keys(merged).filter((k) => WORKSPACE_TEST_KEYS.has(k)) },
611
+ { heading: '# Ops (hot reload, load shedding)', keys: Object.keys(merged).filter((k) => OPS_KEYS.has(k)) },
612
+ { heading: '# Logging', keys: ['LOG_LEVEL'].filter((k) => k in merged) },
613
+ ];
614
+
615
+ const seen = new Set();
616
+ for (const g of groups) {
617
+ if (!g.keys.length) continue;
618
+ lines.push(g.heading);
619
+ for (const k of g.keys) {
620
+ if (seen.has(k)) continue;
621
+ lines.push(`${k}=${merged[k]}`);
622
+ seen.add(k);
623
+ }
624
+ lines.push('');
625
+ }
626
+
627
+ // Catch-all for any other keys (e.g. _DEPLOYMENT defaults) we missed.
628
+ const remaining = Object.keys(merged).filter((k) => !seen.has(k));
629
+ if (remaining.length) {
630
+ lines.push('# Other');
631
+ for (const k of remaining) lines.push(`${k}=${merged[k]}`);
632
+ lines.push('');
633
+ }
634
+
635
+ return lines.join('\n');
636
+ }
637
+
638
+ function writeEnvFile(content, opts) {
639
+ if (opts.dryRun) {
640
+ process.stdout.write(content);
641
+ return;
642
+ }
643
+ const target = opts.output || path.join(process.cwd(), '.env');
644
+ if (fs.existsSync(target) && !opts.force) {
645
+ console.error(`✗ ${target} already exists. Use --force to overwrite, or --output=<path>.`);
646
+ process.exit(1);
647
+ }
648
+ fs.writeFileSync(target, content);
649
+ console.log(`✓ Wrote ${target}`);
650
+ }
651
+
652
+ // ──────────────────────────────────────────────────────────────────────────────
653
+ // Entry
654
+ // ──────────────────────────────────────────────────────────────────────────────
655
+
656
+ async function main() {
657
+ const opts = parseArgs(process.argv.slice(2));
658
+ if (opts.help) return showHelp();
659
+
660
+ if (!process.stdin.isTTY) {
661
+ console.error('✗ lynkr init needs an interactive TTY.');
662
+ console.error(' If you need a non-interactive setup, copy .env.example to .env manually,');
663
+ console.error(' or run `lynkr init --dry-run` to preview the wizard prompts.');
664
+ process.exit(1);
665
+ }
666
+
667
+ return runInteractive(opts);
668
+ }
669
+
670
+ // Run when invoked directly (`node bin/lynkr-init.js`) or dispatched from
671
+ // cli.js (which sets _LYNKR_SUBCMD). Stay quiet when require()'d by tests.
672
+ if (require.main === module || process.env._LYNKR_SUBCMD === 'init') {
673
+ main().catch((err) => {
674
+ console.error(`✗ ${err.message}`);
675
+ process.exit(1);
676
+ });
677
+ }
678
+
679
+ module.exports = {
680
+ PROVIDERS,
681
+ PROVIDER_ORDER,
682
+ TIERS,
683
+ parseArgs,
684
+ buildEnvContent,
685
+ };
package/install.sh CHANGED
@@ -128,63 +128,17 @@ install_dependencies() {
128
128
  fi
129
129
  }
130
130
 
131
- # Create default .env file
131
+ # Skip .env creation — the install script runs without a TTY when invoked via
132
+ # `curl | bash`, so the interactive `lynkr init` wizard can't run here. We leave
133
+ # .env unmade so the user is prompted to run `lynkr init` in their own shell
134
+ # afterward, which produces a fully-populated config (~150 keys grouped by
135
+ # section) instead of the old 892-line .env.example dump.
132
136
  create_env_file() {
133
- if [ ! -f "$INSTALL_DIR/.env" ]; then
134
- print_info "Creating .env configuration file..."
135
-
136
- # Try to copy from .env.example (comprehensive configuration)
137
- if [ -f "$INSTALL_DIR/.env.example" ]; then
138
- cp "$INSTALL_DIR/.env.example" "$INSTALL_DIR/.env"
139
- print_success "Created .env from .env.example (all features documented)"
140
- else
141
- # Fallback: create minimal .env if .env.example doesn't exist
142
- cat > "$INSTALL_DIR/.env" << 'EOF'
143
- # Lynkr Configuration
144
- # For full options, see: https://github.com/Fast-Editor/Lynkr/blob/main/.env.example
145
-
146
- # Model Provider (databricks, openai, azure-openai, azure-anthropic, openrouter, ollama, llamacpp)
147
- MODEL_PROVIDER=ollama
148
-
149
- # Server Configuration
150
- PORT=8081
151
-
152
- # Ollama Configuration (default for local development)
153
- OLLAMA_MODEL=qwen2.5-coder:7b
154
- OLLAMA_ENDPOINT=http://localhost:11434
155
-
156
- # Tier-based routing (uncomment and configure to enable)
157
- # TIER_SIMPLE=ollama:qwen2.5-coder:7b
158
- # TIER_MEDIUM=ollama:qwen2.5-coder:7b
159
- # TIER_COMPLEX=ollama:qwen2.5-coder:7b
160
- # TIER_REASONING=ollama:qwen2.5-coder:7b
161
-
162
- # Long-Term Memory System (Titans-Inspired) - Enabled by default
163
- MEMORY_ENABLED=true
164
- MEMORY_RETRIEVAL_LIMIT=5
165
- MEMORY_SURPRISE_THRESHOLD=0.3
166
-
167
- # Uncomment and configure your preferred cloud provider:
168
- # OPENAI_API_KEY=sk-your-key
169
- # OPENROUTER_API_KEY=your-key
170
- # DATABRICKS_API_KEY=your-key
171
- # DATABRICKS_API_BASE=https://your-workspace.databricks.com
172
- EOF
173
- print_success "Created basic .env file"
174
- fi
175
-
176
- echo ""
177
- print_info "📝 Configuration ready! Key settings:"
178
- echo " • Default provider: Ollama (local, offline)"
179
- echo " • Memory system: Enabled (learns from conversations)"
180
- echo " • Port: 8081"
181
- echo ""
182
- print_warning "To use cloud providers (Databricks/OpenAI/Azure):"
183
- echo " Edit: ${BLUE}nano $INSTALL_DIR/.env${NC}"
184
- echo " Add your API keys and change MODEL_PROVIDER"
185
- else
137
+ if [ -f "$INSTALL_DIR/.env" ]; then
186
138
  print_warning ".env file already exists, skipping"
139
+ return
187
140
  fi
141
+ print_info "Skipping .env creation — run ${BLUE}lynkr init${NC} after install for an interactive setup."
188
142
  }
189
143
 
190
144
  # Create symlink for global access
@@ -224,44 +178,35 @@ print_next_steps() {
224
178
  print_success "Lynkr installed successfully!"
225
179
  echo "=============================="
226
180
  echo ""
227
- echo "🚀 Quick Start Guide:"
181
+ echo "🚀 Quick Start:"
228
182
  echo ""
229
- echo " ${GREEN}Option A: Use Ollama (Free, Local, Offline)${NC}"
230
- echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
183
+ echo " 1. Run the setup wizard:"
184
+ echo " ${BLUE}lynkr init${NC} ${GREEN}← interactive config (4 prompts, ~30 sec)${NC}"
231
185
  echo ""
232
- echo " 1. Install Ollama (if not already installed):"
233
- echo " ${BLUE}lynkr-setup${NC} ${GREEN}← Automatic Ollama installer${NC}"
186
+ echo " The wizard asks for your usage mode (Claude Pro/Max via wrap, or direct"
187
+ echo " API), tier picks across 12 supported providers, credentials for what you"
188
+ echo " chose, and a few routing knobs. It writes a fully-populated .env with"
189
+ echo " production defaults for everything else (caching, compression, policy"
190
+ echo " budgets, MCP sandbox, agents, rate limiting)."
234
191
  echo ""
235
192
  echo " 2. Start Lynkr:"
236
- echo " ${BLUE}lynkr${NC}"
193
+ echo " ${BLUE}lynkr${NC} ${GREEN}← run as a proxy server${NC}"
194
+ echo " ${BLUE}lynkr wrap claude${NC} ${GREEN}← OR launch a wrapped AI tool${NC}"
237
195
  echo ""
238
- echo " 3. Configure Claude Code CLI:"
196
+ echo " 3. Point your tool at Lynkr:"
239
197
  echo " ${BLUE}export ANTHROPIC_BASE_URL=http://localhost:8081${NC}"
198
+ echo " ${BLUE}export ANTHROPIC_API_KEY=any-non-empty-value${NC}"
240
199
  echo " ${BLUE}claude${NC}"
241
200
  echo ""
242
- echo " ${YELLOW}Option B: Use Cloud Providers (Databricks/OpenAI/Azure)${NC}"
243
- echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
244
- echo ""
245
- echo " 1. Edit configuration file:"
246
- echo " ${BLUE}nano $INSTALL_DIR/.env${NC}"
247
- echo ""
248
- echo " Update these lines:"
249
- echo " ${BLUE}MODEL_PROVIDER=databricks${NC} ${GREEN}← Change from 'ollama'${NC}"
250
- echo " ${BLUE}DATABRICKS_API_KEY=dapi_xxxxx${NC} ${GREEN}← Add your key${NC}"
251
- echo " ${BLUE}DATABRICKS_API_BASE=https://your-workspace.databricks.com${NC}"
252
- echo ""
253
- echo " 2. Start Lynkr:"
254
- echo " ${BLUE}lynkr${NC}"
255
- echo ""
256
- echo " 3. Configure Claude Code CLI:"
257
- echo " ${BLUE}export ANTHROPIC_BASE_URL=http://localhost:8081${NC}"
258
- echo " ${BLUE}export ANTHROPIC_API_KEY=any-non-empty-value${NC} ${GREEN}← Placeholder${NC}"
259
- echo " ${BLUE}claude${NC}"
201
+ echo " ${YELLOW}Manual configuration (alternative)${NC}"
202
+ echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
203
+ echo " Copy ${BLUE}.env.example${NC} to ${BLUE}.env${NC} and edit by hand if you prefer."
204
+ echo " The 892-line template documents every available knob."
260
205
  echo ""
261
206
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
262
207
  echo ""
263
- echo "💡 ${YELLOW}Tip:${NC} Memory system is enabled by default"
264
- echo " Lynkr remembers preferences and project context across sessions"
208
+ echo "💡 ${YELLOW}Tip:${NC} Memory system, prompt caching, and TOON compression are all on"
209
+ echo " by default. The wizard's defaults match a production-grade Lynkr setup."
265
210
  echo ""
266
211
  echo "📚 Documentation: ${BLUE}https://github.com/Fast-Editor/Lynkr${NC}"
267
212
  echo "💬 Discord: ${BLUE}https://discord.gg/qF7DDxrX${NC}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "9.7.0",
3
+ "version": "9.7.2",
4
4
  "description": "Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "dev": "nodemon index.js",
17
17
  "lint": "eslint src index.js",
18
18
  "test": "npm run test:unit && npm run test:performance",
19
- "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js",
19
+ "test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/model-registry-cost.test.js test/task-decomposition.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js",
20
20
  "test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
21
21
  "test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
22
22
  "test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",