lynkr 9.0.2 → 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.
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,7 +29,9 @@ ${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
37
  -h, --help Show this help message
@@ -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();
package/funding.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "$schema": "https://fundingjson.org/schema/v1.1.0.json",
3
+ "version": "v1.1.0",
4
+
5
+ "entity": {
6
+ "type": "individual",
7
+ "role": "maintainer",
8
+ "name": "Vishal Veera Reddy",
9
+ "email": "veerareddyvishal56@gmail.com",
10
+ "description": "Indian software engineer building open-source AI infrastructure. Sole maintainer of Lynkr, a self-hosted AI gateway that lets developers run any AI coding tool on any LLM provider.",
11
+ "webpageUrl": {
12
+ "url": "https://github.com/vishalveerareddy123"
13
+ }
14
+ },
15
+
16
+ "projects": [
17
+ {
18
+ "guid": "lynkr",
19
+ "name": "Lynkr",
20
+ "description": "A self-hosted AI gateway that decouples AI coding tools (Claude Code, Cursor, Codex, Cline, jcode, Pi) from their default LLM providers. Lynkr auto-detects the connecting tool, translates between Anthropic and OpenAI request formats, and routes to any of 12+ backends (Ollama, AWS Bedrock, Azure OpenAI, OpenRouter, Databricks, Moonshot, Google Vertex, llama.cpp, LM Studio, and more). A request-complexity classifier sends simple turns to free local models and complex ones to flagship cloud models, cutting per-developer AI bills 60-80% while removing vendor lock-in. Includes tool-result compression, MCP Code Mode (96% token reduction on tool definitions), persistent memory, and tier-based routing — all configured through a single .env file.",
21
+ "webpageUrl": {
22
+ "url": "https://fast-editor.github.io/Lynkr/"
23
+ },
24
+ "repositoryUrl": {
25
+ "url": "https://github.com/Fast-Editor/Lynkr"
26
+ },
27
+ "licenses": ["spdx:Apache-2.0"],
28
+ "tags": [
29
+ "ai",
30
+ "ai-gateway",
31
+ "llm",
32
+ "llm-router",
33
+ "developer-tools",
34
+ "proxy",
35
+ "claude-code",
36
+ "ollama",
37
+ "anthropic",
38
+ "openai"
39
+ ]
40
+ }
41
+ ],
42
+
43
+ "funding": {
44
+ "channels": [
45
+ {
46
+ "guid": "github-sponsors",
47
+ "type": "payment-provider",
48
+ "address": "https://github.com/sponsors/vishalveerareddy123",
49
+ "description": "Support Lynkr development via GitHub Sponsors."
50
+ },
51
+ {
52
+ "guid": "fossunited-grant",
53
+ "type": "other",
54
+ "address": "grants@fossunited.org",
55
+ "description": "FOSS United Foundation grant channel for institutional FOSS funding."
56
+ },
57
+ {
58
+ "guid": "bank-transfer",
59
+ "type": "bank",
60
+ "address": "Available on request via the project email.",
61
+ "description": "Direct bank transfer for organisations or grant disbursements."
62
+ }
63
+ ],
64
+
65
+ "plans": [
66
+ {
67
+ "guid": "core-maintenance-2026",
68
+ "status": "active",
69
+ "name": "Core maintenance + roadmap (12 months)",
70
+ "description": "Funds full-time work on Lynkr's core gateway: provider-format conversions, tier routing, tool-call translation across 10+ model formats (Minimax, Qwen, GLM, Llama, DeepSeek, Mistral), tool-result compression, persistent memory, MCP Code Mode, observability, tests, and docs. Estimated cost reflects one Indian maintainer working full-time for a year.",
71
+ "amount": 500000,
72
+ "currency": "INR",
73
+ "frequency": "yearly",
74
+ "channels": ["fossunited-grant", "bank-transfer"]
75
+ },
76
+ {
77
+ "guid": "infra-2026",
78
+ "status": "active",
79
+ "name": "Infrastructure + benchmarks",
80
+ "description": "Funds CI runners, benchmark harness for cost/quality/latency comparisons across providers, public dashboard at lynkr.dev, and self-hosted SearXNG + telemetry mirrors used by Lynkr's web search and routing layers.",
81
+ "amount": 150000,
82
+ "currency": "INR",
83
+ "frequency": "yearly",
84
+ "channels": ["fossunited-grant", "bank-transfer"]
85
+ },
86
+ {
87
+ "guid": "community-sponsor",
88
+ "status": "active",
89
+ "name": "Community sponsorship",
90
+ "description": "Recurring small-amount sponsorship from individual developers and small teams who use Lynkr.",
91
+ "amount": 0,
92
+ "currency": "USD",
93
+ "frequency": "monthly",
94
+ "channels": ["github-sponsors"]
95
+ },
96
+ {
97
+ "guid": "one-time",
98
+ "status": "active",
99
+ "name": "One-time contribution",
100
+ "description": "Any-amount one-time contribution from users or supporters.",
101
+ "amount": 0,
102
+ "currency": "USD",
103
+ "frequency": "one-time",
104
+ "channels": ["github-sponsors", "bank-transfer"]
105
+ }
106
+ ],
107
+
108
+ "history": []
109
+ }
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lynkr",
3
- "version": "9.0.2",
3
+ "version": "9.1.2",
4
4
  "description": "Self-hosted Claude Code & Cursor proxy with Databricks,AWS BedRock,Azure adapters, openrouter, Ollama,llamacpp,LM Studio, workspace tooling, and MCP integration.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "dev": "nodemon index.js",
15
15
  "lint": "eslint src index.js",
16
16
  "test": "npm run test:unit && npm run test:performance",
17
- "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",
17
+ "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",
18
18
  "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",
19
19
  "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",
20
20
  "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",