lynkr 9.9.1 → 9.10.0

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 (67) hide show
  1. package/README.md +50 -9
  2. package/bin/cli.js +2 -0
  3. package/bin/lynkr-init.js +4 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +2 -2
  7. package/scripts/validate-difficulty-classifier.js +27 -6
  8. package/src/api/providers-handler.js +0 -1
  9. package/src/api/router.js +275 -160
  10. package/src/clients/databricks.js +95 -6
  11. package/src/config/index.js +3 -43
  12. package/src/orchestrator/index.js +117 -1235
  13. package/src/orchestrator/passthrough-stream.js +382 -0
  14. package/src/orchestrator/sse-transformer.js +408 -0
  15. package/src/routing/affinity-store.js +17 -3
  16. package/src/routing/difficulty-classifier.js +52 -10
  17. package/src/routing/index.js +35 -3
  18. package/src/routing/intent-score.js +20 -2
  19. package/src/routing/session-affinity.js +8 -1
  20. package/src/routing/side-channel-detector.js +103 -0
  21. package/src/server.js +0 -46
  22. package/src/agents/context-manager.js +0 -236
  23. package/src/agents/decomposition/dispatcher.js +0 -185
  24. package/src/agents/decomposition/gate.js +0 -136
  25. package/src/agents/decomposition/index.js +0 -183
  26. package/src/agents/decomposition/model-call.js +0 -75
  27. package/src/agents/decomposition/planner.js +0 -223
  28. package/src/agents/decomposition/synthesizer.js +0 -89
  29. package/src/agents/decomposition/telemetry.js +0 -55
  30. package/src/agents/definitions/loader.js +0 -653
  31. package/src/agents/executor.js +0 -457
  32. package/src/agents/index.js +0 -165
  33. package/src/agents/parallel-coordinator.js +0 -68
  34. package/src/agents/reflector.js +0 -331
  35. package/src/agents/skillbook.js +0 -331
  36. package/src/agents/store.js +0 -259
  37. package/src/edits/index.js +0 -171
  38. package/src/indexer/babel-parser.js +0 -213
  39. package/src/indexer/index.js +0 -1629
  40. package/src/indexer/navigation/index.js +0 -32
  41. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  42. package/src/indexer/parser.js +0 -443
  43. package/src/tasks/store.js +0 -349
  44. package/src/tests/coverage.js +0 -173
  45. package/src/tests/index.js +0 -171
  46. package/src/tests/store.js +0 -213
  47. package/src/tools/agent-task.js +0 -145
  48. package/src/tools/code-mode.js +0 -304
  49. package/src/tools/decompose.js +0 -91
  50. package/src/tools/edits.js +0 -94
  51. package/src/tools/execution.js +0 -171
  52. package/src/tools/git.js +0 -1346
  53. package/src/tools/index.js +0 -306
  54. package/src/tools/indexer.js +0 -360
  55. package/src/tools/lazy-loader.js +0 -366
  56. package/src/tools/mcp-remote.js +0 -88
  57. package/src/tools/mcp.js +0 -116
  58. package/src/tools/process.js +0 -167
  59. package/src/tools/smart-selection.js +0 -180
  60. package/src/tools/stubs.js +0 -55
  61. package/src/tools/tasks.js +0 -260
  62. package/src/tools/tests.js +0 -132
  63. package/src/tools/tinyfish.js +0 -358
  64. package/src/tools/truncate.js +0 -106
  65. package/src/tools/web-client.js +0 -71
  66. package/src/tools/web.js +0 -415
  67. package/src/tools/workspace.js +0 -204
