lynkr 9.9.0 → 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 (73) hide show
  1. package/README.md +92 -23
  2. package/bin/cli.js +13 -7
  3. package/bin/lynkr-init.js +34 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +3 -3
  7. package/scripts/build-eval-set.js +256 -0
  8. package/scripts/mine-difficulty-anchors.js +288 -0
  9. package/scripts/validate-difficulty-classifier.js +144 -0
  10. package/scripts/validate-intent-anchors.js +186 -0
  11. package/src/api/providers-handler.js +0 -1
  12. package/src/api/router.js +292 -160
  13. package/src/clients/databricks.js +95 -6
  14. package/src/config/index.js +3 -43
  15. package/src/context/tool-result-compressor.js +883 -40
  16. package/src/orchestrator/index.js +117 -1235
  17. package/src/orchestrator/passthrough-stream.js +382 -0
  18. package/src/orchestrator/sse-transformer.js +408 -0
  19. package/src/routing/affinity-store.js +17 -3
  20. package/src/routing/classifier-setup.js +207 -0
  21. package/src/routing/complexity-analyzer.js +40 -4
  22. package/src/routing/difficulty-classifier.js +261 -0
  23. package/src/routing/index.js +70 -7
  24. package/src/routing/intent-score.js +132 -12
  25. package/src/routing/session-affinity.js +8 -1
  26. package/src/routing/side-channel-detector.js +103 -0
  27. package/src/server.js +20 -46
  28. package/src/agents/context-manager.js +0 -236
  29. package/src/agents/decomposition/dispatcher.js +0 -185
  30. package/src/agents/decomposition/gate.js +0 -136
  31. package/src/agents/decomposition/index.js +0 -183
  32. package/src/agents/decomposition/model-call.js +0 -75
  33. package/src/agents/decomposition/planner.js +0 -223
  34. package/src/agents/decomposition/synthesizer.js +0 -89
  35. package/src/agents/decomposition/telemetry.js +0 -55
  36. package/src/agents/definitions/loader.js +0 -653
  37. package/src/agents/executor.js +0 -457
  38. package/src/agents/index.js +0 -165
  39. package/src/agents/parallel-coordinator.js +0 -68
  40. package/src/agents/reflector.js +0 -331
  41. package/src/agents/skillbook.js +0 -331
  42. package/src/agents/store.js +0 -259
  43. package/src/edits/index.js +0 -171
  44. package/src/indexer/babel-parser.js +0 -213
  45. package/src/indexer/index.js +0 -1629
  46. package/src/indexer/navigation/index.js +0 -32
  47. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  48. package/src/indexer/parser.js +0 -443
  49. package/src/tasks/store.js +0 -349
  50. package/src/tests/coverage.js +0 -173
  51. package/src/tests/index.js +0 -171
  52. package/src/tests/store.js +0 -213
  53. package/src/tools/agent-task.js +0 -145
  54. package/src/tools/code-mode.js +0 -304
  55. package/src/tools/decompose.js +0 -91
  56. package/src/tools/edits.js +0 -94
  57. package/src/tools/execution.js +0 -171
  58. package/src/tools/git.js +0 -1346
  59. package/src/tools/index.js +0 -306
  60. package/src/tools/indexer.js +0 -360
  61. package/src/tools/lazy-loader.js +0 -366
  62. package/src/tools/mcp-remote.js +0 -88
  63. package/src/tools/mcp.js +0 -116
  64. package/src/tools/process.js +0 -167
  65. package/src/tools/smart-selection.js +0 -180
  66. package/src/tools/stubs.js +0 -55
  67. package/src/tools/tasks.js +0 -260
  68. package/src/tools/tests.js +0 -132
  69. package/src/tools/tinyfish.js +0 -358
  70. package/src/tools/truncate.js +0 -106
  71. package/src/tools/web-client.js +0 -71
  72. package/src/tools/web.js +0 -415
  73. package/src/tools/workspace.js +0 -204
