lynkr 9.0.1 → 9.1.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.
Files changed (58) hide show
  1. package/README.md +70 -21
  2. package/bin/cli.js +34 -4
  3. package/bin/lynkr-trajectory.js +136 -0
  4. package/bin/lynkr-usage.js +219 -0
  5. package/funding.json +110 -0
  6. package/index.js +7 -3
  7. package/install.sh +3 -3
  8. package/lynkr-skill.tar.gz +0 -0
  9. package/native/Cargo.toml +26 -0
  10. package/native/index.js +29 -0
  11. package/native/lynkr-native.node +0 -0
  12. package/native/src/lib.rs +321 -0
  13. package/package.json +6 -5
  14. package/public/dashboard.html +665 -0
  15. package/src/api/files-multipart.js +30 -0
  16. package/src/api/files-router.js +81 -0
  17. package/src/api/middleware/budget.js +19 -1
  18. package/src/api/middleware/load-shedding.js +17 -0
  19. package/src/api/openai-router.js +353 -301
  20. package/src/api/router.js +275 -40
  21. package/src/cache/prompt.js +13 -0
  22. package/src/clients/databricks.js +42 -18
  23. package/src/clients/ollama-utils.js +21 -17
  24. package/src/clients/openai-format.js +50 -10
  25. package/src/clients/openrouter-utils.js +42 -37
  26. package/src/clients/prompt-cache-injection.js +140 -0
  27. package/src/clients/provider-capabilities.js +41 -0
  28. package/src/clients/responses-format.js +8 -7
  29. package/src/clients/standard-tools.js +1 -1
  30. package/src/clients/xml-tool-extractor.js +307 -0
  31. package/src/cluster.js +82 -0
  32. package/src/config/index.js +16 -0
  33. package/src/context/distill.js +15 -0
  34. package/src/context/tool-result-compressor.js +563 -0
  35. package/src/dashboard/api.js +170 -0
  36. package/src/dashboard/router.js +13 -0
  37. package/src/headroom/client.js +3 -109
  38. package/src/headroom/index.js +0 -14
  39. package/src/memory/extractor.js +22 -0
  40. package/src/memory/search.js +0 -50
  41. package/src/orchestrator/index.js +163 -204
  42. package/src/orchestrator/preflight.js +188 -0
  43. package/src/routing/index.js +64 -32
  44. package/src/routing/interaction.js +183 -0
  45. package/src/routing/risk-analyzer.js +194 -0
  46. package/src/routing/telemetry.js +47 -2
  47. package/src/server.js +15 -0
  48. package/src/stores/file-store.js +104 -0
  49. package/src/stores/response-store.js +25 -0
  50. package/src/tools/index.js +1 -1
  51. package/src/tools/smart-selection.js +11 -2
  52. package/src/tools/web.js +1 -1
  53. package/src/training/trajectory-compressor.js +266 -0
  54. package/src/usage/aggregator.js +206 -0
  55. package/src/utils/markdown-ansi.js +146 -0
  56. package/.lynkr/telemetry.db +0 -0
  57. package/.lynkr/telemetry.db-shm +0 -0
  58. package/.lynkr/telemetry.db-wal +0 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ### Run Claude Code, Cursor, and Codex on any model. One proxy, every provider.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/lynkr.svg)](https://www.npmjs.com/package/lynkr)