@@ -1,132 +0,0 @@
1
- const { registerTool } = require(".");
2
- const { runWorkspaceTests, listTestRuns, getTestSummary } = require("../tests");
3
-
4
- function formatJson(payload) {
5
- return JSON.stringify(payload, null, 2);
6
- }
7
-
8
- function parseBoolean(value) {
9
- if (typeof value === "boolean") return value;
10
- if (typeof value === "string") {
11
- const normalised = value.trim().toLowerCase();
12
- if (normalised === "true" || normalised === "1" || normalised === "yes") return true;
13
- if (normalised === "false" || normalised === "0" || normalised === "no") return false;
14
- }
15
- return undefined;
16
- }
17
-
18
- function parseNumber(value, fallback) {
19
- if (value === undefined || value === null) return fallback;
20
- const parsed = Number(value);
21
- return Number.isFinite(parsed) ? parsed : fallback;
22
- }
23
-
24
- function parseArgs(args) {
25
- if (!args) return [];
26
- if (Array.isArray(args)) return args.map(String);
27
- if (typeof args === "string" && args.trim().length > 0) {
28
- return args
29
- .split(/\s+/)
30
- .map((item) => item.trim())
31
- .filter(Boolean);
32
- }
33
- return [];
34
- }
35
-
36
- function registerTestRunTool() {
37
- registerTool(
38
- "workspace_test_run",
39
- async ({ args = {} }) => {
40
- const profile = args.profile ?? args.name ?? null;
41
- const collectCoverage = parseBoolean(args.collect_coverage ?? args.collectCoverage);
42
- const sandbox = typeof args.sandbox === "string" ? args.sandbox : undefined;
43
- const sessionId = args.session_id ?? args.sessionId ?? null;
44
-
45
- const result = await runWorkspaceTests({
46
- profileName: profile,
47
- args: parseArgs(args.args ?? args.extra_args),
48
- cwd: typeof args.cwd === "string" ? args.cwd : undefined,
49
- env: typeof args.env === "object" && args.env !== null ? args.env : undefined,
50
- timeoutMs: parseNumber(args.timeout_ms ?? args.timeout, undefined),
51
- sandbox,
52
- sessionId,
53
- collectCoverage: collectCoverage !== undefined ? collectCoverage : true,
54
- });
55
-
56
- return {
57
- ok: true,
58
- status: 200,
59
- content: formatJson({
60
- run: result.run,
61
- coverage: result.coverage,
62
- }),
63
- metadata: {
64
- status: result.run.status,
65
- profile: result.run.profile,
66
- durationMs: result.run.durationMs,
67
- },
68
- };
69
- },
70
- { category: "tests" },
71
- );
72
- }
73
-
74
- function registerTestHistoryTool() {
75
- registerTool(
76
- "workspace_test_history",
77
- async ({ args = {} }) => {
78
- const includeLogs = parseBoolean(args.include_logs ?? args.includeLogs);
79
- const limit = parseNumber(args.limit, 5);
80
- const runs = listTestRuns({
81
- limit,
82
- includeLogs: includeLogs === true,
83
- });
84
- return {
85
- ok: true,
86
- status: 200,
87
- content: formatJson({
88
- runs,
89
- count: runs.length,
90
- }),
91
- metadata: {
92
- count: runs.length,
93
- },
94
- };
95
- },
96
- { category: "tests" },
97
- );
98
- }
99
-
100
- function registerTestSummaryTool() {
101
- registerTool(
102
- "workspace_test_summary",
103
- async ({ args = {} }) => {
104
- const includeRecent = parseBoolean(args.include_recent ?? args.includeRecent);
105
- const recentLimit = parseNumber(args.recent_limit ?? args.recentLimit, 5);
106
- const summary = getTestSummary({
107
- includeRecent,
108
- recentLimit,
109
- });
110
- return {
111
- ok: true,
112
- status: 200,
113
- content: formatJson(summary),
114
- metadata: {
115
- totalRuns: summary.totalRuns,
116
- passRate: summary.passRate,
117
- },
118
- };
119
- },
120
- { category: "tests" },
121
- );
122
- }
123
-
124
- function registerTestTools() {
125
- registerTestRunTool();
126
- registerTestHistoryTool();
127
- registerTestSummaryTool();
128
- }
129
-
130
- module.exports = {
131
- registerTestTools,
132
- };
@@ -1,358 +0,0 @@
1
- const { URL } = require("url");
2
- const { Agent } = require("undici");
3
- const config = require("../config");
4
- const logger = require("../logger");
5
- const { registerTool } = require(".");
6
-
7
- /**
8
- * Dedicated HTTP agent for TinyFish SSE streams.
9
- * The default webAgent in web-client.js has a 30s bodyTimeout which is too
10
- * short for browser-automation tasks that can take up to 120s.
11
- */
12
- const sseAgent = new Agent({
13
- connections: 10,
14
- pipelining: 1,
15
- keepAliveTimeout: 60000,
16
- connectTimeout: 15000,
17
- bodyTimeout: 0, // no body timeout — we manage timeout via AbortController
18
- headersTimeout: 15000,
19
- maxRedirections: 3,
20
- strictContentLength: false,
21
- });
22
-
23
- // ---------------------------------------------------------------------------
24
- // Argument normalisers
25
- // ---------------------------------------------------------------------------
26
-
27
- function normalizeUrl(args) {
28
- const raw = args.url ?? args.uri ?? args.href ?? args.target_url;
29
- if (typeof raw !== "string" || raw.trim().length === 0) {
30
- throw new Error("web_agent requires a non-empty url string.");
31
- }
32
- // Validate URL
33
- try {
34
- new URL(raw.trim());
35
- } catch {
36
- throw new Error(`web_agent received an invalid URL: ${raw}`);
37
- }
38
- return raw.trim();
39
- }
40
-
41
- function normalizeGoal(args) {
42
- const goal = args.goal ?? args.task ?? args.prompt ?? args.instruction;
43
- if (typeof goal !== "string" || goal.trim().length === 0) {
44
- throw new Error("web_agent requires a non-empty goal string.");
45
- }
46
- return goal.trim();
47
- }
48
-
49
- function resolveBrowserProfile(args) {
50
- const profile = args.browser_profile ?? args.browserProfile ?? config.tinyfish.browserProfile;
51
- if (profile === "stealth") return "stealth";
52
- return "lite";
53
- }
54
-
55
- // ---------------------------------------------------------------------------
56
- // SSE stream consumer
57
- // ---------------------------------------------------------------------------
58
-
59
- async function consumeSSEStream(response, timeoutMs) {
60
- const reader = response.body.getReader();
61
- const decoder = new TextDecoder();
62
- let buffer = "";
63
- const startTime = Date.now();
64
-
65
- try {
66
- while (true) {
67
- if (Date.now() - startTime > timeoutMs) {
68
- const err = new Error(`TinyFish SSE stream timed out after ${timeoutMs}ms`);
69
- err.code = "ETIMEDOUT";
70
- err.status = 504;
71
- throw err;
72
- }
73
-
74
- const { done, value } = await reader.read();
75
- if (done) break;
76
-
77
- buffer += decoder.decode(value, { stream: true });
78
-
79
- // SSE events are separated by double newlines
80
- const parts = buffer.split("\n\n");
81
- // Keep the last (possibly incomplete) chunk in the buffer
82
- buffer = parts.pop() || "";
83
-
84
- for (const part of parts) {
85
- // Extract the data: line(s)
86
- const lines = part.split("\n");
87
- let dataStr = "";
88
- for (const line of lines) {
89
- if (line.startsWith("data: ")) {
90
- dataStr += line.slice(6);
91
- } else if (line.startsWith("data:")) {
92
- dataStr += line.slice(5);
93
- }
94
- }
95
-
96
- if (!dataStr) continue;
97
-
98
- let event;
99
- try {
100
- event = JSON.parse(dataStr);
101
- } catch {
102
- // Not valid JSON — skip this SSE frame
103
- logger.debug({ raw: dataStr.slice(0, 200) }, "TinyFish: non-JSON SSE frame, skipping");
104
- continue;
105
- }
106
-
107
- logger.debug(
108
- { type: event.type, status: event.status },
109
- "TinyFish SSE event"
110
- );
111
-
112
- if (event.type === "COMPLETE" || event.type === "complete") {
113
- const status = (event.status ?? "").toUpperCase();
114
- if (status === "COMPLETED" || status === "SUCCESS") {
115
- return event.resultJson ?? event.result ?? event.data ?? event;
116
- }
117
- // Task failed
118
- const errMsg = event.error ?? event.message ?? "TinyFish task failed";
119
- const err = new Error(typeof errMsg === "string" ? errMsg : JSON.stringify(errMsg));
120
- err.code = "TINYFISH_TASK_FAILED";
121
- err.status = 502;
122
- throw err;
123
- }
124
- }
125
- }
126
-
127
- // Stream ended without a COMPLETE event
128
- const err = new Error("TinyFish SSE stream ended without a COMPLETE event");
129
- err.code = "TINYFISH_INCOMPLETE";
130
- err.status = 502;
131
- throw err;
132
- } finally {
133
- reader.releaseLock();
134
- }
135
- }
136
-
137
- // ---------------------------------------------------------------------------
138
- // Core API call
139
- // ---------------------------------------------------------------------------
140
-
141
- async function callTinyFishAPI({ url, goal, browserProfile, proxyConfig, timeoutMs }) {
142
- const endpoint = config.tinyfish.endpoint;
143
- const apiKey = config.tinyfish.apiKey;
144
-
145
- if (!apiKey) {
146
- return {
147
- ok: false,
148
- status: 503,
149
- content: JSON.stringify({
150
- error: "tinyfish_not_configured",
151
- message:
152
- "TinyFish API key is not configured. Set TINYFISH_API_KEY in your .env file. Get a key from https://tinyfish.ai",
153
- }, null, 2),
154
- };
155
- }
156
-
157
- const body = {
158
- url,
159
- goal,
160
- browserProfile,
161
- };
162
-
163
- if (proxyConfig) {
164
- body.proxy = proxyConfig;
165
- }
166
-
167
- const controller = new AbortController();
168
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
169
-
170
- try {
171
- const response = await fetch(endpoint, {
172
- method: "POST",
173
- headers: {
174
- "Content-Type": "application/json",
175
- "X-API-Key": apiKey,
176
- Accept: "text/event-stream",
177
- },
178
- body: JSON.stringify(body),
179
- signal: controller.signal,
180
- dispatcher: sseAgent,
181
- });
182
-
183
- // Handle non-2xx responses before attempting SSE parse
184
- if (!response.ok) {
185
- const text = await response.text().catch(() => "");
186
- const shouldRetry = response.status === 429 || response.status >= 500;
187
-
188
- if (shouldRetry) {
189
- // Retry once with 2s backoff
190
- logger.warn(
191
- { status: response.status, body: text.slice(0, 200) },
192
- "TinyFish API error, retrying once"
193
- );
194
- await new Promise((r) => setTimeout(r, 2000));
195
-
196
- const retryController = new AbortController();
197
- const retryTimeout = setTimeout(() => retryController.abort(), timeoutMs);
198
- try {
199
- const retryResponse = await fetch(endpoint, {
200
- method: "POST",
201
- headers: {
202
- "Content-Type": "application/json",
203
- "X-API-Key": apiKey,
204
- Accept: "text/event-stream",
205
- },
206
- body: JSON.stringify(body),
207
- signal: retryController.signal,
208
- dispatcher: sseAgent,
209
- });
210
-
211
- if (!retryResponse.ok) {
212
- const retryText = await retryResponse.text().catch(() => "");
213
- const err = new Error(
214
- `TinyFish API error (${retryResponse.status}): ${retryResponse.statusText}`
215
- );
216
- err.status = retryResponse.status;
217
- err.body = retryText;
218
- throw err;
219
- }
220
-
221
- const result = await consumeSSEStream(retryResponse, timeoutMs);
222
- return {
223
- ok: true,
224
- status: 200,
225
- result,
226
- };
227
- } finally {
228
- clearTimeout(retryTimeout);
229
- }
230
- }
231
-
232
- const err = new Error(
233
- `TinyFish API error (${response.status}): ${response.statusText}`
234
- );
235
- err.status = response.status;
236
- err.body = text;
237
- throw err;
238
- }
239
-
240
- const result = await consumeSSEStream(response, timeoutMs);
241
- return {
242
- ok: true,
243
- status: 200,
244
- result,
245
- };
246
- } catch (error) {
247
- if (error.name === "AbortError") {
248
- const err = new Error(`TinyFish request timed out after ${timeoutMs}ms`);
249
- err.code = "ETIMEDOUT";
250
- err.status = 504;
251
- throw err;
252
- }
253
- throw error;
254
- } finally {
255
- clearTimeout(timeout);
256
- }
257
- }
258
-
259
- // ---------------------------------------------------------------------------
260
- // Tool registration
261
- // ---------------------------------------------------------------------------
262
-
263
- function registerTinyFishTool() {
264
- registerTool(
265
- "web_agent",
266
- async ({ args = {} }) => {
267
- const url = normalizeUrl(args);
268
- const goal = normalizeGoal(args);
269
- const browserProfile = resolveBrowserProfile(args);
270
- const timeoutMs = config.tinyfish.timeoutMs;
271
-
272
- // Build proxy config if enabled
273
- let proxyConfig = null;
274
- if (config.tinyfish.proxyEnabled) {
275
- proxyConfig = {
276
- enabled: true,
277
- country: config.tinyfish.proxyCountry,
278
- };
279
- }
280
-
281
- try {
282
- const response = await callTinyFishAPI({
283
- url,
284
- goal,
285
- browserProfile,
286
- proxyConfig,
287
- timeoutMs,
288
- });
289
-
290
- // Guard clause: not configured
291
- if (!response.ok && response.status === 503) {
292
- return response;
293
- }
294
-
295
- const resultStr =
296
- typeof response.result === "string"
297
- ? response.result
298
- : JSON.stringify(response.result, null, 2);
299
-
300
- logger.debug(
301
- {
302
- url,
303
- goal: goal.slice(0, 100),
304
- browserProfile,
305
- resultLength: resultStr.length,
306
- },
307
- "TinyFish web_agent completed"
308
- );
309
-
310
- return {
311
- ok: true,
312
- status: 200,
313
- content: resultStr,
314
- metadata: {
315
- url,
316
- goal,
317
- browserProfile,
318
- resultLength: resultStr.length,
319
- },
320
- };
321
- } catch (err) {
322
- logger.error(
323
- { err, url, goal: goal.slice(0, 100) },
324
- "web_agent request failed"
325
- );
326
- return {
327
- ok: false,
328
- status: err.status ?? 500,
329
- content: JSON.stringify(
330
- {
331
- error: err.code ?? "web_agent_failed",
332
- message: err.message,
333
- url,
334
- ...(err.status ? { http_status: err.status } : {}),
335
- },
336
- null,
337
- 2
338
- ),
339
- metadata: {
340
- url,
341
- goal,
342
- error_code: err.code,
343
- ...(err.status ? { http_status: err.status } : {}),
344
- },
345
- };
346
- }
347
- },
348
- { category: "tinyfish" }
349
- );
350
- }
351
-
352
- function registerTinyFishTools() {
353
- registerTinyFishTool();
354
- }
355
-
356
- module.exports = {
357
- registerTinyFishTools,
358
- };
@@ -1,106 +0,0 @@
1
- const logger = require("../logger");
2
- const config = require("../config");
3
-
4
- const TRUNCATION_LIMITS = {
5
- Read: { maxChars: 8000, strategy: 'middle' },
6
- Bash: { maxChars: 30000, strategy: 'tail' },
7
- Grep: { maxChars: 12000, strategy: 'head' },
8
- Glob: { maxChars: 8000, strategy: 'head' },
9
- WebFetch: { maxChars: 16000, strategy: 'head' },
10
- WebSearch: { maxChars: 12000, strategy: 'head' },
11
- WebAgent: { maxChars: 16000, strategy: 'head' },
12
- LSP: { maxChars: 8000, strategy: 'head' },
13
- Edit: { maxChars: 8000, strategy: 'middle' },
14
- Write: { maxChars: 8000, strategy: 'middle' },
15
- Task: { maxChars: 20000, strategy: 'tail' },
16
- AgentTask: { maxChars: 20000, strategy: 'tail' },
17
- };
18
-
19
- /**
20
- * Apply truncation strategy to text
21
- */
22
- function applyTruncationStrategy(text, maxChars, strategy) {
23
- if (text.length <= maxChars) {
24
- return text;
25
- }
26
-
27
- switch (strategy) {
28
- case 'head':
29
- // Keep beginning
30
- return text.slice(0, maxChars);
31
-
32
- case 'tail':
33
- // Keep end
34
- return text.slice(-maxChars);
35
-
36
- case 'middle': {
37
- // Keep start and end, remove middle
38
- const keepSize = Math.floor(maxChars / 2);
39
- const start = text.slice(0, keepSize);
40
- const end = text.slice(-keepSize);
41
- const removed = text.length - (keepSize * 2);
42
- return `${start}\n\n... [${removed} characters truncated for token efficiency] ...\n\n${end}`;
43
- }
44
-
45
- default:
46
- return text.slice(0, maxChars);
47
- }
48
- }
49
-
50
- /**
51
- * Truncate tool output based on tool type
52
- */
53
- function truncateToolOutput(toolName, output) {
54
- // Skip if truncation disabled
55
- if (config.toolTruncation?.enabled === false) {
56
- return output;
57
- }
58
-
59
- if (!output || typeof output !== 'string') {
60
- return output;
61
- }
62
-
63
- const limit = TRUNCATION_LIMITS[toolName];
64
- if (!limit) {
65
- // No truncation for unknown tools
66
- return output;
67
- }
68
-
69
- if (output.length <= limit.maxChars) {
70
- return output;
71
- }
72
-
73
- const truncated = applyTruncationStrategy(output, limit.maxChars, limit.strategy);
74
- const removed = output.length - truncated.length;
75
-
76
- logger.debug({
77
- tool: toolName,
78
- originalLength: output.length,
79
- truncatedLength: truncated.length,
80
- removed,
81
- strategy: limit.strategy
82
- }, 'Truncated tool output for token efficiency');
83
-
84
- return truncated;
85
- }
86
-
87
- /**
88
- * Get truncation limit for a specific tool
89
- */
90
- function getTruncationLimit(toolName) {
91
- return TRUNCATION_LIMITS[toolName] || null;
92
- }
93
-
94
- /**
95
- * Update truncation limit for a tool (useful for testing)
96
- */
97
- function setTruncationLimit(toolName, maxChars, strategy = 'head') {
98
- TRUNCATION_LIMITS[toolName] = { maxChars, strategy };
99
- }
100
-
101
- module.exports = {
102
- truncateToolOutput,
103
- getTruncationLimit,
104
- setTruncationLimit,
105
- TRUNCATION_LIMITS
106
- };
@@ -1,71 +0,0 @@
1
- const { Agent, fetch: undiciFetch, setGlobalDispatcher } = require("undici");
2
- const logger = require("../logger");
3
-
4
- /**
5
- * Create an optimized HTTP agent for web search and fetch operations
6
- * with connection pooling and keep-alive enabled
7
- */
8
- function createWebAgent() {
9
- const agent = new Agent({
10
- // Connection pooling settings
11
- connections: 50, // Max concurrent connections per origin
12
- pipelining: 10, // Max pipelined requests per connection
13
-
14
- // Keep-alive settings
15
- keepAliveTimeout: 60000, // Keep connections alive for 60s
16
- keepAliveMaxTimeout: 600000, // Maximum keep-alive time (10 minutes)
17
-
18
- // Connection timeouts
19
- connectTimeout: 10000, // 10s to establish connection
20
- bodyTimeout: 30000, // 30s to receive response body
21
- headersTimeout: 10000, // 10s to receive headers
22
-
23
- // Connection reuse
24
- maxRedirections: 5,
25
-
26
- // Performance optimizations
27
- strictContentLength: false, // Don't require Content-Length header
28
- });
29
-
30
- logger.info({
31
- connections: 50,
32
- keepAliveTimeout: 60000,
33
- pipelining: 10,
34
- }, "Web HTTP agent initialized with connection pooling");
35
-
36
- return agent;
37
- }
38
-
39
- /**
40
- * Global web agent instance - reused across all web search/fetch calls
41
- */
42
- const webAgent = createWebAgent();
43
-
44
- /**
45
- * Fetch with the optimized agent (uses undici.fetch for dispatcher support)
46
- */
47
- async function fetchWithAgent(url, options = {}) {
48
- return undiciFetch(url, {
49
- ...options,
50
- dispatcher: webAgent,
51
- });
52
- }
53
-
54
- /**
55
- * Get connection pool statistics
56
- */
57
- function getAgentStats() {
58
- // Undici doesn't expose detailed stats easily, but we can log connection info
59
- return {
60
- agent: "undici",
61
- keepAlive: true,
62
- maxConnections: 50,
63
- pipelining: 10,
64
- };
65
- }
66
-
67
- module.exports = {
68
- webAgent,
69
- fetchWithAgent,
70
- getAgentStats,
71
- };