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
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
- };