6
- [![Tests](https://img.shields.io/badge/tests-652%20passing-brightgreen)](https://github.com/vishalveerareddy123/Lynkr)
6
+ [![Tests](https://img.shields.io/badge/tests-699%20passing-brightgreen)](https://github.com/Fast-Editor/Lynkr)
7
7
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
8
8
  [![Node.js](https://img.shields.io/badge/node-20%2B-green)](https://nodejs.org)
9
9
  [![Homebrew Tap](https://img.shields.io/badge/homebrew-lynkr-brightgreen.svg)](https://github.com/vishalveerareddy123/homebrew-lynkr)
@@ -11,9 +11,9 @@
11
11
 
12
12
  <table>
13
13
  <tr>
14
- <td align="center"><strong>10+</strong><br/>LLM Providers</td>
14
+ <td align="center"><strong>12+</strong><br/>LLM Providers</td>
15
15
  <td align="center"><strong>60-80%</strong><br/>Cost Reduction</td>
16
- <td align="center"><strong>652</strong><br/>Tests Passing</td>
16
+ <td align="center"><strong>699</strong><br/>Tests Passing</td>
17
17
  <td align="center"><strong>0</strong><br/>Code Changes Required</td>
18
18
  </tr>
19
19
  </table>
@@ -55,6 +55,12 @@ lynkr start
55
55
 
56
56
  ### Install
57
57
 
58
+ **One-line install (recommended):**
59
+ ```bash
60
+ curl -fsSL https://raw.githubusercontent.com/Fast-Editor/Lynkr/main/install.sh | bash
61
+ ```
62
+
63
+ **Or via npm:**
58
64
  ```bash
59
65
  npm install -g pino-pretty && npm install -g lynkr
60
66
  ```
@@ -125,7 +131,24 @@ const { text } = await generateText({
125
131
  });
126
132
  ```
127
133
 
128
- > Works with any OpenAI-compatible client: Cline, Continue.dev, ClawdBot, KiloCode, and more.
134
+ **OpenClaw**
135
+ ```json
136
+ // openclaw.json
137
+ {
138
+ "models": {
139
+ "providers": [{
140
+ "name": "lynkr",
141
+ "type": "openai-compatible",
142
+ "base_url": "http://localhost:8081/v1",
143
+ "api_key": "any-value",
144
+ "models": ["auto"]
145
+ }]
146
+ }
147
+ }
148
+ ```
149
+ Set `OPENCLAW_MODE=true` in Lynkr's `.env` to show actual provider/model in responses.
150
+
151
+ > Works with any OpenAI-compatible client: Cline, Continue.dev, OpenClaw, KiloCode, and more.
129
152
 
130
153
  ---
131
154
 
@@ -139,12 +162,16 @@ const { text } = await generateText({
139
162
  | **MLX Server** | Local | Apple Silicon optimized | **Free** |
140
163
  | **AWS Bedrock** | Cloud | 100+ (Claude, Llama, Mistral, Titan) | $$ |
141
164
  | **OpenRouter** | Cloud | 100+ (GPT, Claude, Llama, Gemini) | $-$$ |
142
- | **Databricks** | Cloud | Claude Sonnet 4.5, Opus 4.5 | $$$ |
143
- | **Azure OpenAI** | Cloud | GPT-4o, GPT-5, o1, o3 | $$$ |
165
+ | **Databricks** | Cloud | Claude Sonnet 4.5, Opus 4.6 | $$$ |
166
+ | **Azure OpenAI** | Cloud | GPT-4o, o1, o3 | $$$ |
144
167
  | **Azure Anthropic** | Cloud | Claude models | $$$ |
145
- | **OpenAI** | Cloud | GPT-4o, o1, o3 | $$$ |
168
+ | **OpenAI** | Cloud | GPT-4o, o3, o4-mini | $$$ |
169
+ | **Google Vertex** | Cloud | Gemini 2.5 Pro/Flash | $$$ |
170
+ | **Moonshot AI** | Cloud | Kimi K2 Thinking/Turbo | $$ |
171
+ | **Z.AI** | Cloud | GLM-4.7 | $$ |
172
+ | **DeepSeek** | Cloud | DeepSeek Reasoner, R1 | $ |
146
173
 
147
- 4 local providers for **100% offline, free** usage. 6+ cloud providers for scale.
174
+ 4 local providers for **100% offline, free** usage. 10+ cloud providers for scale.
148
175
 
149
176
  ---
150
177
 
@@ -166,6 +193,9 @@ const { text } = await generateText({
166
193
  | **Transaction fees** | None | None (OSS) / Paid enterprise | 5.5% on credits | Free tier / Paid |
167
194
  | **Dependencies** | Node.js only | Python, Prisma, PostgreSQL | N/A | Docker, Python |
168
195
  | **Format conversion** | Anthropic <-> OpenAI (automatic) | Automatic | N/A | Automatic |
196
+ | **Code intelligence** | Graphify (19-lang AST graph) | No | No | No |
197
+ | **Routing telemetry** | Built-in (SQLite + REST API) | No | Dashboard | Dashboard |
198
+ | **Admin hot-reload** | Yes (no restart) | Requires restart | N/A | Requires restart |
169
199
  | **License** | Apache 2.0 | MIT | Proprietary | MIT (gateway) |
170
200
 
171
201
  **Lynkr's edge:** Purpose-built for AI coding tools. Not a general LLM gateway — a proxy that understands Claude Code, Cursor, and Codex natively, with built-in token optimization, complexity-based routing, and a memory system designed for coding workflows. Installs in one command, runs on Node.js, zero infrastructure required.
@@ -188,20 +218,29 @@ const { text } = await generateText({
188
218
 
189
219
  Lynkr isn't just a passthrough proxy. It's an optimization layer.
190
220
 
191
- ### Smart Routing
192
- Routes requests to the right model based on task complexity. Simple questions go to fast/cheap models. Complex architectural tasks go to powerful models. You configure the tiers.
221
+ ### Smart Routing (5-Phase)
222
+ Routes requests to the right model based on 5-phase complexity analysis. Simple questions go to fast/cheap models. Complex architectural tasks go to powerful models. Includes Graphify structural analysis for code-aware routing.
223
+
224
+ - **Complexity scoring** — 15-dimension weighted scoring with agentic workflow detection
225
+ - **Graphify integration** — AST-based knowledge graph detects god nodes, community cohesion, blast radius across 19 languages
226
+ - **Routing telemetry** — every decision recorded with quality scoring (0-100) and latency tracking (P50/P95/P99)
193
227
 
194
- ### Token Optimization
228
+ ### Token Optimization (7 Phases)
195
229
  - **Smart tool selection** — only sends tools relevant to the current task
196
- - **Prompt compression** — removes redundant context before sending
230
+ - **Code Mode** — replaces 100+ MCP tools with 4 meta-tools (~96% token reduction)
231
+ - **Distill compression** — structural similarity, delta rendering, smart dedup of repetitive tool outputs
232
+ - **Prompt caching** — SHA-256 keyed LRU cache
197
233
  - **Memory deduplication** — eliminates repeated information across turns
198
- - **TOON format** — compact serialization that cuts token count
234
+ - **History compression** — sliding window with Distill-powered structural dedup
235
+ - **Headroom sidecar** — optional 47-92% ML-based compression (Smart Crusher, CCR, LLMLingua)
199
236
 
200
237
  ### Enterprise Resilience
201
- - **Circuit breakers** — automatic failover when a provider goes down
238
+ - **Circuit breakers** — automatic failover with half-open probe recovery
239
+ - **Admin hot-reload** — `POST /v1/admin/reload` reloads config + resets circuit breakers without restart
202
240
  - **Load shedding** — graceful degradation under high load
203
241
  - **Prometheus metrics** — full observability at `/metrics`
204
242
  - **Health checks** — K8s-ready endpoints at `/health`
243
+ - **Performance timer** — per-request timing breakdown with `PERF_TIMER=true`
205
244
 
206
245
  ### Memory System
207
246
  Titans-inspired long-term memory with surprise-based filtering. The system remembers important context across sessions and forgets noise — reducing token waste from repeated context.
@@ -214,14 +253,23 @@ SEMANTIC_CACHE_ENABLED=true
214
253
  SEMANTIC_CACHE_THRESHOLD=0.95
215
254
  ```
216
255
 
217
- ### MCP Integration
218
- Automatic Model Context Protocol server discovery and orchestration. Your MCP tools work through Lynkr without configuration.
256
+ ### MCP Integration + Code Mode
257
+ Automatic Model Context Protocol server discovery and orchestration. Your MCP tools work through Lynkr without configuration. Enable Code Mode to replace 100+ MCP tool definitions with 4 lightweight meta-tools:
258
+
259
+ ```bash
260
+ CODE_MODE_ENABLED=true # ~96% reduction in tool-catalog tokens
261
+ ```
219
262
 
220
263
  ---
221
264
 
222
265
  ## Deployment Options
223
266
 
224
- **NPM (recommended)**
267
+ **One-line install (recommended)**
268
+ ```bash
269
+ curl -fsSL https://raw.githubusercontent.com/Fast-Editor/Lynkr/main/install.sh | bash
270
+ ```
271
+
272
+ **NPM**
225
273
  ```bash
226
274
  npm install -g lynkr && lynkr start
227
275
  ```
@@ -233,7 +281,7 @@ docker-compose up -d
233
281
 
234
282
  **Git Clone**
235
283
  ```bash
236
- git clone https://github.com/vishalveerareddy123/Lynkr.git
284
+ git clone https://github.com/Fast-Editor/Lynkr.git
237
285
  cd Lynkr && npm install && cp .env.example .env
238
286
  npm start
239
287
  ```
@@ -251,9 +299,10 @@ brew install lynkr
251
299
  | Guide | Description |
252
300
  |-------|-------------|
253
301
  | [Installation](documentation/installation.md) | All installation methods |
254
- | [Provider Config](documentation/providers.md) | Setup for all 10+ providers |
302
+ | [Provider Config](documentation/providers.md) | Setup for all 12+ providers |
255
303
  | [Claude Code CLI](documentation/claude-code-cli.md) | Detailed Claude Code integration |
256
304
  | [Codex CLI](documentation/codex-cli.md) | Codex config.toml setup |
305
+ | [OpenClaw](documentation/openclaw-integration.md) | OpenClaw integration with tier routing |
257
306
  | [Cursor IDE](documentation/cursor-integration.md) | Cursor integration + troubleshooting |
258
307
  | [Embeddings](documentation/embeddings.md) | @Codebase semantic search (4 options) |
259
308
  | [Token Optimization](documentation/token-optimization.md) | 60-80% cost reduction strategies |
@@ -293,8 +342,8 @@ Apache 2.0 — See [LICENSE](LICENSE).
293
342
 
294
343
  ## Community
295
344
 
296
- - [GitHub Discussions](https://github.com/vishalveerareddy123/Lynkr/discussions) — Questions and tips
297
- - [Report Issues](https://github.com/vishalveerareddy123/Lynkr/issues) — Bug reports and feature requests
345
+ - [GitHub Discussions](https://github.com/Fast-Editor/Lynkr/discussions) — Questions and tips
346
+ - [Report Issues](https://github.com/Fast-Editor/Lynkr/issues) — Bug reports and feature requests
298
347
  - [NPM Package](https://www.npmjs.com/package/lynkr) — Official package
299
348
  - [DeepWiki](https://deepwiki.com/vishalveerareddy123/Lynkr) — AI-powered docs search
300
349
 
package/bin/cli.js CHANGED
@@ -1,7 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const path = require("path");
3
4
  const pkg = require('../package.json');
4
5
 
6
+ // Subcommands. Dispatched before server boot so `lynkr usage` / `lynkr trajectory`
7
+ // don't start the proxy. Add new subcommands here, not in scattered binaries.
8
+ const SUBCOMMANDS = {
9
+ usage: path.join(__dirname, "lynkr-usage.js"),
10
+ trajectory: path.join(__dirname, "lynkr-trajectory.js"),
11
+ };
12
+
13
+ const sub = process.argv[2];
14
+ if (sub && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, sub)) {
15
+ process.argv.splice(2, 1); // drop the subcommand token so the script's own arg parser is happy
16
+ require(SUBCOMMANDS[sub]);
17
+ return;
18
+ }
19
+
5
20
  if (process.argv.includes('--version') || process.argv.includes('-v')) {
6
21
  console.log(pkg.version);
7
22
  process.exit(0);
@@ -14,14 +29,20 @@ ${pkg.name} v${pkg.version}
14
29
  ${pkg.description}
15
30
 
16
31
  Usage:
17
- lynkr [options]
32
+ lynkr [options] Start the proxy server (default)
33
+ lynkr usage [options] Show AI spend report and tier-routing savings
34
+ lynkr trajectory [options] Export agent trajectories as JSONL training data
18
35
 
19
36
  Options:
20
- -h, --help Show this help message
21
- -v, --version Show version number
37
+ -h, --help Show this help message
38
+ -v, --version Show version number
39
+ --cluster Enable cluster mode (multi-core)
40
+ --workers N Number of worker processes (default: auto)
22
41
 
23
42
  Environment Variables:
24
- See .env.example for configuration options
43
+ CLUSTER_ENABLED=true Enable multi-core cluster mode
44
+ CLUSTER_WORKERS=auto Worker count (auto = CPU cores - 1)
45
+ See .env.example for all configuration options
25
46
 
26
47
  Documentation:
27
48
  ${pkg.homepage}
@@ -29,4 +50,13 @@ Documentation:
29
50
  process.exit(0);
30
51
  }
31
52
 
53
+ // CLI flags for cluster mode
54
+ if (process.argv.includes('--cluster')) {
55
+ process.env.CLUSTER_ENABLED = 'true';
56
+ }
57
+ const workersIdx = process.argv.indexOf('--workers');
58
+ if (workersIdx !== -1 && process.argv[workersIdx + 1]) {
59
+ process.env.CLUSTER_WORKERS = process.argv[workersIdx + 1];
60
+ }
61
+
32
62
  require("../index.js");
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ /**
4
+ * lynkr trajectory — export agent trajectories from the session DB
5
+ * as JSONL training data.
6
+ *
7
+ * Usage:
8
+ * lynkr trajectory # stdout, last 30 days
9
+ * lynkr trajectory --since 7d # last 7 days
10
+ * lynkr trajectory --output trajectories.jsonl # write to file
11
+ * lynkr trajectory --tier COMPLEX # only complex sessions
12
+ * lynkr trajectory --anonymize # strip PII / paths / secrets
13
+ * lynkr trajectory --count # just print the row count
14
+ */
15
+
16
+ const path = require("path");
17
+
18
+ process.env.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, "..");
19
+
20
+ const compressor = require("../src/training/trajectory-compressor");
21
+
22
+ function parseArgs(argv) {
23
+ const opts = { since: "30d", anonymize: false, output: "-", count: false };
24
+ for (let i = 2; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ const next = argv[i + 1];
27
+ if (a === "--since" && next) {
28
+ opts.since = next;
29
+ i++;
30
+ } else if (a === "--days" && next) {
31
+ opts.since = `${parseInt(next, 10)}d`;
32
+ i++;
33
+ } else if (a === "--tier" && next) {
34
+ opts.tier = next.toUpperCase();
35
+ i++;
36
+ } else if (a === "--output" && next) {
37
+ opts.output = next;
38
+ i++;
39
+ } else if (a === "-o" && next) {
40
+ opts.output = next;
41
+ i++;
42
+ } else if (a === "--anonymize" || a === "--anonymise") {
43
+ opts.anonymize = true;
44
+ } else if (a === "--count") {
45
+ opts.count = true;
46
+ } else if (a === "--help" || a === "-h") {
47
+ printHelp();
48
+ process.exit(0);
49
+ } else if (a === "--format" && next) {
50
+ // Reserved for future formats. Only "jsonl" is supported today.
51
+ if (next !== "jsonl") {
52
+ console.error(`Unsupported --format: ${next}. Only 'jsonl' is supported.`);
53
+ process.exit(2);
54
+ }
55
+ i++;
56
+ }
57
+ }
58
+ return opts;
59
+ }
60
+
61
+ function printHelp() {
62
+ console.log(`Lynkr trajectory exporter — emit JSONL training samples from session history.
63
+
64
+ Usage:
65
+ lynkr trajectory [options]
66
+
67
+ Options:
68
+ --since <window> "7d", "30d", ISO date, or epoch ms (default: 30d)
69
+ --days N Shorthand for --since Nd
70
+ --tier <tier> Filter to one tier: SIMPLE, MEDIUM, COMPLEX, REASONING
71
+ --output, -o <path> Output file (default: stdout, "-")
72
+ --anonymize Strip PII, file paths, API keys, hostnames
73
+ --count Print only the row count, no output
74
+ --format jsonl Output format (only jsonl supported)
75
+ -h, --help Show this help
76
+
77
+ Examples:
78
+ lynkr trajectory --days 7 --output last-week.jsonl
79
+ lynkr trajectory --tier COMPLEX --anonymize -o complex-anon.jsonl
80
+ lynkr trajectory --count
81
+
82
+ Output format (one JSON object per line):
83
+ {
84
+ "session_id": "...",
85
+ "messages": [{"role": "user", "content": "..."}, ...],
86
+ "tool_calls": [...],
87
+ "outcome": "success" | "error",
88
+ "tier": "MEDIUM",
89
+ "complexity_score": 38,
90
+ "model_used": "gpt-4o",
91
+ "provider_used": "azure-openai",
92
+ "tokens_in": 1234,
93
+ "tokens_out": 456,
94
+ "latency_ms": 2400,
95
+ "started_at": "...",
96
+ "ended_at": "..."
97
+ }
98
+ `);
99
+ }
100
+
101
+ function fmtInt(n) {
102
+ return new Intl.NumberFormat("en-US").format(n || 0);
103
+ }
104
+
105
+ function main() {
106
+ const opts = parseArgs(process.argv);
107
+
108
+ if (opts.count) {
109
+ // Quick path — stream-walk the sessions and just count valid trajectories.
110
+ let count = 0;
111
+ compressor.exportJsonl({
112
+ ...opts,
113
+ output: { write: () => count++, end: () => {} },
114
+ });
115
+ console.log(`${fmtInt(count)} trajectories`);
116
+ return;
117
+ }
118
+
119
+ const isStdout = opts.output === "-";
120
+ const start = Date.now();
121
+ const result = compressor.exportJsonl({
122
+ since: opts.since,
123
+ tier: opts.tier,
124
+ anonymize: opts.anonymize,
125
+ output: opts.output,
126
+ });
127
+
128
+ if (!isStdout) {
129
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
130
+ process.stderr.write(
131
+ `Exported ${fmtInt(result.count)} trajectories to ${result.output} in ${elapsed}s\n`
132
+ );
133
+ }
134
+ }
135
+
136
+ main();
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ /**
4
+ * lynkr usage — print AI spend report from routing telemetry.
5
+ *
6
+ * Usage:
7
+ * lynkr-usage # last 30 days
8
+ * lynkr-usage --days 7
9
+ * lynkr-usage --window 1d
10
+ * lynkr-usage --window all
11
+ * lynkr-usage --json # machine-readable
12
+ * lynkr-usage --flagship gpt-5 # alternative comparison model
13
+ * lynkr-usage --provider moonshot # filter to one provider
14
+ */
15
+
16
+ const path = require("path");
17
+
18
+ // Make sure config/logger pick up the workspace root
19
+ process.env.WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || path.resolve(__dirname, "..");
20
+
21
+ const aggregator = require("../src/usage/aggregator");
22
+
23
+ function parseArgs(argv) {
24
+ const opts = { window: "30d", json: false };
25
+ for (let i = 2; i < argv.length; i++) {
26
+ const a = argv[i];
27
+ const next = argv[i + 1];
28
+ if (a === "--json") opts.json = true;
29
+ else if (a === "--days" && next) {
30
+ opts.window = `${parseInt(next, 10)}d`;
31
+ i++;
32
+ } else if (a === "--window" && next) {
33
+ opts.window = next;
34
+ i++;
35
+ } else if (a === "--since" && next) {
36
+ opts.window = next;
37
+ i++;
38
+ } else if (a === "--flagship" && next) {
39
+ opts.flagship = next;
40
+ i++;
41
+ } else if (a === "--provider" && next) {
42
+ opts.provider = next;
43
+ i++;
44
+ } else if (a === "--model" && next) {
45
+ opts.model = next;
46
+ i++;
47
+ } else if (a === "--help" || a === "-h") {
48
+ printHelp();
49
+ process.exit(0);
50
+ }
51
+ }
52
+ return opts;
53
+ }
54
+
55
+ function printHelp() {
56
+ console.log(`Lynkr usage report — show AI spend and tier-routing savings.
57
+
58
+ Usage:
59
+ lynkr usage [options]
60
+
61
+ Options:
62
+ --days N Window in days (e.g. --days 7)
63
+ --window <preset> Window preset: 1d, 7d, 30d, all (default: 30d)
64
+ --since <iso> Custom start time (ISO 8601 or epoch ms)
65
+ --flagship <model> Comparison model for "savings" math (default: claude-sonnet-4-5-20250929)
66
+ --provider <name> Filter to a single provider
67
+ --model <id> Filter to a single model
68
+ --json Print as JSON instead of a formatted table
69
+ -h, --help Show this help
70
+
71
+ Examples:
72
+ lynkr usage
73
+ lynkr usage --days 7
74
+ lynkr usage --window all --json
75
+ `);
76
+ }
77
+
78
+ const C = {
79
+ reset: "\x1b[0m",
80
+ dim: "\x1b[2m",
81
+ bold: "\x1b[1m",
82
+ green: "\x1b[32m",
83
+ yellow: "\x1b[33m",
84
+ cyan: "\x1b[36m",
85
+ red: "\x1b[31m",
86
+ gray: "\x1b[90m",
87
+ };
88
+
89
+ function colour(text, code) {
90
+ if (!process.stdout.isTTY) return text;
91
+ return `${code}${text}${C.reset}`;
92
+ }
93
+
94
+ function fmtUSD(n) {
95
+ if (!n) return "$0.00";
96
+ if (n < 0.01) return `$${n.toFixed(4)}`;
97
+ return `$${n.toFixed(2)}`;
98
+ }
99
+
100
+ function fmtTokens(n) {
101
+ if (!n) return "0";
102
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
103
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
104
+ return String(n);
105
+ }
106
+
107
+ function fmtInt(n) {
108
+ return new Intl.NumberFormat("en-US").format(n || 0);
109
+ }
110
+
111
+ function pad(s, width, align = "left") {
112
+ s = String(s);
113
+ if (s.length >= width) return s;
114
+ const filler = " ".repeat(width - visibleLength(s));
115
+ return align === "right" ? filler + s : s + filler;
116
+ }
117
+
118
+ function visibleLength(s) {
119
+ // strip ANSI for column-width math
120
+ return String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
121
+ }
122
+
123
+ function tableRow(cells, widths, aligns) {
124
+ return cells
125
+ .map((c, i) => pad(c, widths[i], aligns[i] || "left"))
126
+ .join(" ");
127
+ }
128
+
129
+ function printTable(rows, header, widths, aligns) {
130
+ console.log(colour(tableRow(header, widths, aligns), C.bold));
131
+ console.log(colour(widths.map((w) => "─".repeat(w)).join(" "), C.dim));
132
+ for (const row of rows) {
133
+ console.log(tableRow(row, widths, aligns));
134
+ }
135
+ }
136
+
137
+ function bucketRows(bucket, widths) {
138
+ return Object.entries(bucket)
139
+ .sort((a, b) => b[1].actualCost - a[1].actualCost)
140
+ .map(([key, b]) => [
141
+ key,
142
+ fmtInt(b.requests),
143
+ fmtTokens(b.totalTokens),
144
+ colour(fmtUSD(b.actualCost), C.cyan),
145
+ colour(fmtUSD(b.flagshipCost), C.gray),
146
+ colour(fmtUSD(b.saved), C.green),
147
+ colour(`${b.savedPercent.toFixed(1)}%`, C.green),
148
+ ]);
149
+ }
150
+
151
+ function printReport(usage) {
152
+ const { window, since, flagship, totals, byTier, byProvider, byModel } = usage;
153
+
154
+ const banner = `Lynkr — Usage Report`;
155
+ console.log("");
156
+ console.log(colour(banner, C.bold));
157
+ console.log(
158
+ colour(
159
+ `window: ${window}${since ? ` since: ${since}` : ""} flagship-comparison: ${flagship}`,
160
+ C.dim
161
+ )
162
+ );
163
+ console.log("");
164
+
165
+ // Summary line
166
+ const headline =
167
+ `${fmtInt(totals.requests)} requests ` +
168
+ `${fmtTokens(totals.totalTokens)} tokens ` +
169
+ `actual ${colour(fmtUSD(totals.actualCost), C.cyan)} ` +
170
+ `flagship-only ${colour(fmtUSD(totals.flagshipCost), C.gray)} ` +
171
+ `saved ${colour(fmtUSD(totals.saved), C.green)} ` +
172
+ colour(`(${totals.savedPercent.toFixed(1)}%)`, C.green);
173
+ console.log(headline);
174
+ if (totals.fallbacks || totals.errors) {
175
+ console.log(
176
+ colour(
177
+ ` ${totals.fallbacks} fallback${totals.fallbacks !== 1 ? "s" : ""}, ` +
178
+ `${totals.errors} error${totals.errors !== 1 ? "s" : ""}`,
179
+ C.yellow
180
+ )
181
+ );
182
+ }
183
+ console.log("");
184
+
185
+ if (totals.requests === 0) {
186
+ console.log(colour("No telemetry yet for this window. Send some requests through Lynkr first.", C.yellow));
187
+ return;
188
+ }
189
+
190
+ const headers = ["", "REQUESTS", "TOKENS", "ACTUAL", "FLAGSHIP", "SAVED", "PCT"];
191
+ const widths = [22, 9, 9, 10, 10, 10, 7];
192
+ const aligns = ["left", "right", "right", "right", "right", "right", "right"];
193
+
194
+ console.log(colour("BY TIER", C.bold));
195
+ printTable(bucketRows(byTier, widths), ["TIER", ...headers.slice(1)], widths, aligns);
196
+ console.log("");
197
+
198
+ console.log(colour("BY PROVIDER", C.bold));
199
+ printTable(bucketRows(byProvider, widths), ["PROVIDER", ...headers.slice(1)], widths, aligns);
200
+ console.log("");
201
+
202
+ console.log(colour("BY MODEL", C.bold));
203
+ printTable(bucketRows(byModel, widths), ["MODEL", ...headers.slice(1)], widths, aligns);
204
+ console.log("");
205
+ }
206
+
207
+ function main() {
208
+ const opts = parseArgs(process.argv);
209
+ const usage = aggregator.getUsage(opts);
210
+
211
+ if (opts.json) {
212
+ process.stdout.write(JSON.stringify(usage, null, 2) + "\n");
213
+ return;
214
+ }
215
+
216
+ printReport(usage);
217
+ }
218
+
219
+ main();