lynkr 9.7.0 → 9.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -318
- package/bin/cli.js +4 -0
- package/bin/lynkr-init.js +679 -0
- package/install.sh +26 -81
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lynkr
|
|
2
2
|
|
|
3
|
-
###
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
**Option A: Free & Local (Ollama) - Recommended for Testing**
|
|
62
|
+
The fastest path is the interactive wizard:
|
|
74
63
|
|
|
75
64
|
```bash
|
|
76
|
-
|
|
77
|
-
ollama pull qwen2.5-coder:latest
|
|
65
|
+
lynkr init
|
|
78
66
|
```
|
|
79
67
|
|
|
80
|
-
|
|
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
|
-
|
|
70
|
+
Useful flags:
|
|
102
71
|
|
|
103
72
|
```bash
|
|
104
|
-
#
|
|
73
|
+
lynkr init --force # overwrite an existing .env
|
|
105
74
|
```
|
|
106
75
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
131
|
-
|
|
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,679 @@
|
|
|
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/v1' },
|
|
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
|
+
{ key: 'BEDROCK_API_KEY', label: 'AWS Bedrock API key (or use IAM)', secret: true },
|
|
116
|
+
],
|
|
117
|
+
extras: [],
|
|
118
|
+
defaultModel: 'anthropic.claude-sonnet-4-v1:0',
|
|
119
|
+
},
|
|
120
|
+
vertex: {
|
|
121
|
+
label: 'Google Vertex AI',
|
|
122
|
+
local: false,
|
|
123
|
+
creds: [
|
|
124
|
+
{ key: 'VERTEX_API_KEY', label: 'Vertex API key (or use ADC)', secret: true },
|
|
125
|
+
],
|
|
126
|
+
extras: [],
|
|
127
|
+
defaultModel: 'gemini-2.0-flash',
|
|
128
|
+
},
|
|
129
|
+
zai: {
|
|
130
|
+
label: 'Z.ai (GLM family)',
|
|
131
|
+
local: false,
|
|
132
|
+
creds: [
|
|
133
|
+
{ key: 'ZAI_API_KEY', label: 'Z.ai API key', secret: true },
|
|
134
|
+
],
|
|
135
|
+
extras: [],
|
|
136
|
+
defaultModel: 'GLM-4.7',
|
|
137
|
+
},
|
|
138
|
+
moonshot: {
|
|
139
|
+
label: 'Moonshot (Kimi family)',
|
|
140
|
+
local: false,
|
|
141
|
+
creds: [
|
|
142
|
+
{ key: 'MOONSHOT_API_KEY', label: 'Moonshot API key', secret: true },
|
|
143
|
+
],
|
|
144
|
+
extras: [],
|
|
145
|
+
defaultModel: 'kimi-k2-turbo-preview',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const PROVIDER_ORDER = [
|
|
150
|
+
'ollama', 'llamacpp', 'lmstudio',
|
|
151
|
+
'azure-anthropic', 'azure-openai', 'openai', 'openrouter',
|
|
152
|
+
'databricks', 'bedrock', 'vertex', 'zai', 'moonshot',
|
|
153
|
+
];
|
|
154
|
+
const TIERS = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
155
|
+
|
|
156
|
+
// Always-emitted baseline. Mirrors the production-grade config the maintainer
|
|
157
|
+
// runs locally: caching/compression on, generous policy budgets, sandboxed
|
|
158
|
+
// agents, MCP/web defaults, etc. Users can edit any of these post-generation;
|
|
159
|
+
// the wizard prompts only for tier picks, credentials, and a handful of
|
|
160
|
+
// intelligence knobs. Everything else is opinionated default.
|
|
161
|
+
//
|
|
162
|
+
// Categorised inline so future contributors know which group a key lives in.
|
|
163
|
+
const BASELINE_ENV = {
|
|
164
|
+
// ── Databricks placeholders (satisfy startup validator) ───────────────
|
|
165
|
+
DATABRICKS_API_BASE: 'http://localhost:8081',
|
|
166
|
+
DATABRICKS_API_KEY: 'tier-routing-active',
|
|
167
|
+
DATABRICKS_ENDPOINT_PATH: '/unused',
|
|
168
|
+
|
|
169
|
+
// ── Server ────────────────────────────────────────────────────────────
|
|
170
|
+
PORT: '8081',
|
|
171
|
+
NODE_ENV: 'production',
|
|
172
|
+
REQUEST_JSON_LIMIT: '1gb',
|
|
173
|
+
SESSION_DB_PATH: './data/sessions.db',
|
|
174
|
+
ENABLE_TOOL_SEARCH: 'true',
|
|
175
|
+
LOG_LEVEL: 'silent',
|
|
176
|
+
|
|
177
|
+
// ── Routing intelligence (tuned defaults) ─────────────────────────────
|
|
178
|
+
LYNKR_PREFLIGHT_ENABLED: 'false',
|
|
179
|
+
LYNKR_PREFLIGHT_TIMEOUT_MS: '120000',
|
|
180
|
+
LYNKR_CASCADE_ENABLED: 'true',
|
|
181
|
+
LYNKR_KNN_MIN_INDEX_SIZE: '200',
|
|
182
|
+
LYNKR_KNN_CONFIDENCE_HIGH: '0.55',
|
|
183
|
+
LYNKR_KNN_CONFIDENCE_LOW: '0.30',
|
|
184
|
+
|
|
185
|
+
// ── Tool execution ────────────────────────────────────────────────────
|
|
186
|
+
TOOL_EXECUTION_MODE: 'client',
|
|
187
|
+
TOOL_INJECTION_ENABLED: 'false',
|
|
188
|
+
SMART_TOOL_SELECTION_MODE: 'disabled',
|
|
189
|
+
SMART_TOOL_SELECTION_TOKEN_BUDGET: '2500',
|
|
190
|
+
CODE_MODE_ENABLED: 'true',
|
|
191
|
+
|
|
192
|
+
// ── Caching ───────────────────────────────────────────────────────────
|
|
193
|
+
PROMPT_CACHE_ENABLED: 'true',
|
|
194
|
+
PROMPT_CACHE_MAX_ENTRIES: '1000',
|
|
195
|
+
PROMPT_CACHE_TTL_MS: '300000',
|
|
196
|
+
SEMANTIC_CACHE_ENABLED: 'true',
|
|
197
|
+
SEMANTIC_CACHE_THRESHOLD: '0.85',
|
|
198
|
+
SEMANTIC_CACHE_MAX_ENTRIES: '50',
|
|
199
|
+
SEMANTIC_CACHE_TTL_MS: '300000',
|
|
200
|
+
|
|
201
|
+
// ── Compression: TOON + Headroom sidecar ──────────────────────────────
|
|
202
|
+
TOON_ENABLED: 'true',
|
|
203
|
+
TOON_MIN_BYTES: '4096',
|
|
204
|
+
TOON_FAIL_OPEN: 'true',
|
|
205
|
+
TOON_LOG_STATS: 'true',
|
|
206
|
+
HEADROOM_ENABLED: 'true',
|
|
207
|
+
HEADROOM_ENDPOINT: 'http://localhost:8787',
|
|
208
|
+
HEADROOM_TIMEOUT_MS: '5000',
|
|
209
|
+
HEADROOM_MIN_TOKENS: '100',
|
|
210
|
+
HEADROOM_MODE: 'optimize',
|
|
211
|
+
HEADROOM_PROVIDER: 'anthropic',
|
|
212
|
+
HEADROOM_DOCKER_ENABLED: 'true',
|
|
213
|
+
HEADROOM_DOCKER_IMAGE: 'lynkr/headroom-sidecar:latest',
|
|
214
|
+
HEADROOM_DOCKER_CONTAINER_NAME: 'lynkr-headroom',
|
|
215
|
+
HEADROOM_DOCKER_PORT: '8787',
|
|
216
|
+
HEADROOM_DOCKER_AUTO_BUILD: 'true',
|
|
217
|
+
HEADROOM_SMART_CRUSHER: 'true',
|
|
218
|
+
HEADROOM_SMART_CRUSHER_MIN_TOKENS: '200',
|
|
219
|
+
HEADROOM_SMART_CRUSHER_MAX_ITEMS: '15',
|
|
220
|
+
HEADROOM_TOOL_CRUSHER: 'true',
|
|
221
|
+
HEADROOM_CACHE_ALIGNER: 'true',
|
|
222
|
+
HEADROOM_ROLLING_WINDOW: 'true',
|
|
223
|
+
HEADROOM_KEEP_TURNS: '10',
|
|
224
|
+
HEADROOM_CCR: 'true',
|
|
225
|
+
HEADROOM_CCR_TTL: '300',
|
|
226
|
+
|
|
227
|
+
// ── Memory + token tracking ───────────────────────────────────────────
|
|
228
|
+
MEMORY_ENABLED: 'true',
|
|
229
|
+
MEMORY_RETRIEVAL_LIMIT: '5',
|
|
230
|
+
MEMORY_SURPRISE_THRESHOLD: '0.3',
|
|
231
|
+
MEMORY_MAX_AGE_DAYS: '90',
|
|
232
|
+
MEMORY_MAX_COUNT: '10000',
|
|
233
|
+
MEMORY_EXTRACTION_ENABLED: 'true',
|
|
234
|
+
MEMORY_DECAY_ENABLED: 'true',
|
|
235
|
+
MEMORY_DECAY_HALF_LIFE: '30',
|
|
236
|
+
MEMORY_FORMAT: 'compact',
|
|
237
|
+
MEMORY_DEDUP_ENABLED: 'true',
|
|
238
|
+
MEMORY_DEDUP_LOOKBACK: '5',
|
|
239
|
+
MEMORY_TTL: '3600000',
|
|
240
|
+
TOKEN_TRACKING_ENABLED: 'true',
|
|
241
|
+
TOOL_TRUNCATION_ENABLED: 'true',
|
|
242
|
+
|
|
243
|
+
// ── Prompt/output shaping ─────────────────────────────────────────────
|
|
244
|
+
SYSTEM_PROMPT_MODE: 'dynamic',
|
|
245
|
+
TOOL_DESCRIPTIONS: 'minimal',
|
|
246
|
+
HISTORY_COMPRESSION_ENABLED: 'true',
|
|
247
|
+
HISTORY_KEEP_RECENT_TURNS: '10',
|
|
248
|
+
HISTORY_SUMMARIZE_OLDER: 'true',
|
|
249
|
+
TOKEN_BUDGET_WARNING: '100000',
|
|
250
|
+
TOKEN_BUDGET_MAX: '180000',
|
|
251
|
+
TOKEN_BUDGET_ENFORCEMENT: 'true',
|
|
252
|
+
CAVEMAN_ENABLED: 'false',
|
|
253
|
+
CAVEMAN_LEVEL: 'full',
|
|
254
|
+
MARKDOWN_RENDER_ANSI: 'false',
|
|
255
|
+
|
|
256
|
+
// ── Policy & budgets ──────────────────────────────────────────────────
|
|
257
|
+
POLICY_MAX_STEPS: '2000',
|
|
258
|
+
POLICY_MAX_TOOL_CALLS: '2000',
|
|
259
|
+
POLICY_TOOL_LOOP_THRESHOLD: '100',
|
|
260
|
+
POLICY_GIT_ALLOW_PUSH: 'false',
|
|
261
|
+
POLICY_GIT_ALLOW_PULL: 'true',
|
|
262
|
+
POLICY_GIT_ALLOW_COMMIT: 'true',
|
|
263
|
+
POLICY_GIT_REQUIRE_TESTS: 'false',
|
|
264
|
+
POLICY_GIT_AUTOSTASH: 'false',
|
|
265
|
+
POLICY_FILE_BLOCKED_PATHS: '/.env,.env,/etc/passwd,/etc/shadow',
|
|
266
|
+
POLICY_SAFE_COMMANDS_ENABLED: 'true',
|
|
267
|
+
|
|
268
|
+
// ── Agents ────────────────────────────────────────────────────────────
|
|
269
|
+
AGENTS_ENABLED: 'true',
|
|
270
|
+
AGENTS_MAX_CONCURRENT: '10',
|
|
271
|
+
AGENTS_DEFAULT_MODEL: 'haiku',
|
|
272
|
+
AGENTS_MAX_STEPS: '15',
|
|
273
|
+
AGENTS_TIMEOUT: '300000',
|
|
274
|
+
|
|
275
|
+
// ── Rate limiting ─────────────────────────────────────────────────────
|
|
276
|
+
RATE_LIMIT_ENABLED: 'true',
|
|
277
|
+
RATE_LIMIT_WINDOW_MS: '60000',
|
|
278
|
+
RATE_LIMIT_MAX: '100',
|
|
279
|
+
RATE_LIMIT_KEY_BY: 'session',
|
|
280
|
+
|
|
281
|
+
// ── Hot reload + load shedding ────────────────────────────────────────
|
|
282
|
+
HOT_RELOAD_ENABLED: 'true',
|
|
283
|
+
HOT_RELOAD_DEBOUNCE_MS: '1000',
|
|
284
|
+
LOAD_SHEDDING_ENABLED: 'true',
|
|
285
|
+
LOAD_SHEDDING_HEAP_THRESHOLD: '0.85',
|
|
286
|
+
LOAD_SHEDDING_MEMORY_THRESHOLD: '0.95',
|
|
287
|
+
|
|
288
|
+
// ── Per-provider extras (secrets stay empty; wizard or user fills in) ─
|
|
289
|
+
AZURE_ANTHROPIC_ENDPOINT: 'https://api.anthropic.com/v1/messages',
|
|
290
|
+
AZURE_ANTHROPIC_VERSION: '2023-06-01',
|
|
291
|
+
AZURE_OPENAI_API_VERSION: '2024-08-01-preview',
|
|
292
|
+
OLLAMA_MODEL: 'minimax-m2.5:cloud',
|
|
293
|
+
OLLAMA_TIMEOUT_MS: '120000',
|
|
294
|
+
OLLAMA_EMBEDDINGS_MODEL: 'nomic-embed-text',
|
|
295
|
+
OLLAMA_EMBEDDINGS_ENDPOINT: 'http://localhost:11434/api/embeddings',
|
|
296
|
+
OPENROUTER_API_KEY: '',
|
|
297
|
+
OPENROUTER_MODEL: 'openai/gpt-4o-mini',
|
|
298
|
+
OPENROUTER_EMBEDDINGS_MODEL: 'openai/text-embedding-ada-002',
|
|
299
|
+
OPENROUTER_ENDPOINT: 'https://openrouter.ai/api/v1/chat/completions',
|
|
300
|
+
OPENROUTER_MAX_TOOLS_FOR_ROUTING: '15',
|
|
301
|
+
MOONSHOT_API_KEY: '',
|
|
302
|
+
MOONSHOT_ENDPOINT: 'https://api.moonshot.ai/v1/chat/completions',
|
|
303
|
+
MOONSHOT_MODEL: 'kimi-k2.6',
|
|
304
|
+
LLAMACPP_ENDPOINT: 'http://localhost:8080',
|
|
305
|
+
LLAMACPP_MODEL: 'default',
|
|
306
|
+
LLAMACPP_TIMEOUT_MS: '120000',
|
|
307
|
+
LLAMACPP_EMBEDDINGS_ENDPOINT: 'http://localhost:8080/embeddings',
|
|
308
|
+
LMSTUDIO_ENDPOINT: 'http://localhost:1234',
|
|
309
|
+
LMSTUDIO_MODEL: 'default',
|
|
310
|
+
LMSTUDIO_TIMEOUT_MS: '120000',
|
|
311
|
+
|
|
312
|
+
// ── MCP sandbox (Docker-isolated MCP tool execution) ──────────────────
|
|
313
|
+
MCP_SANDBOX_ENABLED: 'true',
|
|
314
|
+
MCP_SANDBOX_RUNTIME: 'docker',
|
|
315
|
+
MCP_SANDBOX_CONTAINER_WORKSPACE: '/workspace',
|
|
316
|
+
MCP_SANDBOX_MOUNT_WORKSPACE: 'true',
|
|
317
|
+
MCP_SANDBOX_ALLOW_NETWORKING: 'false',
|
|
318
|
+
MCP_SANDBOX_NETWORK_MODE: 'none',
|
|
319
|
+
MCP_SANDBOX_PASSTHROUGH_ENV: 'PATH,LANG,LC_ALL,TERM,HOME',
|
|
320
|
+
MCP_SANDBOX_TIMEOUT_MS: '20000',
|
|
321
|
+
MCP_SANDBOX_REUSE_SESSION: 'true',
|
|
322
|
+
MCP_SANDBOX_READ_ONLY_ROOT: 'false',
|
|
323
|
+
MCP_SANDBOX_NO_NEW_PRIVILEGES: 'true',
|
|
324
|
+
MCP_SANDBOX_DROP_CAPABILITIES: 'ALL',
|
|
325
|
+
MCP_SANDBOX_MEMORY_LIMIT: '512m',
|
|
326
|
+
MCP_SANDBOX_CPU_LIMIT: '1.0',
|
|
327
|
+
MCP_SANDBOX_PIDS_LIMIT: '100',
|
|
328
|
+
MCP_SANDBOX_PERMISSION_MODE: 'auto',
|
|
329
|
+
MCP_MANIFEST_DIRS: '~/.claude/mcp',
|
|
330
|
+
|
|
331
|
+
// ── Web tools (search + fetch) ────────────────────────────────────────
|
|
332
|
+
WEB_SEARCH_ENDPOINT: 'http://localhost:8888/search',
|
|
333
|
+
WEB_SEARCH_ALLOW_ALL: 'true',
|
|
334
|
+
WEB_SEARCH_TIMEOUT_MS: '10000',
|
|
335
|
+
WEB_FETCH_BODY_PREVIEW_MAX: '10000',
|
|
336
|
+
WEB_SEARCH_RETRY_ENABLED: 'true',
|
|
337
|
+
WEB_SEARCH_MAX_RETRIES: '2',
|
|
338
|
+
|
|
339
|
+
// ── TinyFish (web automation) ─────────────────────────────────────────
|
|
340
|
+
TINYFISH_API_KEY: '',
|
|
341
|
+
TINYFISH_ENDPOINT: 'https://agent.tinyfish.ai/v1/automation/run-sse',
|
|
342
|
+
TINYFISH_BROWSER_PROFILE: 'lite',
|
|
343
|
+
TINYFISH_TIMEOUT_MS: '120000',
|
|
344
|
+
TINYFISH_PROXY_ENABLED: 'false',
|
|
345
|
+
TINYFISH_PROXY_COUNTRY: 'US',
|
|
346
|
+
|
|
347
|
+
// ── Workspace test runner ─────────────────────────────────────────────
|
|
348
|
+
WORKSPACE_TEST_TIMEOUT_MS: '600000',
|
|
349
|
+
WORKSPACE_TEST_SANDBOX: 'auto',
|
|
350
|
+
WORKSPACE_TEST_COVERAGE_FILES: 'coverage/coverage-summary.json',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
354
|
+
// Args
|
|
355
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
function parseArgs(argv) {
|
|
358
|
+
const opts = { force: false, dryRun: false, output: null, help: false };
|
|
359
|
+
for (let i = 0; i < argv.length; i++) {
|
|
360
|
+
const a = argv[i];
|
|
361
|
+
if (a === '--help' || a === '-h') opts.help = true;
|
|
362
|
+
else if (a === '--force' || a === '-f') opts.force = true;
|
|
363
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
364
|
+
else if (a.startsWith('--output=')) opts.output = a.slice('--output='.length);
|
|
365
|
+
else if (a === '--output' || a === '-o') opts.output = argv[++i];
|
|
366
|
+
}
|
|
367
|
+
return opts;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function showHelp() {
|
|
371
|
+
console.log(`lynkr init — interactive setup wizard
|
|
372
|
+
|
|
373
|
+
Usage:
|
|
374
|
+
lynkr init Interactive wizard
|
|
375
|
+
lynkr init --force Overwrite existing .env
|
|
376
|
+
lynkr init --output=<path> Write to <path> instead of .env
|
|
377
|
+
lynkr init --dry-run Print to stdout, don't write
|
|
378
|
+
lynkr init --help
|
|
379
|
+
|
|
380
|
+
The wizard asks for:
|
|
381
|
+
1. Usage mode (Claude Pro/Max via wrap, or direct API keys)
|
|
382
|
+
2. Provider + model for each tier (SIMPLE / MEDIUM / COMPLEX / REASONING)
|
|
383
|
+
3. Credentials for each picked provider (re-used across tiers)
|
|
384
|
+
4. Routing intelligence (visible badge, intent window, decay)
|
|
385
|
+
|
|
386
|
+
Providers covered: ${PROVIDER_ORDER.join(', ')}.
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
391
|
+
// Prompt helpers
|
|
392
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
function makeAsker() {
|
|
395
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
396
|
+
const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
|
|
397
|
+
const close = () => rl.close();
|
|
398
|
+
return { ask, close };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function pickFromList(ask, label, choices, defaultIdx = 0) {
|
|
402
|
+
console.log(`\n${label}`);
|
|
403
|
+
choices.forEach((c, i) => {
|
|
404
|
+
const marker = i === defaultIdx ? '>' : ' ';
|
|
405
|
+
console.log(` ${marker} ${i + 1}) ${c}`);
|
|
406
|
+
});
|
|
407
|
+
const raw = await ask(`Choice [1-${choices.length}] (default ${defaultIdx + 1}): `);
|
|
408
|
+
if (!raw) return defaultIdx;
|
|
409
|
+
const n = parseInt(raw, 10);
|
|
410
|
+
if (Number.isNaN(n) || n < 1 || n > choices.length) {
|
|
411
|
+
console.log(` → invalid, using default (${choices[defaultIdx]})`);
|
|
412
|
+
return defaultIdx;
|
|
413
|
+
}
|
|
414
|
+
return n - 1;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function askWithDefault(ask, label, defaultValue) {
|
|
418
|
+
const v = await ask(`${label}${defaultValue ? ` [${defaultValue}]` : ''}: `);
|
|
419
|
+
return v || defaultValue || '';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function askYesNo(ask, label, defaultYes = true) {
|
|
423
|
+
const v = await ask(`${label} [${defaultYes ? 'Y/n' : 'y/N'}]: `);
|
|
424
|
+
if (!v) return defaultYes;
|
|
425
|
+
return /^y(es)?$/i.test(v);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
429
|
+
// Wizard
|
|
430
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
async function runInteractive(opts) {
|
|
433
|
+
console.log('lynkr init — interactive setup\n');
|
|
434
|
+
const { ask, close } = makeAsker();
|
|
435
|
+
const env = {};
|
|
436
|
+
const credsCollected = {}; // dedupe per env key
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// ── 1. Usage mode ──
|
|
440
|
+
const modeIdx = await pickFromList(ask,
|
|
441
|
+
'Usage mode:',
|
|
442
|
+
[
|
|
443
|
+
'Claude Pro/Max subscription (via `lynkr wrap claude`, OAuth passthrough)',
|
|
444
|
+
'Direct API usage (pay-as-you-go with API keys)',
|
|
445
|
+
],
|
|
446
|
+
0,
|
|
447
|
+
);
|
|
448
|
+
const isWrap = modeIdx === 0;
|
|
449
|
+
|
|
450
|
+
if (isWrap) {
|
|
451
|
+
env.LYNKR_OAUTH_PASSTHROUGH = 'true';
|
|
452
|
+
console.log('\n → OAuth passthrough enabled. COMPLEX/REASONING tiers will be sent');
|
|
453
|
+
console.log(' byte-for-byte to api.anthropic.com against your subscription.');
|
|
454
|
+
console.log(' You only need to configure a local model for SIMPLE/MEDIUM.\n');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── 2. Per-tier provider + model ──
|
|
458
|
+
const tierConfig = {};
|
|
459
|
+
const collectCreds = async (providerKey) => {
|
|
460
|
+
const p = PROVIDERS[providerKey];
|
|
461
|
+
for (const c of p.creds) {
|
|
462
|
+
if (credsCollected[c.key]) continue;
|
|
463
|
+
const existing = process.env[c.key];
|
|
464
|
+
const def = existing || c.default || '';
|
|
465
|
+
const prompt = ` ${c.label}${c.secret ? ' (hidden output not supported; paste anyway)' : ''}`;
|
|
466
|
+
const v = await askWithDefault(ask, prompt, def);
|
|
467
|
+
if (v) {
|
|
468
|
+
env[c.key] = v;
|
|
469
|
+
credsCollected[c.key] = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const ex of p.extras) {
|
|
473
|
+
if (env[ex.key]) continue;
|
|
474
|
+
const v = await askWithDefault(ask, ` ${ex.label}`, ex.default);
|
|
475
|
+
if (v) env[ex.key] = v;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const providerChoices = PROVIDER_ORDER.map((k) => PROVIDERS[k].label);
|
|
480
|
+
|
|
481
|
+
for (const tier of TIERS) {
|
|
482
|
+
const headline = isWrap && (tier === 'COMPLEX' || tier === 'REASONING')
|
|
483
|
+
? `Tier ${tier} — covered by Pro/Max subscription, but you can override:`
|
|
484
|
+
: `Tier ${tier} — pick a provider:`;
|
|
485
|
+
const defaultIdx = isWrap && (tier === 'COMPLEX' || tier === 'REASONING')
|
|
486
|
+
? PROVIDER_ORDER.indexOf('azure-anthropic')
|
|
487
|
+
: 0;
|
|
488
|
+
|
|
489
|
+
const skipOpt = isWrap && (tier === 'COMPLEX' || tier === 'REASONING')
|
|
490
|
+
? [...providerChoices, 'Skip — let subscription passthrough handle it']
|
|
491
|
+
: providerChoices;
|
|
492
|
+
|
|
493
|
+
const idx = await pickFromList(ask, headline, skipOpt, defaultIdx);
|
|
494
|
+
|
|
495
|
+
if (idx === providerChoices.length) {
|
|
496
|
+
// Skip selected — leave TIER_<tier> unset
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const providerKey = PROVIDER_ORDER[idx];
|
|
501
|
+
const p = PROVIDERS[providerKey];
|
|
502
|
+
const model = await askWithDefault(ask, ` Model for ${tier}`, p.defaultModel);
|
|
503
|
+
tierConfig[tier] = { provider: providerKey, model };
|
|
504
|
+
await collectCreds(providerKey);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
for (const tier of TIERS) {
|
|
508
|
+
if (tierConfig[tier]) {
|
|
509
|
+
env[`TIER_${tier}`] = `${tierConfig[tier].provider}:${tierConfig[tier].model}`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Primary provider hint for legacy code paths
|
|
514
|
+
const firstTier = TIERS.map((t) => tierConfig[t]).find(Boolean);
|
|
515
|
+
if (firstTier) env.MODEL_PROVIDER = firstTier.provider;
|
|
516
|
+
|
|
517
|
+
// ── 3. Routing intelligence ──
|
|
518
|
+
console.log('\nRouting intelligence:');
|
|
519
|
+
if (await askYesNo(ask, 'Show the routing badge in your TUI (`*[Lynkr] …*`)?', isWrap)) {
|
|
520
|
+
env.LYNKR_VISIBLE_ROUTING = 'true';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const windowRaw = await askWithDefault(ask, 'Intent-scoring window size (1 = latest message only)', '5');
|
|
524
|
+
const windowN = parseInt(windowRaw, 10);
|
|
525
|
+
if (!Number.isNaN(windowN) && windowN >= 1) env.LYNKR_INTENT_WINDOW_N = String(windowN);
|
|
526
|
+
|
|
527
|
+
const decayRaw = await askWithDefault(ask, 'Intent-scoring per-turn decay (0.1-1.0)', '0.7');
|
|
528
|
+
const decay = parseFloat(decayRaw);
|
|
529
|
+
if (!Number.isNaN(decay) && decay > 0 && decay <= 1) env.LYNKR_INTENT_DECAY = String(decay);
|
|
530
|
+
|
|
531
|
+
close();
|
|
532
|
+
console.log('');
|
|
533
|
+
writeEnvFile(buildEnvContent(env, isWrap, tierConfig), opts);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
close();
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
541
|
+
// Output
|
|
542
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
function buildEnvContent(env, isWrap, tierConfig) {
|
|
545
|
+
// Baseline first, user choices on top — so user input always wins for keys
|
|
546
|
+
// they explicitly answered (e.g. LOG_LEVEL if the wizard ever asks for it).
|
|
547
|
+
const merged = { ...BASELINE_ENV, ...env };
|
|
548
|
+
|
|
549
|
+
const lines = [
|
|
550
|
+
'# Lynkr configuration',
|
|
551
|
+
`# Generated by 'lynkr init' at ${new Date().toISOString()}`,
|
|
552
|
+
`# Mode: ${isWrap ? 'wrap (Claude Pro/Max subscription)' : 'direct API'}`,
|
|
553
|
+
'# Edit directly to tweak; full reference in .env.example',
|
|
554
|
+
'',
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
// Group output by section in the order it appears in the generated file.
|
|
558
|
+
// Mirrors the layout of the .env.example reference doc.
|
|
559
|
+
const SERVER_KEYS = new Set(['PORT', 'NODE_ENV', 'REQUEST_JSON_LIMIT', 'SESSION_DB_PATH', 'WORKSPACE_ROOT', 'ENABLE_TOOL_SEARCH']);
|
|
560
|
+
const TOOL_EXEC_KEYS = new Set(['TOOL_EXECUTION_MODE', 'TOOL_INJECTION_ENABLED', 'SMART_TOOL_SELECTION_MODE', 'SMART_TOOL_SELECTION_TOKEN_BUDGET', 'CODE_MODE_ENABLED']);
|
|
561
|
+
const CACHE_KEYS = new Set([
|
|
562
|
+
'PROMPT_CACHE_ENABLED', 'PROMPT_CACHE_MAX_ENTRIES', 'PROMPT_CACHE_TTL_MS',
|
|
563
|
+
'SEMANTIC_CACHE_ENABLED', 'SEMANTIC_CACHE_THRESHOLD', 'SEMANTIC_CACHE_MAX_ENTRIES', 'SEMANTIC_CACHE_TTL_MS',
|
|
564
|
+
]);
|
|
565
|
+
const MEMORY_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('MEMORY_') || k === 'TOKEN_TRACKING_ENABLED' || k === 'TOOL_TRUNCATION_ENABLED'));
|
|
566
|
+
const SHAPING_KEYS = new Set([
|
|
567
|
+
'SYSTEM_PROMPT_MODE', 'TOOL_DESCRIPTIONS',
|
|
568
|
+
'HISTORY_COMPRESSION_ENABLED', 'HISTORY_KEEP_RECENT_TURNS', 'HISTORY_SUMMARIZE_OLDER',
|
|
569
|
+
'TOKEN_BUDGET_WARNING', 'TOKEN_BUDGET_MAX', 'TOKEN_BUDGET_ENFORCEMENT',
|
|
570
|
+
'CAVEMAN_ENABLED', 'CAVEMAN_LEVEL', 'MARKDOWN_RENDER_ANSI',
|
|
571
|
+
]);
|
|
572
|
+
const POLICY_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('POLICY_')));
|
|
573
|
+
const AGENT_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('AGENTS_')));
|
|
574
|
+
const RATE_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('RATE_LIMIT_')));
|
|
575
|
+
const OPS_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('HOT_RELOAD_') || k.startsWith('LOAD_SHEDDING_')));
|
|
576
|
+
const COMPRESSION_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('TOON_') || k.startsWith('HEADROOM_')));
|
|
577
|
+
const MCP_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('MCP_')));
|
|
578
|
+
const WEB_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('WEB_SEARCH_') || k.startsWith('WEB_FETCH_')));
|
|
579
|
+
const TINYFISH_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('TINYFISH_')));
|
|
580
|
+
const WORKSPACE_TEST_KEYS = new Set(Object.keys(merged).filter((k) => k.startsWith('WORKSPACE_TEST_')));
|
|
581
|
+
|
|
582
|
+
const groups = [
|
|
583
|
+
{ heading: '# Tier routing', keys: Object.keys(merged).filter((k) => k.startsWith('TIER_') || k === 'MODEL_PROVIDER') },
|
|
584
|
+
{ heading: '# Server', keys: Object.keys(merged).filter((k) => SERVER_KEYS.has(k)) },
|
|
585
|
+
{ heading: '# Provider credentials', keys: Object.keys(merged).filter((k) =>
|
|
586
|
+
/(_API_KEY|_ENDPOINT|_API_BASE|_DEPLOYMENT|_MODEL|_ENDPOINT_PATH|_API_VERSION|_VERSION|_TIMEOUT_MS|_EMBEDDINGS_MODEL|_EMBEDDINGS_ENDPOINT|_MAX_TOOLS_FOR_ROUTING)$/.test(k) &&
|
|
587
|
+
!k.startsWith('LYNKR_') && !k.startsWith('HEADROOM_') && !k.startsWith('RATE_LIMIT_') &&
|
|
588
|
+
!k.startsWith('HOT_RELOAD_') && !k.startsWith('LOAD_SHEDDING_') && !k.startsWith('AGENTS_') &&
|
|
589
|
+
!k.startsWith('MCP_') && !k.startsWith('WEB_') && !k.startsWith('TINYFISH_') && !k.startsWith('WORKSPACE_TEST_') &&
|
|
590
|
+
!k.startsWith('NODE_') && !k.startsWith('TOON_')
|
|
591
|
+
) },
|
|
592
|
+
{ heading: '# Routing intelligence', keys: Object.keys(merged).filter((k) => k.startsWith('LYNKR_')) },
|
|
593
|
+
{ heading: '# Tool execution', keys: Object.keys(merged).filter((k) => TOOL_EXEC_KEYS.has(k)) },
|
|
594
|
+
{ heading: '# Caching', keys: Object.keys(merged).filter((k) => CACHE_KEYS.has(k)) },
|
|
595
|
+
{ heading: '# Compression & context', keys: Object.keys(merged).filter((k) => COMPRESSION_KEYS.has(k)) },
|
|
596
|
+
{ heading: '# Memory & tracking', keys: Object.keys(merged).filter((k) => MEMORY_KEYS.has(k)) },
|
|
597
|
+
{ heading: '# Prompt & output shaping', keys: Object.keys(merged).filter((k) => SHAPING_KEYS.has(k)) },
|
|
598
|
+
{ heading: '# Policy & budgets', keys: Object.keys(merged).filter((k) => POLICY_KEYS.has(k)) },
|
|
599
|
+
{ heading: '# Agents', keys: Object.keys(merged).filter((k) => AGENT_KEYS.has(k)) },
|
|
600
|
+
{ heading: '# Rate limiting', keys: Object.keys(merged).filter((k) => RATE_KEYS.has(k)) },
|
|
601
|
+
{ heading: '# MCP sandbox', keys: Object.keys(merged).filter((k) => MCP_KEYS.has(k)) },
|
|
602
|
+
{ heading: '# Web tools', keys: Object.keys(merged).filter((k) => WEB_KEYS.has(k)) },
|
|
603
|
+
{ heading: '# TinyFish (web automation)', keys: Object.keys(merged).filter((k) => TINYFISH_KEYS.has(k)) },
|
|
604
|
+
{ heading: '# Workspace test runner', keys: Object.keys(merged).filter((k) => WORKSPACE_TEST_KEYS.has(k)) },
|
|
605
|
+
{ heading: '# Ops (hot reload, load shedding)', keys: Object.keys(merged).filter((k) => OPS_KEYS.has(k)) },
|
|
606
|
+
{ heading: '# Logging', keys: ['LOG_LEVEL'].filter((k) => k in merged) },
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
const seen = new Set();
|
|
610
|
+
for (const g of groups) {
|
|
611
|
+
if (!g.keys.length) continue;
|
|
612
|
+
lines.push(g.heading);
|
|
613
|
+
for (const k of g.keys) {
|
|
614
|
+
if (seen.has(k)) continue;
|
|
615
|
+
lines.push(`${k}=${merged[k]}`);
|
|
616
|
+
seen.add(k);
|
|
617
|
+
}
|
|
618
|
+
lines.push('');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Catch-all for any other keys (e.g. _DEPLOYMENT defaults) we missed.
|
|
622
|
+
const remaining = Object.keys(merged).filter((k) => !seen.has(k));
|
|
623
|
+
if (remaining.length) {
|
|
624
|
+
lines.push('# Other');
|
|
625
|
+
for (const k of remaining) lines.push(`${k}=${merged[k]}`);
|
|
626
|
+
lines.push('');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return lines.join('\n');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function writeEnvFile(content, opts) {
|
|
633
|
+
if (opts.dryRun) {
|
|
634
|
+
process.stdout.write(content);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const target = opts.output || path.join(process.cwd(), '.env');
|
|
638
|
+
if (fs.existsSync(target) && !opts.force) {
|
|
639
|
+
console.error(`✗ ${target} already exists. Use --force to overwrite, or --output=<path>.`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
fs.writeFileSync(target, content);
|
|
643
|
+
console.log(`✓ Wrote ${target}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
647
|
+
// Entry
|
|
648
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
async function main() {
|
|
651
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
652
|
+
if (opts.help) return showHelp();
|
|
653
|
+
|
|
654
|
+
if (!process.stdin.isTTY) {
|
|
655
|
+
console.error('✗ lynkr init needs an interactive TTY.');
|
|
656
|
+
console.error(' If you need a non-interactive setup, copy .env.example to .env manually,');
|
|
657
|
+
console.error(' or run `lynkr init --dry-run` to preview the wizard prompts.');
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return runInteractive(opts);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Run when invoked directly (`node bin/lynkr-init.js`) or dispatched from
|
|
665
|
+
// cli.js (which sets _LYNKR_SUBCMD). Stay quiet when require()'d by tests.
|
|
666
|
+
if (require.main === module || process.env._LYNKR_SUBCMD === 'init') {
|
|
667
|
+
main().catch((err) => {
|
|
668
|
+
console.error(`✗ ${err.message}`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
module.exports = {
|
|
674
|
+
PROVIDERS,
|
|
675
|
+
PROVIDER_ORDER,
|
|
676
|
+
TIERS,
|
|
677
|
+
parseArgs,
|
|
678
|
+
buildEnvContent,
|
|
679
|
+
};
|
package/install.sh
CHANGED
|
@@ -128,63 +128,17 @@ install_dependencies() {
|
|
|
128
128
|
fi
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
#
|
|
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 [
|
|
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
|
|
181
|
+
echo "🚀 Quick Start:"
|
|
228
182
|
echo ""
|
|
229
|
-
echo "
|
|
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 "
|
|
233
|
-
echo "
|
|
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.
|
|
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}
|
|
243
|
-
echo "
|
|
244
|
-
echo ""
|
|
245
|
-
echo "
|
|
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
|
|
264
|
-
echo "
|
|
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.
|
|
3
|
+
"version": "9.7.1",
|
|
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",
|