package/src/tools/web.js DELETED
@@ -1,415 +0,0 @@
1
- const { URL } = require("url");
2
- const config = require("../config");
3
- const logger = require("../logger");
4
- const { registerTool } = require(".");
5
- const { withRetry } = require("../clients/retry");
6
- const { fetchWithAgent } = require("./web-client");
7
-
8
- const DEFAULT_MAX_RESULTS = 5;
9
-
10
- /**
11
- * Extract readable text from HTML
12
- * Removes scripts, styles, and extracts meaningful content
13
- */
14
- function extractTextFromHtml(html) {
15
- if (typeof html !== "string") return "";
16
-
17
- let text = html;
18
-
19
- // Remove script and style tags with their content
20
- text = text.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, " ");
21
- text = text.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, " ");
22
-
23
- // Remove HTML comments
24
- text = text.replace(/<!--[\s\S]*?-->/g, " ");
25
-
26
- // Convert common block elements to newlines
27
- text = text.replace(/<\/(div|p|br|h[1-6]|li|tr|section|article|header|footer|nav)>/gi, "\n");
28
-
29
- // Remove all remaining HTML tags
30
- text = text.replace(/<[^>]+>/g, " ");
31
-
32
- // Decode common HTML entities
33
- text = text
34
- .replace(/&nbsp;/g, " ")
35
- .replace(/&amp;/g, "&")
36
- .replace(/&lt;/g, "<")
37
- .replace(/&gt;/g, ">")
38
- .replace(/&quot;/g, '"')
39
- .replace(/&#39;/g, "'")
40
- .replace(/&apos;/g, "'");
41
-
42
- // Normalize whitespace
43
- text = text.replace(/\r\n/g, "\n");
44
- text = text.replace(/\r/g, "\n");
45
- text = text.replace(/[ \t]+/g, " ");
46
- text = text.replace(/\n\s+/g, "\n");
47
- text = text.replace(/\n{3,}/g, "\n\n");
48
-
49
- return text.trim();
50
- }
51
-
52
- function normaliseQuery(args = {}) {
53
- const query = args.query ?? args.q ?? args.prompt ?? args.search ?? args.input;
54
- if (typeof query !== "string" || query.trim().length === 0) {
55
- throw new Error("web_search requires a non-empty query string.");
56
- }
57
- return query.trim();
58
- }
59
-
60
- function resolveLimit(args = {}) {
61
- const raw = args.limit ?? args.top_k ?? args.max_results;
62
- if (raw === undefined) return DEFAULT_MAX_RESULTS;
63
- const parsed = Number.parseInt(raw, 10);
64
- if (Number.isNaN(parsed) || parsed <= 0) return DEFAULT_MAX_RESULTS;
65
- return Math.min(parsed, 20);
66
- }
67
-
68
- function buildAllowedHosts() {
69
- if (config.webSearch.allowAllHosts) {
70
- return null;
71
- }
72
- const configured = config.webSearch.allowedHosts ?? [];
73
- const hosts = new Set();
74
- if (config.webSearch.endpoint) {
75
- try {
76
- const endpointHost = new URL(config.webSearch.endpoint).hostname.toLowerCase();
77
- hosts.add(endpointHost);
78
- } catch {
79
- // ignore parse errors; config already validated
80
- }
81
- }
82
- configured.forEach((host) => hosts.add(host));
83
- return hosts;
84
- }
85
-
86
- function buildSearchUrl({ query, limit }) {
87
- const endpoint = new URL(config.webSearch.endpoint);
88
- endpoint.searchParams.set("q", query);
89
- endpoint.searchParams.set("format", "json");
90
- endpoint.searchParams.set("per_page", String(limit));
91
- return endpoint;
92
- }
93
-
94
- async function performSearch({ query, limit, timeoutMs }) {
95
- const url = buildSearchUrl({ query, limit });
96
-
97
- // Wrap fetch in retry logic if enabled
98
- const fetchFn = async () => {
99
- const controller = new AbortController();
100
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
101
-
102
- try {
103
- const response = await fetchWithAgent(url, {
104
- method: "GET",
105
- signal: controller.signal,
106
- });
107
-
108
- const text = await response.text();
109
- let json;
110
- try {
111
- json = JSON.parse(text);
112
- } catch {
113
- json = null;
114
- }
115
-
116
- if (!response.ok) {
117
- const error = new Error(`Web search provider error (${response.status}): ${response.statusText}`);
118
- error.status = response.status;
119
- error.body = text;
120
- error.code = response.status === 429 ? "RATE_LIMITED" :
121
- response.status >= 500 ? "SERVER_ERROR" : "REQUEST_ERROR";
122
- throw error;
123
- }
124
-
125
- return json ?? { results: [], raw: text };
126
- } catch (error) {
127
- if (error.name === "AbortError") {
128
- const timeoutError = new Error(`Web search timeout after ${timeoutMs}ms`);
129
- timeoutError.code = "ETIMEDOUT";
130
- throw timeoutError;
131
- }
132
- throw error;
133
- } finally {
134
- clearTimeout(timeout);
135
- }
136
- };
137
-
138
- // Apply retry logic if enabled
139
- if (config.webSearch.retryEnabled) {
140
- return withRetry(fetchFn, {
141
- maxRetries: config.webSearch.maxRetries,
142
- initialDelay: 500,
143
- maxDelay: 5000,
144
- });
145
- }
146
-
147
- return fetchFn();
148
- }
149
-
150
- function summariseResult(item) {
151
- if (!item) return null;
152
- return {
153
- title: item.title ?? item.name ?? null,
154
- url: item.url ?? item.link ?? null,
155
- snippet: item.snippet ?? item.content ?? item.summary ?? item.excerpt ?? null,
156
- score: item.score ?? item.rank ?? null,
157
- source: item.source ?? null,
158
- metadata: item.metadata ?? null,
159
- };
160
- }
161
-
162
- function formatSearchResponse(payload, { query, limit }) {
163
- const results = Array.isArray(payload?.results) ? payload.results : [];
164
- const payloadCount =
165
- typeof payload?.number_of_results === "number" && payload.number_of_results > 0
166
- ? payload.number_of_results
167
- : null;
168
- const effectiveCount = payloadCount ?? results.length;
169
- const numberOfResults = effectiveCount > 0 ? effectiveCount : undefined;
170
- const metadata = {
171
- ...(payload?.metadata ?? {}),
172
- raw_number_of_results: payloadCount,
173
- engines: payload?.engines ?? null,
174
- categories: payload?.categories ?? null,
175
- };
176
- if (numberOfResults !== undefined) {
177
- metadata.number_of_results = numberOfResults;
178
- }
179
- return {
180
- query,
181
- limit,
182
- number_of_results: numberOfResults,
183
- results: results.map(summariseResult).filter(Boolean),
184
- metadata,
185
- };
186
- }
187
-
188
- function buildAllowedFetchHosts() {
189
- return config.webSearch.allowAllHosts ? null : buildAllowedHosts();
190
- }
191
-
192
- function parseUrl(rawUrl) {
193
- try {
194
- return new URL(rawUrl);
195
- } catch (err) {
196
- err.code = "invalid_url";
197
- throw err;
198
- }
199
- }
200
-
201
- function ensureHostAllowed(url, allowedHosts) {
202
- if (allowedHosts === null) {
203
- return;
204
- }
205
- const host = url.hostname.toLowerCase();
206
- if (!allowedHosts.has(host)) {
207
- const error = new Error(`Host ${host} is not in the allowlist.`);
208
- error.code = "host_not_allowed";
209
- throw error;
210
- }
211
- }
212
-
213
- async function fetchDocument(url, timeoutMs) {
214
- // Wrap fetch in retry logic
215
- const fetchFn = async () => {
216
- const controller = new AbortController();
217
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
218
-
219
- try {
220
- const response = await fetchWithAgent(url.toString(), { signal: controller.signal });
221
- const text = await response.text();
222
-
223
- if (!response.ok) {
224
- const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
225
- error.status = response.status;
226
- error.code = response.status === 429 ? "RATE_LIMITED" :
227
- response.status >= 500 ? "SERVER_ERROR" : "REQUEST_ERROR";
228
- throw error;
229
- }
230
-
231
- return {
232
- status: response.status,
233
- headers: Object.fromEntries(response.headers.entries()),
234
- body: text,
235
- contentType: response.headers.get("content-type") || "",
236
- };
237
- } catch (error) {
238
- if (error.name === "AbortError") {
239
- const timeoutError = new Error(`Web fetch timeout after ${timeoutMs}ms`);
240
- timeoutError.code = "ETIMEDOUT";
241
- throw timeoutError;
242
- }
243
- throw error;
244
- } finally {
245
- clearTimeout(timeout);
246
- }
247
- };
248
-
249
- // Apply retry logic if enabled
250
- if (config.webSearch.retryEnabled) {
251
- return withRetry(fetchFn, {
252
- maxRetries: config.webSearch.maxRetries,
253
- initialDelay: 500,
254
- maxDelay: 5000,
255
- });
256
- }
257
-
258
- return fetchFn();
259
- }
260
-
261
- function registerWebSearchTool() {
262
- registerTool(
263
- "web_search",
264
- async ({ args = {} }) => {
265
- const query = normaliseQuery(args);
266
- const limit = resolveLimit(args);
267
- const timeoutMs = config.webSearch.timeoutMs;
268
-
269
- try {
270
- const payload = await performSearch({ query, limit, timeoutMs });
271
- const formatted = formatSearchResponse(payload, { query, limit });
272
- const resultCount = formatted.results.length;
273
- logger.debug(
274
- {
275
- query,
276
- limit,
277
- result_count: resultCount,
278
- number_of_results: formatted.number_of_results,
279
- engines: payload?.engines ?? null,
280
- categories: payload?.categories ?? null,
281
- sample_result: formatted.results[0] ?? null,
282
- },
283
- "Web search results summarised",
284
- );
285
- return {
286
- ok: true,
287
- status: 200,
288
- content: JSON.stringify(formatted, null, 2),
289
- metadata: {
290
- query,
291
- limit,
292
- result_count: resultCount,
293
- ...(formatted.number_of_results !== undefined
294
- ? { number_of_results: formatted.number_of_results }
295
- : {}),
296
- },
297
- };
298
- } catch (err) {
299
- logger.error({ err }, "Web search request failed");
300
- return {
301
- ok: false,
302
- status: err.status ?? 500,
303
- content: JSON.stringify(
304
- {
305
- error: err.code ?? "web_search_failed",
306
- message: err.message,
307
- status: err.status ?? 500,
308
- },
309
- null,
310
- 2,
311
- ),
312
- metadata: {
313
- query,
314
- limit,
315
- },
316
- };
317
- }
318
- },
319
- { category: "web" },
320
- );
321
- }
322
-
323
- function registerWebFetchTool() {
324
- registerTool(
325
- "web_fetch",
326
- async ({ args = {} }) => {
327
- const rawUrl = args.url ?? args.uri ?? args.href;
328
- if (typeof rawUrl !== "string" || rawUrl.trim().length === 0) {
329
- throw new Error("web_fetch requires a url string.");
330
- }
331
- const url = parseUrl(rawUrl.trim());
332
- const allowedHosts = buildAllowedFetchHosts();
333
- ensureHostAllowed(url, allowedHosts);
334
-
335
- const timeoutMs = config.webSearch.timeoutMs;
336
- try {
337
- const document = await fetchDocument(url, timeoutMs);
338
-
339
- // Extract text content from HTML if content type indicates HTML
340
- const isHtml = document.contentType.toLowerCase().includes("text/html");
341
- const rawBody = document.body;
342
- const bodyPreview = rawBody.slice(0, config.webSearch.bodyPreviewMax);
343
-
344
- // For HTML, provide both raw preview and extracted text
345
- const result = {
346
- url: url.toString(),
347
- status: document.status,
348
- content_type: document.contentType,
349
- headers: document.headers,
350
- body_preview: bodyPreview,
351
- };
352
-
353
- if (isHtml) {
354
- const extractedText = extractTextFromHtml(rawBody);
355
- result.text_content = extractedText.slice(0, config.webSearch.bodyPreviewMax);
356
- result.text_length = extractedText.length;
357
- logger.debug({
358
- url: url.toString(),
359
- originalLength: rawBody.length,
360
- extractedLength: extractedText.length,
361
- }, "Extracted text from HTML");
362
- }
363
-
364
- return {
365
- ok: document.status >= 200 && document.status < 400,
366
- status: document.status,
367
- content: JSON.stringify(result, null, 2),
368
- metadata: {
369
- url: url.toString(),
370
- status: document.status,
371
- content_type: document.contentType,
372
- is_html: isHtml,
373
- },
374
- };
375
- } catch (err) {
376
- logger.error({
377
- err,
378
- url: url.toString(),
379
- code: err.code,
380
- status: err.status
381
- }, "web_fetch failed");
382
-
383
- return {
384
- ok: false,
385
- status: err.status ?? 500,
386
- content: JSON.stringify(
387
- {
388
- error: err.code ?? "web_fetch_failed",
389
- message: err.message,
390
- url: url.toString(),
391
- ...(err.status ? { http_status: err.status } : {}),
392
- },
393
- null,
394
- 2,
395
- ),
396
- metadata: {
397
- url: url.toString(),
398
- error_code: err.code,
399
- ...(err.status ? { http_status: err.status } : {}),
400
- },
401
- };
402
- }
403
- },
404
- { category: "web" },
405
- );
406
- }
407
-
408
- function registerWebTools() {
409
- registerWebSearchTool();
410
- registerWebFetchTool();
411
- }
412
-
413
- module.exports = {
414
- registerWebTools,
415
- };
@@ -1,204 +0,0 @@
1
- const path = require("path");
2
- const {
3
- readFile,
4
- writeFile,
5
- applyFilePatch,
6
- resolveWorkspacePath,
7
- expandTilde,
8
- isExternalPath,
9
- readExternalFile,
10
- fileExists,
11
- workspaceRoot,
12
- } = require("../workspace");
13
- const { recordEdit } = require("../edits");
14
- const { registerTool } = require(".");
15
- const logger = require("../logger");
16
-
17
- function validateString(value, field) {
18
- if (typeof value !== "string" || value.trim().length === 0) {
19
- throw new Error(`${field} must be a non-empty string`);
20
- }
21
- return value;
22
- }
23
-
24
- function normalizeEncoding(value) {
25
- if (!value) return "utf8";
26
- const encoding = value.toLowerCase();
27
- if (!["utf8", "utf-8"].includes(encoding)) {
28
- throw new Error(`Unsupported encoding: ${value}`);
29
- }
30
- return "utf8";
31
- }
32
-
33
- function registerWorkspaceTools() {
34
- registerTool(
35
- "fs_read",
36
- async ({ args = {} }) => {
37
- const targetPath = validateString(args.path ?? args.file ?? args.file_path, "path");
38
- const encoding = normalizeEncoding(args.encoding);
39
-
40
- // Check if path is outside workspace
41
- if (isExternalPath(targetPath)) {
42
- if (args.user_approved !== true) {
43
- const expanded = expandTilde(targetPath);
44
- const resolved = path.resolve(expanded);
45
- return {
46
- ok: false,
47
- status: 403,
48
- content: JSON.stringify({
49
- error: "external_path_requires_approval",
50
- message: `The file "${targetPath}" resolves to "${resolved}" which is outside the workspace. You MUST ask the user for permission before reading this file. If the user approves, call this tool again with the same path and set user_approved to true.`,
51
- resolved_path: resolved,
52
- }),
53
- };
54
- }
55
- // User approved — read external file
56
- const { content, resolvedPath } = await readExternalFile(targetPath, encoding);
57
- return {
58
- ok: true,
59
- status: 200,
60
- content,
61
- metadata: { path: targetPath, encoding, resolved_path: resolvedPath },
62
- };
63
- }
64
-
65
- // Normal workspace read (unchanged)
66
- const content = await readFile(targetPath, encoding);
67
- return {
68
- ok: true,
69
- status: 200,
70
- content,
71
- metadata: {
72
- path: targetPath,
73
- encoding,
74
- resolved_path: resolveWorkspacePath(targetPath),
75
- },
76
- };
77
- },
78
- { category: "workspace" },
79
- );
80
-
81
- registerTool(
82
- "fs_write",
83
- async ({ args = {} }, context = {}) => {
84
- const relativePath = validateString(
85
- args.path ??
86
- args.file ??
87
- args.file_path ??
88
- args.filePath ??
89
- args.filename ??
90
- args.name,
91
- "path",
92
- );
93
- const encoding = normalizeEncoding(args.encoding);
94
- const content =
95
- typeof args.content === "string"
96
- ? args.content
97
- : typeof args.contents === "string"
98
- ? args.contents
99
- : "";
100
- const createParents = args.create_parents !== false;
101
-
102
- const writeResult = await writeFile(relativePath, content, {
103
- encoding,
104
- createParents,
105
- });
106
-
107
- try {
108
- recordEdit({
109
- sessionId: context.session?.id ?? context.sessionId ?? null,
110
- filePath: relativePath,
111
- source: "fs_write",
112
- beforeContent:
113
- typeof writeResult.previousContent === "string"
114
- ? writeResult.previousContent
115
- : writeResult.previousContent ?? null,
116
- afterContent: content,
117
- metadata: {
118
- encoding,
119
- },
120
- });
121
- } catch (err) {
122
- logger.warn({ err }, "Failed to record fs_write edit");
123
- }
124
-
125
- return {
126
- ok: true,
127
- status: 200,
128
- content: JSON.stringify(
129
- {
130
- path: relativePath,
131
- bytes: Buffer.byteLength(content, encoding),
132
- resolved_path: resolveWorkspacePath(relativePath),
133
- },
134
- null,
135
- 2,
136
- ),
137
- metadata: {
138
- path: relativePath,
139
- },
140
- };
141
- },
142
- { category: "workspace" },
143
- );
144
-
145
- registerTool(
146
- "edit_patch",
147
- async ({ args = {} }, context = {}) => {
148
- const relativePath = validateString(args.path ?? args.file, "path");
149
- const patch = validateString(args.patch, "patch");
150
- const encoding = normalizeEncoding(args.encoding);
151
-
152
- const exists = await fileExists(relativePath);
153
- if (!exists) {
154
- throw new Error("Cannot apply patch to non-existent file.");
155
- }
156
-
157
- const patchResult = await applyFilePatch(relativePath, patch, { encoding });
158
-
159
- try {
160
- recordEdit({
161
- sessionId: context.session?.id ?? context.sessionId ?? null,
162
- filePath: relativePath,
163
- source: "edit_patch",
164
- beforeContent:
165
- typeof patchResult.previousContent === "string"
166
- ? patchResult.previousContent
167
- : patchResult.previousContent ?? null,
168
- afterContent:
169
- typeof patchResult.nextContent === "string"
170
- ? patchResult.nextContent
171
- : patchResult.nextContent ?? null,
172
- metadata: {
173
- encoding,
174
- patchLength: patch.length,
175
- },
176
- });
177
- } catch (err) {
178
- logger.warn({ err }, "Failed to record edit_patch edit");
179
- }
180
-
181
- return {
182
- ok: true,
183
- status: 200,
184
- content: JSON.stringify(
185
- {
186
- path: relativePath,
187
- resolved_path: resolveWorkspacePath(relativePath),
188
- },
189
- null,
190
- 2,
191
- ),
192
- metadata: {
193
- path: relativePath,
194
- },
195
- };
196
- },
197
- { category: "workspace" },
198
- );
199
- }
200
-
201
- module.exports = {
202
- workspaceRoot,
203
- registerWorkspaceTools,
204
- };