@moikapy/lich 0.3.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.
@@ -0,0 +1,3431 @@
1
+ // src/tools/builtin/disk_usage.ts
2
+ import { execFile } from "child_process";
3
+ import { readdir } from "fs/promises";
4
+ import path2 from "path";
5
+
6
+ // src/tools/guard.ts
7
+ import path from "path";
8
+
9
+ // src/util/json.ts
10
+ function safe_json_parse(raw) {
11
+ try {
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+ function safe_stringify(value, space) {
18
+ try {
19
+ return JSON.stringify(value, null, space) ?? String(value);
20
+ } catch {
21
+ return String(value);
22
+ }
23
+ }
24
+ function truncate_text(text, max_chars) {
25
+ if (text.length <= max_chars) {
26
+ return text;
27
+ }
28
+ const omitted = text.length - max_chars;
29
+ return `${text.slice(0, max_chars)}
30
+ [... truncated, ${omitted} chars omitted ...]`;
31
+ }
32
+
33
+ // src/tools/guard.ts
34
+ var DEFAULT_MAX_OUTPUT_CHARS = 2e4;
35
+ var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
36
+ function resolve_safe_path(base_dir, target) {
37
+ const base = path.resolve(base_dir);
38
+ const resolved = path.resolve(base, target);
39
+ const relative = path.relative(base, resolved);
40
+ if (relative.startsWith("..") === true || path.isAbsolute(relative) === true) {
41
+ throw new Error(`path_escape: ${target} escapes ${base_dir}`);
42
+ }
43
+ return resolved;
44
+ }
45
+ function require_string_arg(args, key) {
46
+ const value = args[key];
47
+ if (typeof value !== "string" || value.length === 0) {
48
+ throw new Error(`missing_arg: ${key}`);
49
+ }
50
+ return value;
51
+ }
52
+ function optional_string_arg(args, key, fallback) {
53
+ const value = args[key];
54
+ if (typeof value === "string" && value.length > 0) {
55
+ return value;
56
+ }
57
+ return fallback;
58
+ }
59
+ function optional_number_arg(args, key, fallback) {
60
+ const value = args[key];
61
+ if (typeof value === "number" && Number.isFinite(value) === true) {
62
+ return value;
63
+ }
64
+ return fallback;
65
+ }
66
+ function optional_boolean_arg(args, key, fallback) {
67
+ const value = args[key];
68
+ if (typeof value === "boolean") {
69
+ return value;
70
+ }
71
+ return fallback;
72
+ }
73
+ var ToolTimeoutError = class extends Error {
74
+ constructor(label, timeout_ms) {
75
+ super(`timeout: ${label} exceeded ${timeout_ms}ms`);
76
+ this.name = "ToolTimeoutError";
77
+ }
78
+ };
79
+ function with_timeout(promise_factory, timeout_ms, label) {
80
+ const controller = new AbortController();
81
+ const holder = {};
82
+ const timeout_promise = new Promise((_resolve, reject) => {
83
+ holder.timer = setTimeout(() => {
84
+ controller.abort();
85
+ reject(new ToolTimeoutError(label, timeout_ms));
86
+ }, timeout_ms);
87
+ });
88
+ const raced = promise_factory(controller.signal);
89
+ raced.catch(() => void 0);
90
+ return Promise.race([raced, timeout_promise]).finally(() => {
91
+ const timer = holder.timer;
92
+ if (timer !== void 0) {
93
+ clearTimeout(timer);
94
+ }
95
+ });
96
+ }
97
+ function clamp_output(text, max_chars = DEFAULT_MAX_OUTPUT_CHARS) {
98
+ return truncate_text(text, max_chars);
99
+ }
100
+ function is_enoent(err) {
101
+ if (typeof err !== "object" || err === null) {
102
+ return false;
103
+ }
104
+ const code = err.code;
105
+ return code === "ENOENT";
106
+ }
107
+ function error_result(err) {
108
+ const message = err instanceof Error ? err.message : String(err);
109
+ return { ok: false, output: "", error: message };
110
+ }
111
+ async function capture_errors(run) {
112
+ try {
113
+ return await run();
114
+ } catch (err) {
115
+ return error_result(err);
116
+ }
117
+ }
118
+
119
+ // src/tools/builtin/fetch_url.ts
120
+ var DEFAULT_MAX_CHARS = 2e4;
121
+ var MAX_MAX_CHARS = 1e5;
122
+ var DEFAULT_TIMEOUT_MS = 2e4;
123
+ var MAX_TIMEOUT_MS = 6e4;
124
+ var USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36";
125
+ var parameters = {
126
+ type: "object",
127
+ properties: {
128
+ url: { type: "string", description: "Absolute http:// or https:// URL to GET" },
129
+ max_chars: { type: "number", description: "Clamp the body to this many chars (default 20000, max 100000)" },
130
+ timeout_ms: { type: "number", description: "Abort the request after this many ms (default 20000, max 60000)" }
131
+ },
132
+ required: ["url"],
133
+ additionalProperties: false
134
+ };
135
+ function clamp_int_arg(args, key, fallback, max) {
136
+ return Math.min(max, Math.max(1, Math.floor(optional_number_arg(args, key, fallback))));
137
+ }
138
+ function valid_http_url(raw) {
139
+ let parsed;
140
+ try {
141
+ parsed = new URL(raw);
142
+ } catch {
143
+ throw new Error(`invalid_url: ${raw}`);
144
+ }
145
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
146
+ throw new Error(`invalid_url: unsupported protocol ${parsed.protocol}`);
147
+ }
148
+ }
149
+ function compose_abort_signal(timeout_ms, external) {
150
+ const timeout_signal = AbortSignal.timeout(timeout_ms);
151
+ return external === void 0 ? timeout_signal : AbortSignal.any([timeout_signal, external]);
152
+ }
153
+ async function run_fetch_url(args, external) {
154
+ const url = require_string_arg(args, "url");
155
+ valid_http_url(url);
156
+ const max_chars = clamp_int_arg(args, "max_chars", DEFAULT_MAX_CHARS, MAX_MAX_CHARS);
157
+ const timeout_ms = clamp_int_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
158
+ const response = await fetch(url, {
159
+ redirect: "follow",
160
+ headers: { "user-agent": USER_AGENT },
161
+ signal: compose_abort_signal(timeout_ms, external)
162
+ });
163
+ const content_type = response.headers.get("content-type") ?? "";
164
+ if (response.ok === false) {
165
+ return { ok: false, output: "", error: `http_${response.status}` };
166
+ }
167
+ if (content_type.startsWith("image/") === true || content_type.startsWith("application/octet-stream") === true) {
168
+ return { ok: false, output: "", error: `unsupported_content_type: ${content_type}` };
169
+ }
170
+ const text = await response.text();
171
+ const marker = content_type.toLowerCase().includes("text/html") === true ? "[html content]\n" : "";
172
+ const body = clamp_output(`${marker}${text}`, max_chars);
173
+ return { ok: true, output: `${header_line(response, text)}
174
+ ${body}` };
175
+ }
176
+ function header_line(response, text) {
177
+ const content_type = response.headers.get("content-type") ?? "unknown";
178
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
179
+ const bytes = Number.isFinite(declared) === true ? declared : Buffer.byteLength(text, "utf8");
180
+ return `# ${response.status} ${content_type} (${bytes} bytes)`;
181
+ }
182
+ var fetch_url_tool = {
183
+ name: "fetch_url",
184
+ description: "GET an http(s) URL and return the response body text with a status header; rejects binary content.",
185
+ parameters,
186
+ execute: async (args, context) => capture_errors(async () => run_fetch_url(args, context.signal))
187
+ };
188
+
189
+ // src/tools/builtin/disk_usage.ts
190
+ var DEFAULT_MAX_ENTRIES = 25;
191
+ var MAX_MAX_ENTRIES = 200;
192
+ var DU_TIMEOUT_MS = 1e4;
193
+ var parameters2 = {
194
+ type: "object",
195
+ properties: {
196
+ path: { type: "string", description: "Directory to measure, relative to the working directory (default .)" },
197
+ max_entries: { type: "number", description: "Show at most this many entries (default 25, max 200)" }
198
+ },
199
+ additionalProperties: false
200
+ };
201
+ function run_du(entry_path) {
202
+ return new Promise((resolve) => {
203
+ execFile("du", ["-sb", entry_path], { timeout: DU_TIMEOUT_MS }, (err, stdout) => {
204
+ if (err !== null) {
205
+ resolve(-1);
206
+ return;
207
+ }
208
+ const parsed = Number.parseInt(stdout.trim().split(" ")[0] ?? "", 10);
209
+ resolve(Number.isFinite(parsed) === true ? parsed : -1);
210
+ });
211
+ });
212
+ }
213
+ async function measure_entries(root) {
214
+ const entries = await readdir(root, { withFileTypes: true });
215
+ const measured = [];
216
+ for (const entry of entries) {
217
+ const bytes = await run_du(path2.join(root, entry.name));
218
+ if (bytes < 0) {
219
+ return null;
220
+ }
221
+ measured.push({ name: entry.name, bytes });
222
+ }
223
+ return measured;
224
+ }
225
+ function format_usage(usage, max_entries) {
226
+ const sorted = [...usage].sort((a, b) => b.bytes - a.bytes);
227
+ const total = sorted.reduce((sum, entry) => sum + entry.bytes, 0);
228
+ const shown = sorted.slice(0, max_entries);
229
+ const lines = shown.map((entry) => `${entry.bytes} ${entry.name}`);
230
+ if (sorted.length > shown.length) {
231
+ lines.push(`(... ${sorted.length - shown.length} more entries suppressed)`);
232
+ }
233
+ lines.push(`TOTAL ${total}`);
234
+ return lines.join("\n");
235
+ }
236
+ async function run_disk_usage(args, work_dir) {
237
+ const target = optional_string_arg(args, "path", ".");
238
+ const max_entries = clamp_int_arg(args, "max_entries", DEFAULT_MAX_ENTRIES, MAX_MAX_ENTRIES);
239
+ const root = resolve_safe_path(work_dir, target);
240
+ const usage = await measure_entries(root);
241
+ if (usage === null) {
242
+ throw new Error("du_unavailable");
243
+ }
244
+ return format_usage(usage, max_entries);
245
+ }
246
+ var disk_usage_tool = {
247
+ name: "disk_usage",
248
+ description: "Measure depth-1 directory/file sizes with du -sb and report sorted sizes plus a total.",
249
+ parameters: parameters2,
250
+ execute: async (args, context) => capture_errors(async () => ({ ok: true, output: await run_disk_usage(args, context.work_dir) }))
251
+ };
252
+
253
+ // src/tools/builtin/docs_read.ts
254
+ import { readdirSync, readFileSync, statSync } from "fs";
255
+ import { fileURLToPath } from "url";
256
+ import path3 from "path";
257
+ var MAX_DOC_OUTPUT_CHARS = 3e4;
258
+ var DEFAULT_DOC_LIMIT = 400;
259
+ var DOCS_UNAVAILABLE = "docs_unavailable: set LICH_DOCS_DIR or install the lich package with docs";
260
+ var MAX_AVAILABLE_LISTED = 8;
261
+ var MAX_WALK_DEPTH = 4;
262
+ var PACKAGE_DOCS_CANDIDATES = ["../../docs/", "../docs/"];
263
+ var cached_root;
264
+ var cached_files;
265
+ var parameters3 = {
266
+ type: "object",
267
+ properties: {
268
+ path: { type: "string", description: "Doc path relative to the lich docs root (e.g. index.md or user-guide/cli.md)" },
269
+ offset: { type: "number", description: "1-based line number to start reading from" },
270
+ limit: { type: "number", description: "Maximum number of lines to return (default 400)" }
271
+ },
272
+ required: ["path"],
273
+ additionalProperties: false
274
+ };
275
+ function dir_with_index(candidate) {
276
+ try {
277
+ if (statSync(candidate).isDirectory() === false) {
278
+ return void 0;
279
+ }
280
+ if (statSync(path3.join(candidate, "index.md")).isFile() === false) {
281
+ return void 0;
282
+ }
283
+ return path3.resolve(candidate);
284
+ } catch {
285
+ return void 0;
286
+ }
287
+ }
288
+ function env_docs_root(value) {
289
+ if (value === void 0 || value.length === 0) {
290
+ return void 0;
291
+ }
292
+ const nested = dir_with_index(path3.join(value, "docs"));
293
+ if (nested !== void 0) {
294
+ return nested;
295
+ }
296
+ return dir_with_index(value);
297
+ }
298
+ function package_docs_root() {
299
+ for (const candidate of PACKAGE_DOCS_CANDIDATES) {
300
+ try {
301
+ const found = dir_with_index(fileURLToPath(new URL(candidate, import.meta.url)));
302
+ if (found !== void 0) {
303
+ return found;
304
+ }
305
+ } catch {
306
+ }
307
+ }
308
+ return void 0;
309
+ }
310
+ function default_resolve_docs_root(context) {
311
+ if (cached_root !== void 0) {
312
+ return cached_root;
313
+ }
314
+ const candidates = [
315
+ env_docs_root(context.env["LICH_DOCS_DIR"]),
316
+ dir_with_index(path3.join(context.work_dir, "docs")),
317
+ package_docs_root()
318
+ ];
319
+ for (const candidate of candidates) {
320
+ if (candidate !== void 0) {
321
+ cached_root = candidate;
322
+ return candidate;
323
+ }
324
+ }
325
+ return void 0;
326
+ }
327
+ var active_resolver = default_resolve_docs_root;
328
+ function resolve_docs_root(context) {
329
+ return active_resolver(context);
330
+ }
331
+ function collect_doc_entries(current, root, files, stack) {
332
+ let entries;
333
+ try {
334
+ entries = readdirSync(current.dir, { withFileTypes: true });
335
+ } catch {
336
+ return;
337
+ }
338
+ for (const entry of entries) {
339
+ const full = path3.join(current.dir, entry.name);
340
+ if (entry.isDirectory() === true) {
341
+ if (entry.name !== ".vitepress" && current.depth < MAX_WALK_DEPTH) {
342
+ stack.push({ dir: full, depth: current.depth + 1 });
343
+ }
344
+ continue;
345
+ }
346
+ if (entry.isFile() === true && entry.name.endsWith(".md") === true) {
347
+ files.push(path3.relative(root, full));
348
+ }
349
+ }
350
+ }
351
+ function walk_doc_files(root) {
352
+ const files = [];
353
+ const stack = [{ dir: path3.resolve(root), depth: 0 }];
354
+ while (stack.length > 0) {
355
+ const current = stack.pop();
356
+ if (current === void 0) {
357
+ continue;
358
+ }
359
+ collect_doc_entries(current, path3.resolve(root), files, stack);
360
+ }
361
+ files.sort();
362
+ return files;
363
+ }
364
+ function list_doc_files(root) {
365
+ if (cached_root === root && cached_files !== void 0) {
366
+ return cached_files;
367
+ }
368
+ const files = walk_doc_files(root);
369
+ if (cached_root === root) {
370
+ cached_files = files;
371
+ }
372
+ return files;
373
+ }
374
+ function require_docs_root(context) {
375
+ const root = resolve_docs_root(context);
376
+ if (root === void 0) {
377
+ throw new Error(DOCS_UNAVAILABLE);
378
+ }
379
+ return root;
380
+ }
381
+ function not_found_error(target, files) {
382
+ const listed = files.slice(0, MAX_AVAILABLE_LISTED).join(", ");
383
+ return `not_found: ${target} (available: ${listed.length > 0 ? listed : "none"})`;
384
+ }
385
+ function resolve_doc_rel(root, files, target) {
386
+ if (path3.isAbsolute(target) === true) {
387
+ throw new Error(`path_escape: ${target} is not relative to the docs root`);
388
+ }
389
+ const absolute = resolve_safe_path(root, target);
390
+ const rel = path3.relative(path3.resolve(root), absolute);
391
+ if (files.includes(rel) === true) {
392
+ return rel;
393
+ }
394
+ if (rel.endsWith(".md") === false && files.includes(`${rel}.md`) === true) {
395
+ return `${rel}.md`;
396
+ }
397
+ return void 0;
398
+ }
399
+ function clamp_line_arg(args, key, fallback) {
400
+ return Math.max(1, Math.trunc(optional_number_arg(args, key, fallback)));
401
+ }
402
+ function slice_lines(content, offset, limit) {
403
+ const lines = content.split("\n");
404
+ const start = Math.min(Math.max(0, offset - 1), lines.length);
405
+ const end = Math.min(lines.length, start + Math.max(0, limit));
406
+ const picked = [];
407
+ for (let index = start; index < end; index += 1) {
408
+ const line = lines[index];
409
+ if (line !== void 0) {
410
+ picked.push(line);
411
+ }
412
+ }
413
+ return picked.join("\n");
414
+ }
415
+ var docs_read_tool = {
416
+ name: "docs_read",
417
+ description: "Read one bundled lich doc file, optionally sliced by 1-based line offset and line limit.",
418
+ parameters: parameters3,
419
+ execute: async (args, context) => capture_errors(async () => {
420
+ const root = require_docs_root(context);
421
+ const target = require_string_arg(args, "path");
422
+ const files = list_doc_files(root);
423
+ const rel = resolve_doc_rel(root, files, target);
424
+ if (rel === void 0) {
425
+ return { ok: false, output: "", error: not_found_error(target, files) };
426
+ }
427
+ try {
428
+ const content = readFileSync(path3.join(root, rel), "utf8");
429
+ const body = slice_lines(content, clamp_line_arg(args, "offset", 1), clamp_line_arg(args, "limit", DEFAULT_DOC_LIMIT));
430
+ return { ok: true, output: clamp_output(`# lich doc: ${rel}
431
+ ${body}`, MAX_DOC_OUTPUT_CHARS) };
432
+ } catch (err) {
433
+ if (is_enoent(err) === true) {
434
+ return { ok: false, output: "", error: not_found_error(target, files) };
435
+ }
436
+ throw err;
437
+ }
438
+ })
439
+ };
440
+
441
+ // src/tools/builtin/docs_search.ts
442
+ import { readFileSync as readFileSync2 } from "fs";
443
+ import path4 from "path";
444
+ var DEFAULT_MAX_RESULTS = 5;
445
+ var MAX_RESULTS_CAP = 20;
446
+ var SNIPPET_CHARS = 160;
447
+ var MAX_TERM_HITS = 5;
448
+ var PHRASE_BONUS = 10;
449
+ var TITLE_SCORE = 3;
450
+ var FILENAME_SCORE = 2;
451
+ var BODY_SCORE = 1;
452
+ var MAX_DOC_OUTPUT_CHARS2 = 2e4;
453
+ var STOP_WORDS = /* @__PURE__ */ new Set(["the", "a", "an", "is", "how", "to", "in", "for", "of", "and", "or"]);
454
+ var cached_sections;
455
+ var cached_sections_list = [];
456
+ var parameters4 = {
457
+ type: "object",
458
+ properties: {
459
+ query: { type: "string", description: "Words to look for across all lich docs" },
460
+ max_results: { type: "number", description: "Maximum number of results (default 5, cap 20)" }
461
+ },
462
+ required: ["query"],
463
+ additionalProperties: false
464
+ };
465
+ function split_sections(rel_path, content) {
466
+ const sections = [];
467
+ const heading = `# ${rel_path}`;
468
+ const chunks = content.split(/^## /m);
469
+ for (let index = 0; index < chunks.length; index += 1) {
470
+ const chunk = chunks[index];
471
+ if (chunk === void 0 || chunk.length === 0) {
472
+ continue;
473
+ }
474
+ if (index === 0) {
475
+ sections.push({ rel_path, heading, body: chunk });
476
+ continue;
477
+ }
478
+ const newline = chunk.indexOf("\n");
479
+ const title = newline === -1 ? chunk : chunk.slice(0, newline);
480
+ const body = newline === -1 ? "" : chunk.slice(newline + 1);
481
+ sections.push({ rel_path, heading: `## ${title}`, body });
482
+ }
483
+ return sections;
484
+ }
485
+ function build_section_index(root, files) {
486
+ const sections = [];
487
+ for (const rel_path of files) {
488
+ try {
489
+ sections.push(...split_sections(rel_path, readFileSync2(path4.join(root, rel_path), "utf8")));
490
+ } catch {
491
+ }
492
+ }
493
+ return sections;
494
+ }
495
+ function load_sections(root, files) {
496
+ if (cached_sections === root) {
497
+ return cached_sections_list;
498
+ }
499
+ const sections = build_section_index(root, files);
500
+ cached_sections = root;
501
+ cached_sections_list = sections;
502
+ return sections;
503
+ }
504
+ function query_terms(query) {
505
+ const terms = [];
506
+ for (const raw of query.toLowerCase().split(/[^a-z0-9]+/i)) {
507
+ if (raw.length > 0 && STOP_WORDS.has(raw) === false) {
508
+ terms.push(raw);
509
+ }
510
+ }
511
+ return terms;
512
+ }
513
+ function count_term(haystack, term) {
514
+ let count = 0;
515
+ let position = haystack.indexOf(term);
516
+ while (position !== -1 && count < MAX_TERM_HITS) {
517
+ count += 1;
518
+ position = haystack.indexOf(term, position + term.length);
519
+ }
520
+ return count;
521
+ }
522
+ function score_sections(query, sections) {
523
+ const lower_query = query.toLowerCase();
524
+ const terms = query_terms(query);
525
+ const scored = [];
526
+ for (const section of sections) {
527
+ let score = 0;
528
+ const lower_heading = section.heading.toLowerCase();
529
+ const lower_body = section.body.toLowerCase();
530
+ for (const term of terms) {
531
+ if (lower_heading.includes(term) === true) {
532
+ score += TITLE_SCORE;
533
+ }
534
+ if (section.rel_path.toLowerCase().includes(term) === true) {
535
+ score += FILENAME_SCORE;
536
+ }
537
+ score += BODY_SCORE * count_term(lower_body, term);
538
+ }
539
+ if (terms.length > 0 && lower_body.includes(lower_query) === true) {
540
+ score += PHRASE_BONUS;
541
+ }
542
+ if (score > 0) {
543
+ scored.push({ ...section, score });
544
+ }
545
+ }
546
+ scored.sort((left, right) => right.score - left.score);
547
+ return scored;
548
+ }
549
+ function first_term_match(section, terms) {
550
+ for (const term of terms) {
551
+ const position = section.body.toLowerCase().indexOf(term);
552
+ if (position !== -1) {
553
+ const start = Math.max(0, position - 40);
554
+ return section.body.slice(start, start + SNIPPET_CHARS).replace(/\s+/g, " ").trim();
555
+ }
556
+ }
557
+ return section.body.replace(/\s+/g, " ").slice(0, SNIPPET_CHARS).trim();
558
+ }
559
+ function flatten_results(query, sections, max_results) {
560
+ const scored = score_sections(query, sections);
561
+ if (scored.length === 0) {
562
+ return `no results for: ${query}`;
563
+ }
564
+ const terms = query_terms(query);
565
+ const lines = [];
566
+ for (let index = 0; index < Math.min(scored.length, max_results); index += 1) {
567
+ const hit = scored[index];
568
+ if (hit === void 0) {
569
+ continue;
570
+ }
571
+ lines.push(`${index + 1}. ${hit.rel_path} \u2014 ${hit.heading} (score ${hit.score})`);
572
+ lines.push(` ${first_term_match(hit, terms)}`);
573
+ }
574
+ return lines.join("\n");
575
+ }
576
+ var docs_search_tool = {
577
+ name: "docs_search",
578
+ description: "Search across all bundled lich docs; returns scored section matches with short excerpts.",
579
+ parameters: parameters4,
580
+ execute: async (args, context) => capture_errors(async () => {
581
+ const root = require_docs_root(context);
582
+ const query = require_string_arg(args, "query");
583
+ const max_results = Math.min(
584
+ MAX_RESULTS_CAP,
585
+ Math.max(1, Math.trunc(optional_number_arg(args, "max_results", DEFAULT_MAX_RESULTS)))
586
+ );
587
+ const output = flatten_results(query, load_sections(root, list_doc_files(root)), max_results);
588
+ return { ok: true, output: clamp_output(output, MAX_DOC_OUTPUT_CHARS2) };
589
+ })
590
+ };
591
+
592
+ // src/tools/builtin/edit_file.ts
593
+ import { readFile, writeFile } from "fs/promises";
594
+ var parameters5 = {
595
+ type: "object",
596
+ properties: {
597
+ path: { type: "string", description: "File to edit, relative to the working directory" },
598
+ old_string: { type: "string", description: "Exact text to replace (must be unique unless replace_all)" },
599
+ new_string: { type: "string", description: "Replacement text" },
600
+ replace_all: { type: "boolean", description: "Replace every occurrence instead of failing on multiples" }
601
+ },
602
+ required: ["path", "old_string", "new_string"],
603
+ additionalProperties: false
604
+ };
605
+ function count_occurrences(content, needle) {
606
+ return content.split(needle).length - 1;
607
+ }
608
+ function replacement_for(content, old_string, new_string, replace_all) {
609
+ if (replace_all === true) {
610
+ return content.split(old_string).join(new_string);
611
+ }
612
+ return content.replace(old_string, new_string);
613
+ }
614
+ async function apply_edit(work_dir, target, old_string, new_string, replace_all) {
615
+ const file_path = resolve_safe_path(work_dir, target);
616
+ let content;
617
+ try {
618
+ content = await readFile(file_path, "utf8");
619
+ } catch (err) {
620
+ if (is_enoent(err) === true) {
621
+ throw new Error(`not_found: ${target}`);
622
+ }
623
+ throw err;
624
+ }
625
+ const occurrences = count_occurrences(content, old_string);
626
+ if (occurrences === 0) {
627
+ throw new Error("old_string_not_found");
628
+ }
629
+ if (occurrences > 1 && replace_all === false) {
630
+ throw new Error(`old_string_not_unique (${occurrences} occurrences)`);
631
+ }
632
+ const updated = replacement_for(content, old_string, new_string, replace_all);
633
+ await writeFile(file_path, updated, "utf8");
634
+ const replaced = replace_all === true ? occurrences : 1;
635
+ return { output: `edited ${target} (replaced ${replaced} occurrence(s))`, replaced };
636
+ }
637
+ var edit_file_tool = {
638
+ name: "edit_file",
639
+ description: "Replace an exact string in a text file; fails on missing or non-unique matches unless replace_all.",
640
+ parameters: parameters5,
641
+ execute: async (args, context) => capture_errors(async () => {
642
+ const target = require_string_arg(args, "path");
643
+ const old_string = require_string_arg(args, "old_string");
644
+ const new_string = require_string_arg(args, "new_string");
645
+ const replace_all = optional_boolean_arg(args, "replace_all", false);
646
+ const result = await apply_edit(context.work_dir, target, old_string, new_string, replace_all);
647
+ return { ok: true, output: result.output };
648
+ })
649
+ };
650
+
651
+ // src/tools/builtin/env_get.ts
652
+ var MAX_KEYS = 50;
653
+ var MAX_VALUE_CHARS = 2e3;
654
+ var SECRET_PATTERN = /(secret|token|password|key|credential|auth)/i;
655
+ var parameters6 = {
656
+ type: "object",
657
+ properties: {
658
+ keys: { type: "array", description: "Up to 50 variable names to inspect", items: { type: "string" } },
659
+ prefix: { type: "string", description: "Match variable names starting with this prefix" },
660
+ reveal: { type: "boolean", description: "Show values for non-secret keys (default false)" }
661
+ },
662
+ additionalProperties: false
663
+ };
664
+ function read_keys(args) {
665
+ const raw = args.keys;
666
+ if (Array.isArray(raw) === false) {
667
+ return [];
668
+ }
669
+ const names = [];
670
+ for (const item of raw) {
671
+ if (typeof item === "string" && item.length > 0 && names.length < MAX_KEYS) {
672
+ names.push(item);
673
+ }
674
+ }
675
+ return names;
676
+ }
677
+ function reveal_line(name) {
678
+ const value = process.env[name] ?? "";
679
+ if (SECRET_PATTERN.test(name) === true) {
680
+ return `${name}=<redacted: ${value.length} chars>`;
681
+ }
682
+ return `${name}=${value.slice(0, MAX_VALUE_CHARS)}`;
683
+ }
684
+ function summary_line(name) {
685
+ const value = process.env[name];
686
+ if (value === void 0) {
687
+ return `${name}=<unset>`;
688
+ }
689
+ return `${name}=set (${value.length} chars)`;
690
+ }
691
+ function prefix_names(prefix) {
692
+ const names = [];
693
+ for (const name of Object.keys(process.env)) {
694
+ if (name.startsWith(prefix) === true) {
695
+ names.push(name);
696
+ }
697
+ }
698
+ return names.sort();
699
+ }
700
+ function list_all_names() {
701
+ const names = Object.keys(process.env).sort();
702
+ return `${names.length} variables: ${names.join(", ")}`;
703
+ }
704
+ function run_env_get(args) {
705
+ const reveal = optional_boolean_arg(args, "reveal", false);
706
+ const prefix = typeof args.prefix === "string" ? args.prefix : "";
707
+ const keys = read_keys(args);
708
+ if (keys.length === 0 && prefix.length === 0) {
709
+ return list_all_names();
710
+ }
711
+ const names = keys.length > 0 ? keys : prefix_names(prefix);
712
+ const lines = names.map((name) => reveal === true ? reveal_line(name) : summary_line(name));
713
+ return lines.join("\n");
714
+ }
715
+ var env_get_tool = {
716
+ name: "env_get",
717
+ description: "Inspect environment variables: names by default, lengths unless reveal=true (secrets always masked).",
718
+ parameters: parameters6,
719
+ execute: async (args) => capture_errors(async () => ({ ok: true, output: run_env_get(args) }))
720
+ };
721
+
722
+ // src/tools/builtin/grep_files.ts
723
+ import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
724
+ import path5 from "path";
725
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".lich", ".cursor"]);
726
+ var MAX_FILE_BYTES = 1e6;
727
+ var SNIFF_BYTES = 1e3;
728
+ var DEFAULT_MAX_RESULTS2 = 200;
729
+ var parameters7 = {
730
+ type: "object",
731
+ properties: {
732
+ pattern: { type: "string", description: "Regular expression source to match against each line" },
733
+ path: { type: "string", description: "Directory or file to search, relative to the working directory (default .)" },
734
+ glob: { type: "string", description: "Simple filename filter like *.ts (suffix match only)" },
735
+ max_results: { type: "number", description: "Stop after this many matches (default 200)" }
736
+ },
737
+ required: ["pattern"],
738
+ additionalProperties: false
739
+ };
740
+ function glob_matcher(glob) {
741
+ const star = glob.indexOf("*");
742
+ if (star < 0) {
743
+ return (name) => name === glob;
744
+ }
745
+ const prefix = glob.slice(0, star);
746
+ const suffix = glob.slice(star + 1);
747
+ return (name) => name.startsWith(prefix) === true && name.endsWith(suffix) === true;
748
+ }
749
+ function is_binary(buffer) {
750
+ const limit = Math.min(SNIFF_BYTES, buffer.length);
751
+ for (let index = 0; index < limit; index += 1) {
752
+ if (buffer[index] === 0) {
753
+ return true;
754
+ }
755
+ }
756
+ return false;
757
+ }
758
+ async function read_if_text(file_path, size) {
759
+ if (size >= MAX_FILE_BYTES) {
760
+ return void 0;
761
+ }
762
+ try {
763
+ const content = await readFile2(file_path);
764
+ if (is_binary(content) === true) {
765
+ return void 0;
766
+ }
767
+ return content.toString("utf8").split("\n");
768
+ } catch {
769
+ return void 0;
770
+ }
771
+ }
772
+ function match_lines(lines, regex) {
773
+ const hits = [];
774
+ for (let index = 0; index < lines.length; index += 1) {
775
+ const line = lines[index];
776
+ if (line !== void 0 && regex.test(line) === true) {
777
+ hits.push({ line_no: index + 1, text: line.trim() });
778
+ }
779
+ }
780
+ return hits;
781
+ }
782
+ async function safe_readdir(dir) {
783
+ try {
784
+ return await readdir2(dir, { withFileTypes: true });
785
+ abort_marker: ;
786
+ } catch {
787
+ return void 0;
788
+ }
789
+ }
790
+ async function file_size(file_path) {
791
+ try {
792
+ const info = await stat(file_path);
793
+ return info.size;
794
+ } catch {
795
+ return 0;
796
+ }
797
+ }
798
+ async function scan_dir(frame, matcher) {
799
+ const files = [];
800
+ const dirs = [];
801
+ const entries = await safe_readdir(frame.dir);
802
+ if (entries === void 0) {
803
+ return { files, dirs };
804
+ }
805
+ for (const entry of entries) {
806
+ if (SKIP_DIRS.has(entry.name) === true) {
807
+ continue;
808
+ }
809
+ const full = path5.join(frame.dir, entry.name);
810
+ if (entry.isDirectory() === true) {
811
+ dirs.push({ dir: full, name: entry.name });
812
+ } else if (matcher(entry.name) === true) {
813
+ files.push({ dir: full, name: entry.name });
814
+ }
815
+ }
816
+ return { files, dirs };
817
+ }
818
+ async function search_file(frame, relative_root, regex, collected, max_results) {
819
+ const size = await file_size(frame.dir);
820
+ const lines = await read_if_text(frame.dir, size);
821
+ if (lines === void 0) {
822
+ return false;
823
+ }
824
+ const relative = path5.relative(relative_root, frame.dir);
825
+ for (const hit of match_lines(lines, regex)) {
826
+ collected.push(`${relative}:${hit.line_no}: ${hit.text}`);
827
+ if (collected.length >= max_results) {
828
+ return true;
829
+ }
830
+ }
831
+ return false;
832
+ }
833
+ async function search_tree(root, regex, matcher, max_results) {
834
+ const collected = [];
835
+ const stack = [{ dir: root, name: root }];
836
+ while (stack.length > 0 && collected.length < max_results) {
837
+ const frame = stack.pop();
838
+ if (frame === void 0) {
839
+ break;
840
+ }
841
+ const found = await scan_dir(frame, matcher);
842
+ for (const file of found.files) {
843
+ const hit_cap = await search_file(file, root, regex, collected, max_results);
844
+ if (hit_cap === true) {
845
+ break;
846
+ }
847
+ }
848
+ for (const dir of found.dirs) {
849
+ stack.push(dir);
850
+ }
851
+ }
852
+ return collected;
853
+ }
854
+ function finalize_output(matches, max_results) {
855
+ if (matches.length > max_results) {
856
+ const shown = matches.slice(0, max_results);
857
+ const suppressed = matches.length - max_results;
858
+ return shown.join("\n") + `
859
+ (... ${suppressed} more matches suppressed)`;
860
+ }
861
+ return matches.join("\n");
862
+ }
863
+ function search_file_direct(file_path, regex, collected, max_results) {
864
+ return search_file({ dir: file_path, name: path5.basename(file_path) }, path5.dirname(file_path), regex, collected, max_results);
865
+ }
866
+ async function collect_file_matches(root, regex, matcher, max_results) {
867
+ const collected = [];
868
+ if (matcher(path5.basename(root)) === true) {
869
+ await search_file_direct(root, regex, collected, max_results);
870
+ }
871
+ return collected;
872
+ }
873
+ async function run_grep(args, work_dir) {
874
+ const pattern = require_string_arg(args, "pattern");
875
+ const target = optional_string_arg(args, "path", ".");
876
+ const max_results = Math.max(1, Math.floor(optional_number_arg(args, "max_results", DEFAULT_MAX_RESULTS2)));
877
+ const glob = optional_string_arg(args, "glob", "");
878
+ let regex;
879
+ try {
880
+ regex = new RegExp(pattern);
881
+ } catch {
882
+ throw new Error(`invalid_regex: ${pattern}`);
883
+ }
884
+ const matcher = glob.length > 0 ? glob_matcher(glob) : () => true;
885
+ const root = resolve_safe_path(work_dir, target);
886
+ const root_stat = await stat(root);
887
+ const cap = max_results + 1;
888
+ const matches = root_stat.isDirectory() === true ? await search_tree(root, regex, matcher, cap) : await collect_file_matches(root, regex, matcher, cap);
889
+ return finalize_output(matches, max_results);
890
+ }
891
+ var grep_files_tool = {
892
+ name: "grep_files",
893
+ description: "Search files line-by-line with a regex, skipping node_modules/.git/dist and binary files.",
894
+ parameters: parameters7,
895
+ execute: async (args, context) => capture_errors(async () => {
896
+ const output = await run_grep(args, context.work_dir);
897
+ return { ok: true, output };
898
+ })
899
+ };
900
+
901
+ // src/tools/builtin/http_request.ts
902
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
903
+ var MAX_TIMEOUT_MS2 = 12e4;
904
+ var DEFAULT_MAX_CHARS2 = 2e4;
905
+ var MAX_MAX_CHARS2 = 1e5;
906
+ var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
907
+ var REPORTED_HEADERS = ["content-length", "ratelimit-remaining", "retry-after"];
908
+ var parameters8 = {
909
+ type: "object",
910
+ properties: {
911
+ url: { type: "string", description: "Absolute http:// or https:// URL to call" },
912
+ method: { type: "string", description: "HTTP method: GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS (default GET)" },
913
+ headers: { type: "object", description: "Request headers as name/value pairs (values stringified)" },
914
+ body: { type: "string", description: "Request body text (ignored for GET/HEAD)" },
915
+ timeout_ms: { type: "number", description: "Abort the request after this many ms (default 30000, max 120000)" },
916
+ max_chars: { type: "number", description: "Clamp the body to this many chars (default 20000, max 100000)" }
917
+ },
918
+ required: ["url"],
919
+ additionalProperties: false
920
+ };
921
+ function read_method(args) {
922
+ const method = optional_string_arg(args, "method", "GET").toUpperCase();
923
+ if (METHODS.has(method) === false) {
924
+ throw new Error(`invalid_method: ${method}`);
925
+ }
926
+ return method;
927
+ }
928
+ function read_headers(args) {
929
+ const raw = args.headers;
930
+ const headers = {};
931
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw) === true) {
932
+ return headers;
933
+ }
934
+ for (const [name, value] of Object.entries(raw)) {
935
+ if (value !== void 0 && value !== null) {
936
+ headers[name] = String(value);
937
+ }
938
+ }
939
+ return headers;
940
+ }
941
+ function header_lines(response) {
942
+ const lines = [];
943
+ for (const name of REPORTED_HEADERS) {
944
+ const value = response.headers.get(name);
945
+ if (value !== null) {
946
+ lines.push(`# header ${name}: ${value}`);
947
+ }
948
+ }
949
+ return lines;
950
+ }
951
+ async function run_http(args, external) {
952
+ const url = require_string_arg(args, "url");
953
+ valid_http_url(url);
954
+ const method = read_method(args);
955
+ const timeout_ms = clamp_int_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS2, MAX_TIMEOUT_MS2);
956
+ const max_chars = clamp_int_arg(args, "max_chars", DEFAULT_MAX_CHARS2, MAX_MAX_CHARS2);
957
+ const init = {
958
+ method,
959
+ headers: read_headers(args),
960
+ redirect: "follow",
961
+ signal: compose_abort_signal(timeout_ms, external)
962
+ };
963
+ if (method !== "GET" && method !== "HEAD") {
964
+ const body = optional_string_arg(args, "body", "");
965
+ if (body.length > 0) {
966
+ init.body = body;
967
+ }
968
+ }
969
+ const response = await fetch(url, init);
970
+ const content_type = response.headers.get("content-type") ?? "unknown";
971
+ const text = await response.text();
972
+ const sections = [
973
+ `# status ${response.status}`,
974
+ `# content-type ${content_type}`,
975
+ ...header_lines(response),
976
+ "",
977
+ clamp_output(text, max_chars)
978
+ ];
979
+ return { ok: true, output: sections.join("\n") };
980
+ }
981
+ var http_request_tool = {
982
+ name: "http_request",
983
+ description: "Call an http(s) API with a custom method/headers/body and return status, key headers, and the body.",
984
+ parameters: parameters8,
985
+ execute: async (args, context) => capture_errors(async () => run_http(args, context.signal))
986
+ };
987
+
988
+ // src/tools/builtin/list_dir.ts
989
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
990
+ import path6 from "path";
991
+ var MAX_ENTRIES = 500;
992
+ var MAX_DEPTH = 4;
993
+ var SKIP_ENTRIES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".lich", ".cursor"]);
994
+ var parameters9 = {
995
+ type: "object",
996
+ properties: {
997
+ path: { type: "string", description: "Directory to list, relative to the working directory (default .)" },
998
+ depth: { type: "number", description: "How many levels deep to list (1-4, default 1)" }
999
+ },
1000
+ additionalProperties: false
1001
+ };
1002
+ async function entry_size(file_path) {
1003
+ try {
1004
+ const info = await stat2(file_path);
1005
+ return info.size;
1006
+ } catch {
1007
+ return 0;
1008
+ }
1009
+ }
1010
+ async function safe_readdir2(dir) {
1011
+ try {
1012
+ return await readdir3(dir, { withFileTypes: true });
1013
+ } catch {
1014
+ return void 0;
1015
+ }
1016
+ }
1017
+ function sort_entries(entries) {
1018
+ return entries.filter((entry) => SKIP_ENTRIES.has(entry.name) === false).sort((a, b) => {
1019
+ const a_rank = a.isDirectory() === true ? 0 : 1;
1020
+ const b_rank = b.isDirectory() === true ? 0 : 1;
1021
+ if (a_rank !== b_rank) {
1022
+ return a_rank - b_rank;
1023
+ }
1024
+ return a.name.localeCompare(b.name);
1025
+ });
1026
+ }
1027
+ async function push_entries(lines, queue, entries, current) {
1028
+ for (const entry of sort_entries(entries)) {
1029
+ if (lines.length >= MAX_ENTRIES) {
1030
+ return true;
1031
+ }
1032
+ const full = path6.join(current.dir, entry.name);
1033
+ if (entry.isDirectory() === true) {
1034
+ lines.push(`d ${entry.name}/`);
1035
+ if (current.remaining > 1) {
1036
+ queue.push({ dir: full, remaining: current.remaining - 1 });
1037
+ }
1038
+ } else {
1039
+ lines.push(`- ${entry.name} (${await entry_size(full)} bytes)`);
1040
+ }
1041
+ }
1042
+ return false;
1043
+ }
1044
+ async function collect_lines(root, max_depth) {
1045
+ const lines = [];
1046
+ const queue = [{ dir: root, remaining: max_depth }];
1047
+ let truncated = false;
1048
+ while (queue.length > 0 && truncated === false) {
1049
+ const current = queue.shift();
1050
+ if (current === void 0) {
1051
+ break;
1052
+ }
1053
+ const entries = await safe_readdir2(current.dir);
1054
+ if (entries === void 0) {
1055
+ continue;
1056
+ }
1057
+ truncated = await push_entries(lines, queue, entries, current);
1058
+ }
1059
+ if (truncated === true) {
1060
+ lines.push(`(... truncated at ${MAX_ENTRIES} entries)`);
1061
+ }
1062
+ return lines;
1063
+ }
1064
+ function clamp_depth(depth) {
1065
+ return Math.min(MAX_DEPTH, Math.max(1, Math.floor(depth)));
1066
+ }
1067
+ var list_dir_tool = {
1068
+ name: "list_dir",
1069
+ description: "List a directory tree iteratively (dirs first, sizes for files), skipping node_modules/.git/dist.",
1070
+ parameters: parameters9,
1071
+ execute: async (args, context) => capture_errors(async () => {
1072
+ const target = optional_string_arg(args, "path", ".");
1073
+ const depth = clamp_depth(optional_number_arg(args, "depth", 1));
1074
+ const root = resolve_safe_path(context.work_dir, target);
1075
+ const lines = await collect_lines(root, depth);
1076
+ return { ok: true, output: lines.join("\n") };
1077
+ })
1078
+ };
1079
+
1080
+ // src/tools/builtin/process_list.ts
1081
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1082
+ var DEFAULT_MAX_RESULTS3 = 50;
1083
+ var MAX_MAX_RESULTS = 500;
1084
+ var CMDLINE_MAX_CHARS = 200;
1085
+ var PROC_DIR = "/proc";
1086
+ var PID_PATTERN = /^[0-9]+$/;
1087
+ var parameters10 = {
1088
+ type: "object",
1089
+ properties: {
1090
+ filter: { type: "string", description: "Case-insensitive substring match on the command line" },
1091
+ max_results: { type: "number", description: "Stop after this many processes (default 50, max 500)" }
1092
+ },
1093
+ additionalProperties: false
1094
+ };
1095
+ function read_proc_text(pid, file) {
1096
+ try {
1097
+ return readFileSync3(`${PROC_DIR}/${pid}/${file}`, "utf8");
1098
+ } catch {
1099
+ return "";
1100
+ }
1101
+ }
1102
+ function numeric_pids() {
1103
+ const pids = [];
1104
+ for (const name of readdirSync2(PROC_DIR)) {
1105
+ if (PID_PATTERN.test(name) === true) {
1106
+ pids.push(name);
1107
+ }
1108
+ }
1109
+ return pids;
1110
+ }
1111
+ function cmdline_text(pid) {
1112
+ const raw = read_proc_text(pid, "cmdline");
1113
+ const parts = raw.split("\0").filter((part) => part.length > 0);
1114
+ return parts.join(" ").slice(0, CMDLINE_MAX_CHARS);
1115
+ }
1116
+ function comm_text(pid) {
1117
+ return read_proc_text(pid, "comm").trim();
1118
+ }
1119
+ function collect_lines2(filter, max_results) {
1120
+ const lines = [];
1121
+ let total = 0;
1122
+ for (const pid of numeric_pids()) {
1123
+ const comm = comm_text(pid);
1124
+ const cmdline = cmdline_text(pid);
1125
+ const haystack = cmdline.length > 0 ? cmdline : comm;
1126
+ if (filter.length > 0 && haystack.toLowerCase().includes(filter) === false) {
1127
+ continue;
1128
+ }
1129
+ total += 1;
1130
+ if (lines.length < max_results) {
1131
+ lines.push(`${pid} ${comm} ${cmdline}`);
1132
+ }
1133
+ }
1134
+ return { lines, total };
1135
+ }
1136
+ function run_process_list(args) {
1137
+ const filter = optional_string_arg(args, "filter", "").toLowerCase();
1138
+ const max_results = clamp_int_arg(args, "max_results", DEFAULT_MAX_RESULTS3, MAX_MAX_RESULTS);
1139
+ let listing;
1140
+ try {
1141
+ listing = collect_lines2(filter, max_results);
1142
+ } catch {
1143
+ throw new Error("proc_unavailable");
1144
+ }
1145
+ const lines = [...listing.lines];
1146
+ if (listing.total > listing.lines.length) {
1147
+ lines.push(`(... ${listing.total - listing.lines.length} more processes suppressed)`);
1148
+ }
1149
+ if (lines.length === 0) {
1150
+ lines.push("no matching processes");
1151
+ }
1152
+ return clamp_output(lines.join("\n"));
1153
+ }
1154
+ var process_list_tool = {
1155
+ name: "process_list",
1156
+ description: "Snapshot running processes from /proc as pid/comm/cmdline rows with an optional filter.",
1157
+ parameters: parameters10,
1158
+ execute: async (args) => capture_errors(async () => ({ ok: true, output: run_process_list(args) }))
1159
+ };
1160
+
1161
+ // src/tools/builtin/read_file.ts
1162
+ import { readFile as readFile3 } from "fs/promises";
1163
+ var MAX_READ_CHARS = 256e3;
1164
+ var parameters11 = {
1165
+ type: "object",
1166
+ properties: {
1167
+ path: { type: "string", description: "File to read, relative to the working directory" },
1168
+ offset: { type: "number", description: "1-based line number to start reading from" },
1169
+ limit: { type: "number", description: "Maximum number of lines to return" }
1170
+ },
1171
+ required: ["path"],
1172
+ additionalProperties: false
1173
+ };
1174
+ function slice_lines2(content, offset, limit) {
1175
+ const lines = content.split("\n");
1176
+ const start = Math.max(0, offset - 1);
1177
+ return lines.slice(start, start + Math.max(0, limit)).join("\n");
1178
+ }
1179
+ async function read_target(args, work_dir, target) {
1180
+ const file_path = resolve_safe_path(work_dir, target);
1181
+ const content = await readFile3(file_path, "utf8");
1182
+ const offset = optional_number_arg(args, "offset", 1);
1183
+ const limit = optional_number_arg(args, "limit", Number.MAX_SAFE_INTEGER);
1184
+ return clamp_output(slice_lines2(content, offset, limit), MAX_READ_CHARS);
1185
+ }
1186
+ var read_file_tool = {
1187
+ name: "read_file",
1188
+ description: "Read a UTF-8 text file, optionally starting at a 1-based line offset with a line limit.",
1189
+ parameters: parameters11,
1190
+ execute: async (args, context) => capture_errors(async () => {
1191
+ const target = require_string_arg(args, "path");
1192
+ try {
1193
+ const output = await read_target(args, context.work_dir, target);
1194
+ return { ok: true, output };
1195
+ } catch (err) {
1196
+ if (is_enoent(err) === true) {
1197
+ return { ok: false, output: "", error: `not_found: ${target}` };
1198
+ }
1199
+ throw err;
1200
+ }
1201
+ })
1202
+ };
1203
+
1204
+ // src/tools/builtin/terminal.ts
1205
+ import { spawn } from "child_process";
1206
+ var MAX_STREAM_CHARS = 5e4;
1207
+ var DEFAULT_TIMEOUT_MS3 = 6e4;
1208
+ var MAX_TIMEOUT_MS3 = 3e5;
1209
+ var parameters12 = {
1210
+ type: "object",
1211
+ properties: {
1212
+ command: { type: "string", description: "Shell command to run via bash -lc" },
1213
+ timeout_ms: { type: "number", description: "Kill the command after this many ms (default 60000, max 300000)" }
1214
+ },
1215
+ required: ["command"],
1216
+ additionalProperties: false
1217
+ };
1218
+ function stream_chunk(current, chunk) {
1219
+ if (current.text.length >= MAX_STREAM_CHARS) {
1220
+ return;
1221
+ }
1222
+ current.text = current.text + chunk.toString("utf8");
1223
+ if (current.text.length > MAX_STREAM_CHARS) {
1224
+ current.text = current.text.slice(0, MAX_STREAM_CHARS);
1225
+ }
1226
+ }
1227
+ function clamp_timeout(raw) {
1228
+ return Math.min(MAX_TIMEOUT_MS3, Math.max(1, Math.floor(raw)));
1229
+ }
1230
+ function wire_kill(child, timeout_signal, external) {
1231
+ timeout_signal.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
1232
+ external?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
1233
+ }
1234
+ function wait_close(child) {
1235
+ return new Promise((resolve) => {
1236
+ child.on("close", (code) => resolve(code ?? -1));
1237
+ child.on("error", () => resolve(-1));
1238
+ });
1239
+ }
1240
+ async function run_command(command, work_dir, env, timeout_ms, external) {
1241
+ const stdout = { text: "" };
1242
+ const stderr = { text: "" };
1243
+ const child = spawn("bash", ["-lc", command], { cwd: work_dir, env: { ...process.env, ...env } });
1244
+ child.stdout.on("data", (chunk) => stream_chunk(stdout, chunk));
1245
+ child.stderr.on("data", (chunk) => stream_chunk(stderr, chunk));
1246
+ const close_promise = wait_close(child);
1247
+ let timed_out = false;
1248
+ let exit_code;
1249
+ try {
1250
+ exit_code = await with_timeout((timeout_signal) => {
1251
+ wire_kill(child, timeout_signal, external);
1252
+ return close_promise;
1253
+ }, timeout_ms, "terminal");
1254
+ } catch (err) {
1255
+ if (err instanceof ToolTimeoutError === true) {
1256
+ timed_out = true;
1257
+ exit_code = await close_promise;
1258
+ } else {
1259
+ throw err;
1260
+ }
1261
+ }
1262
+ const output = `${stdout.text}${stderr.text}
1263
+ [exit ${exit_code}]`;
1264
+ return { output, exit_code, timed_out, cancelled: external?.aborted === true };
1265
+ }
1266
+ function terminal_result(outcome) {
1267
+ const result = {
1268
+ ok: outcome.exit_code === 0 && outcome.cancelled === false,
1269
+ output: clamp_output(outcome.output)
1270
+ };
1271
+ if (outcome.cancelled === true) {
1272
+ result.error = "cancelled";
1273
+ } else if (outcome.timed_out === true) {
1274
+ result.error = "timeout";
1275
+ }
1276
+ return result;
1277
+ }
1278
+ var terminal_tool = {
1279
+ name: "terminal",
1280
+ description: "Run a shell command with bash -lc and capture combined stdout/stderr plus the exit code.",
1281
+ parameters: parameters12,
1282
+ execute: async (args, context) => capture_errors(async () => {
1283
+ const command = require_string_arg(args, "command");
1284
+ const timeout_ms = clamp_timeout(optional_number_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS3));
1285
+ const outcome = await run_command(command, context.work_dir, context.env, timeout_ms, context.signal);
1286
+ return terminal_result(outcome);
1287
+ })
1288
+ };
1289
+
1290
+ // src/tools/builtin/web_search.ts
1291
+ var DEFAULT_MAX_RESULTS4 = 8;
1292
+ var MAX_RESULTS = 20;
1293
+ var DEFAULT_TIMEOUT_MS4 = 2e4;
1294
+ var MAX_TIMEOUT_MS4 = 6e4;
1295
+ var SEARCH_ENDPOINT = "https://html.duckduckgo.com/html/?q=";
1296
+ var REDIRECT_PREFIXES = ["//duckduckgo.com/l/?", "/l/?"];
1297
+ var RESULT_PATTERN = /<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
1298
+ var ENTITY_REPLACEMENTS = [
1299
+ ["&amp;", "&"],
1300
+ ["&lt;", "<"],
1301
+ ["&gt;", ">"],
1302
+ ["&quot;", '"'],
1303
+ ["&#x27;", "'"],
1304
+ ["&#39;", "'"]
1305
+ ];
1306
+ var parameters13 = {
1307
+ type: "object",
1308
+ properties: {
1309
+ query: { type: "string", description: "Search query text" },
1310
+ max_results: { type: "number", description: "Maximum results to return (default 8, max 20)" },
1311
+ timeout_ms: { type: "number", description: "Abort the search after this many ms (default 20000, max 60000)" }
1312
+ },
1313
+ required: ["query"],
1314
+ additionalProperties: false
1315
+ };
1316
+ function decode_entities(text) {
1317
+ let decoded = text;
1318
+ for (const [entity, replacement] of ENTITY_REPLACEMENTS) {
1319
+ decoded = decoded.split(entity).join(replacement);
1320
+ }
1321
+ return decoded;
1322
+ }
1323
+ function strip_tags(raw_title) {
1324
+ return decode_entities(raw_title).replace(/<[^>]*>/g, "").trim();
1325
+ }
1326
+ function unwrap_redirect(href) {
1327
+ for (const prefix of REDIRECT_PREFIXES) {
1328
+ if (href.startsWith(prefix) === false) {
1329
+ continue;
1330
+ }
1331
+ const query = href.slice(prefix.length);
1332
+ const marker_index = query.indexOf("uddg=");
1333
+ if (marker_index < 0) {
1334
+ return href;
1335
+ }
1336
+ const encoded = query.slice(marker_index + 5).split("&")[0] ?? "";
1337
+ try {
1338
+ return decodeURIComponent(encoded);
1339
+ } catch {
1340
+ return encoded;
1341
+ }
1342
+ }
1343
+ return href;
1344
+ }
1345
+ function parse_results(html, cap) {
1346
+ const hits = [];
1347
+ for (const match of html.matchAll(RESULT_PATTERN)) {
1348
+ const href = match[1];
1349
+ const raw_title = match[2];
1350
+ if (href === void 0 || raw_title === void 0) {
1351
+ continue;
1352
+ }
1353
+ hits.push({ title: strip_tags(raw_title), url: unwrap_redirect(decode_entities(href)) });
1354
+ if (hits.length >= cap) {
1355
+ break;
1356
+ }
1357
+ }
1358
+ return hits;
1359
+ }
1360
+ function format_results(hits) {
1361
+ const lines = [];
1362
+ for (let index = 0; index < hits.length; index += 1) {
1363
+ const hit = hits[index];
1364
+ if (hit !== void 0) {
1365
+ lines.push(`${index + 1}. ${hit.title}`, ` ${hit.url}`);
1366
+ }
1367
+ }
1368
+ return lines.join("\n");
1369
+ }
1370
+ async function run_search(args, external) {
1371
+ const query = require_string_arg(args, "query");
1372
+ const max_results = clamp_int_arg(args, "max_results", DEFAULT_MAX_RESULTS4, MAX_RESULTS);
1373
+ const timeout_ms = clamp_int_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS4, MAX_TIMEOUT_MS4);
1374
+ try {
1375
+ const response = await fetch(`${SEARCH_ENDPOINT}${encodeURIComponent(query)}`, {
1376
+ headers: { "user-agent": USER_AGENT, "accept-language": "en" },
1377
+ signal: compose_abort_signal(timeout_ms, external)
1378
+ });
1379
+ if (response.ok === false) {
1380
+ return { ok: false, output: "", error: `search_failed: http_${response.status}` };
1381
+ }
1382
+ const hits = parse_results(await response.text(), max_results);
1383
+ return { ok: true, output: hits.length === 0 ? "no results" : format_results(hits) };
1384
+ } catch (err) {
1385
+ const message = err instanceof Error ? err.message : String(err);
1386
+ return { ok: false, output: "", error: `search_failed: ${message}` };
1387
+ }
1388
+ }
1389
+ var web_search_tool = {
1390
+ name: "web_search",
1391
+ description: "Search the web via DuckDuckGo's HTML endpoint (no api key) and return numbered title/url results.",
1392
+ parameters: parameters13,
1393
+ execute: async (args, context) => capture_errors(async () => run_search(args, context.signal))
1394
+ };
1395
+
1396
+ // src/tools/builtin/write_file.ts
1397
+ import { mkdir, writeFile as writeFile2 } from "fs/promises";
1398
+ import path7 from "path";
1399
+ var parameters14 = {
1400
+ type: "object",
1401
+ properties: {
1402
+ path: { type: "string", description: "File to write, relative to the working directory" },
1403
+ content: { type: "string", description: "Full content to write (overwrites existing file)" }
1404
+ },
1405
+ required: ["path", "content"],
1406
+ additionalProperties: false
1407
+ };
1408
+ function read_content_arg(args) {
1409
+ const content = args.content;
1410
+ if (typeof content !== "string") {
1411
+ throw new Error("missing_arg: content");
1412
+ }
1413
+ return content;
1414
+ }
1415
+ async function write_target(work_dir, target, content) {
1416
+ const file_path = resolve_safe_path(work_dir, target);
1417
+ await mkdir(path7.dirname(file_path), { recursive: true });
1418
+ await writeFile2(file_path, content, "utf8");
1419
+ return `wrote ${content.length} chars to ${target}`;
1420
+ }
1421
+ var write_file_tool = {
1422
+ name: "write_file",
1423
+ description: "Write (or overwrite) a UTF-8 text file, creating parent directories as needed.",
1424
+ parameters: parameters14,
1425
+ execute: async (args, context) => capture_errors(async () => {
1426
+ const target = require_string_arg(args, "path");
1427
+ const content = read_content_arg(args);
1428
+ const output = await write_target(context.work_dir, target, content);
1429
+ return { ok: true, output };
1430
+ })
1431
+ };
1432
+
1433
+ // src/util/log.ts
1434
+ var level_order = {
1435
+ debug: 10,
1436
+ info: 20,
1437
+ warn: 30,
1438
+ error: 40
1439
+ };
1440
+ var current_level = "info";
1441
+ function set_log_level(level) {
1442
+ current_level = level;
1443
+ }
1444
+ function log(level, message, data) {
1445
+ if (level_order[level] < level_order[current_level]) {
1446
+ return;
1447
+ }
1448
+ const line = `[lich:${level}] ${message}`;
1449
+ if (data === void 0) {
1450
+ console.error(line);
1451
+ return;
1452
+ }
1453
+ console.error(line, data);
1454
+ }
1455
+ var logger = {
1456
+ debug: (message, data) => log("debug", message, data),
1457
+ info: (message, data) => log("info", message, data),
1458
+ warn: (message, data) => log("warn", message, data),
1459
+ error: (message, data) => log("error", message, data)
1460
+ };
1461
+
1462
+ // src/tools/builtin/index.ts
1463
+ var core_tools = [
1464
+ read_file_tool,
1465
+ write_file_tool,
1466
+ edit_file_tool,
1467
+ list_dir_tool,
1468
+ terminal_tool,
1469
+ grep_files_tool,
1470
+ fetch_url_tool,
1471
+ web_search_tool,
1472
+ http_request_tool,
1473
+ process_list_tool,
1474
+ disk_usage_tool,
1475
+ env_get_tool
1476
+ ];
1477
+ function docs_tools(context) {
1478
+ if (resolve_docs_root(context) === void 0) {
1479
+ logger.debug("docs tools skipped: no docs root (LICH_DOCS_DIR, work_dir/docs, or package docs)");
1480
+ return [];
1481
+ }
1482
+ return [docs_read_tool, docs_search_tool];
1483
+ }
1484
+ function builtin_tools(context) {
1485
+ return [...core_tools, ...docs_tools(context)];
1486
+ }
1487
+ function register_builtin_tools(registry, context) {
1488
+ const merged_context = context ?? {
1489
+ work_dir: process.cwd(),
1490
+ env: { LICH_DOCS_DIR: process.env["LICH_DOCS_DIR"] ?? "" }
1491
+ };
1492
+ registry.register_toolset({ name: "builtin", tools: builtin_tools(merged_context) });
1493
+ }
1494
+
1495
+ // src/tools/registry.ts
1496
+ var ToolRegistry = class {
1497
+ tools = /* @__PURE__ */ new Map();
1498
+ register(tool) {
1499
+ if (this.tools.has(tool.name) === true) {
1500
+ throw new Error(`duplicate_tool: ${tool.name}`);
1501
+ }
1502
+ this.tools.set(tool.name, tool);
1503
+ }
1504
+ register_toolset(toolset) {
1505
+ for (const tool of toolset.tools) {
1506
+ this.register(tool);
1507
+ }
1508
+ }
1509
+ get(name) {
1510
+ return this.tools.get(name);
1511
+ }
1512
+ has(name) {
1513
+ return this.tools.has(name);
1514
+ }
1515
+ list() {
1516
+ return [...this.tools.values()];
1517
+ }
1518
+ /** Map registered tools onto the provider-facing wire shape. */
1519
+ definitions() {
1520
+ return this.list().map((tool) => ({
1521
+ name: tool.name,
1522
+ description: tool.description,
1523
+ parameters: tool.parameters
1524
+ }));
1525
+ }
1526
+ };
1527
+ function default_tool_context(work_dir, env) {
1528
+ return { work_dir, env: env ?? {} };
1529
+ }
1530
+
1531
+ // src/tools/executor.ts
1532
+ function clamp_result(result) {
1533
+ return { ...result, output: clamp_output(result.output) };
1534
+ }
1535
+ function failure_result(err, context) {
1536
+ if (context.signal?.aborted === true) {
1537
+ return { ok: false, output: "", error: "cancelled" };
1538
+ }
1539
+ return error_result(err);
1540
+ }
1541
+ var ToolExecutor = class {
1542
+ registry;
1543
+ defaults;
1544
+ constructor(registry, defaults) {
1545
+ this.registry = registry;
1546
+ this.defaults = defaults ?? {};
1547
+ }
1548
+ async execute(name, args, context) {
1549
+ const tool = this.registry.get(name);
1550
+ if (tool === void 0) {
1551
+ return { ok: false, output: "", error: `unknown_tool: ${name}` };
1552
+ }
1553
+ const resolved = context ?? default_tool_context(this.defaults.work_dir ?? process.cwd(), this.defaults.env);
1554
+ if (resolved.signal?.aborted === true) {
1555
+ return { ok: false, output: "", error: "cancelled" };
1556
+ }
1557
+ return this.run_tool(tool, args, resolved);
1558
+ }
1559
+ async run_tool(tool, args, context) {
1560
+ const combined = new AbortController();
1561
+ const link_external = () => {
1562
+ combined.abort();
1563
+ };
1564
+ context.signal?.addEventListener("abort", link_external, { once: true });
1565
+ logger.debug(`tool_call_start: ${tool.name}`);
1566
+ try {
1567
+ const result = await with_timeout(
1568
+ (timeout_signal) => {
1569
+ timeout_signal.addEventListener("abort", () => {
1570
+ combined.abort();
1571
+ }, { once: true });
1572
+ return tool.execute(args, { ...context, signal: combined.signal });
1573
+ },
1574
+ DEFAULT_TOOL_TIMEOUT_MS,
1575
+ `tool:${tool.name}`
1576
+ );
1577
+ logger.debug(`tool_call_end: ${tool.name}`);
1578
+ return clamp_result(result);
1579
+ } catch (err) {
1580
+ logger.debug(`tool_call_error: ${tool.name}`);
1581
+ return failure_result(err, context);
1582
+ } finally {
1583
+ context.signal?.removeEventListener("abort", link_external);
1584
+ }
1585
+ }
1586
+ /** Render a result for a tool-role message: JSON on error, raw output otherwise. */
1587
+ static format_result(result) {
1588
+ if (result.error !== void 0 && result.error.length > 0) {
1589
+ return safe_stringify({ ok: result.ok, output: result.output, error: result.error });
1590
+ }
1591
+ return result.output;
1592
+ }
1593
+ };
1594
+
1595
+ // src/plugins/hooks.ts
1596
+ var SUMMARY_MAX_CHARS = 300;
1597
+ function clamp_summary(text) {
1598
+ return text.length > SUMMARY_MAX_CHARS ? text.slice(0, SUMMARY_MAX_CHARS) : text;
1599
+ }
1600
+ function pick_defined(hooks, pick) {
1601
+ const defined = [];
1602
+ for (const hooks_entry of hooks) {
1603
+ const hook = pick(hooks_entry);
1604
+ if (hook !== void 0) {
1605
+ defined.push(hook);
1606
+ }
1607
+ }
1608
+ return defined;
1609
+ }
1610
+ var HookedToolRunner = class {
1611
+ wrapped;
1612
+ before_hooks;
1613
+ after_hooks;
1614
+ run_start_hooks;
1615
+ run_end_hooks;
1616
+ constructor(wrapped, hooks) {
1617
+ this.wrapped = wrapped;
1618
+ this.before_hooks = pick_defined(hooks, (entry) => entry.before_tool_call);
1619
+ this.after_hooks = pick_defined(hooks, (entry) => entry.after_tool_call);
1620
+ this.run_start_hooks = pick_defined(hooks, (entry) => entry.on_run_start);
1621
+ this.run_end_hooks = pick_defined(hooks, (entry) => entry.on_run_end);
1622
+ }
1623
+ /** Run before hooks in order; the first {block: true} verdict wins. */
1624
+ async run_before_hooks(info, ctx) {
1625
+ for (const hook of this.before_hooks) {
1626
+ try {
1627
+ const verdict = await hook(info, ctx);
1628
+ if (verdict?.block === true) {
1629
+ return verdict;
1630
+ }
1631
+ } catch (hook_error) {
1632
+ logger.warn(`plugin before_tool_call hook threw for ${info.tool_name}; continuing`, hook_error);
1633
+ }
1634
+ }
1635
+ return {};
1636
+ }
1637
+ /** Fire-and-forget in spirit but awaited here so runs settle cleanly. */
1638
+ async run_after_hooks(info, ctx) {
1639
+ for (const hook of this.after_hooks) {
1640
+ try {
1641
+ await hook(info, ctx);
1642
+ } catch (hook_error) {
1643
+ logger.warn(`plugin after_tool_call hook threw for ${info.tool_name}; continuing`, hook_error);
1644
+ }
1645
+ }
1646
+ }
1647
+ async execute(name, args, context) {
1648
+ const info = { tool_name: name, args };
1649
+ const ctx = { work_dir: context?.work_dir ?? process.cwd() };
1650
+ const verdict = await this.run_before_hooks(info, ctx);
1651
+ if (verdict.block === true) {
1652
+ const reason = verdict.reason ?? "plugin-less";
1653
+ logger.info(`plugin blocked tool ${name}: ${reason}`);
1654
+ return { ok: false, output: "", error: `blocked_by_plugin: ${reason}` };
1655
+ }
1656
+ const result = await this.wrapped.execute(name, args, context);
1657
+ const after_info = {
1658
+ ...info,
1659
+ result_summary: clamp_summary(result.error ?? result.output)
1660
+ };
1661
+ await this.run_after_hooks(after_info, ctx);
1662
+ return result;
1663
+ }
1664
+ /** Best-effort on_run_start fan-out used by Agent.run; never throws. */
1665
+ async call_run_start(info, ctx) {
1666
+ for (const hook of this.run_start_hooks) {
1667
+ try {
1668
+ await hook(info, ctx);
1669
+ } catch (hook_error) {
1670
+ logger.warn("plugin on_run_start hook threw; continuing", hook_error);
1671
+ }
1672
+ }
1673
+ }
1674
+ /** Best-effort on_run_end fan-out used by Agent.run; never throws. */
1675
+ async call_run_end(info, ctx) {
1676
+ for (const hook of this.run_end_hooks) {
1677
+ try {
1678
+ await hook(info, ctx);
1679
+ } catch (hook_error) {
1680
+ logger.warn("plugin on_run_end hook threw; continuing", hook_error);
1681
+ }
1682
+ }
1683
+ }
1684
+ };
1685
+
1686
+ // src/plugins/loader.ts
1687
+ import { pathToFileURL } from "url";
1688
+ import path8 from "path";
1689
+ var MODULE_QUERY = /(\.mjs|\.js|\.ts|\.mts|\.cts|\.jsx|\.tsx)$/;
1690
+ function describe_error(error) {
1691
+ if (error instanceof Error) {
1692
+ return error.message;
1693
+ }
1694
+ return String(error);
1695
+ }
1696
+ function is_object_with_name(candidate) {
1697
+ if (typeof candidate !== "object" || candidate === null) {
1698
+ return false;
1699
+ }
1700
+ const plugin = candidate;
1701
+ return typeof plugin.name === "string" && plugin.name.length > 0;
1702
+ }
1703
+ function is_plugin_module(candidate) {
1704
+ if (is_object_with_name(candidate) === false) {
1705
+ return false;
1706
+ }
1707
+ const plugin = candidate;
1708
+ return plugin.tools !== void 0 || plugin.hooks !== void 0;
1709
+ }
1710
+ function extract_plugin(mod) {
1711
+ const module = mod;
1712
+ for (const candidate of [module?.default, module?.plugin]) {
1713
+ if (is_object_with_name(candidate) === true) {
1714
+ return candidate;
1715
+ }
1716
+ }
1717
+ if (is_plugin_module(mod) === true) {
1718
+ return mod;
1719
+ }
1720
+ return void 0;
1721
+ }
1722
+ async function load_one_entry(entry, base_dir) {
1723
+ const abs = path8.resolve(base_dir, entry);
1724
+ if (MODULE_QUERY.test(abs) === false) {
1725
+ throw new Error(`plugin_entry_not_a_module: ${entry}`);
1726
+ }
1727
+ const mod = await import(pathToFileURL(abs).href);
1728
+ const plugin = extract_plugin(mod);
1729
+ if (plugin === void 0) {
1730
+ throw new Error(`plugin_module_has_no_plugin_export: ${entry}`);
1731
+ }
1732
+ return { plugin, entry };
1733
+ }
1734
+ async function load_plugins(entries, base_dir) {
1735
+ const plugins = [];
1736
+ const errors = [];
1737
+ const seen = /* @__PURE__ */ new Set();
1738
+ for (const entry of entries) {
1739
+ if (entry.length === 0) {
1740
+ continue;
1741
+ }
1742
+ try {
1743
+ const loaded = await load_one_entry(entry, base_dir);
1744
+ if (seen.has(loaded.plugin.name) === true) {
1745
+ errors.push({ entry, error_message: `duplicate_plugin_name: ${loaded.plugin.name}` });
1746
+ continue;
1747
+ }
1748
+ seen.add(loaded.plugin.name);
1749
+ plugins.push(loaded);
1750
+ } catch (error) {
1751
+ errors.push({ entry, error_message: describe_error(error) });
1752
+ }
1753
+ }
1754
+ return { plugins, errors };
1755
+ }
1756
+ function plugin_errors_summary(errors) {
1757
+ return errors.map((error) => `${error.entry}: ${error.error_message}`).join("; ");
1758
+ }
1759
+
1760
+ // src/providers/types.ts
1761
+ var ProviderError = class extends Error {
1762
+ kind;
1763
+ provider_name;
1764
+ status;
1765
+ retry_after_ms;
1766
+ constructor(params) {
1767
+ super(params.message);
1768
+ this.name = "ProviderError";
1769
+ this.kind = params.kind;
1770
+ this.provider_name = params.provider_name;
1771
+ this.status = params.status;
1772
+ this.retry_after_ms = params.retry_after_ms;
1773
+ if (params.cause !== void 0) {
1774
+ this.cause = params.cause;
1775
+ }
1776
+ }
1777
+ };
1778
+
1779
+ // src/agent/config.ts
1780
+ import { z } from "zod";
1781
+ var provider_schema = z.object({
1782
+ kind: z.enum(["openai_compat", "anthropic", "ollama"]),
1783
+ name: z.string().min(1),
1784
+ model: z.string().min(1),
1785
+ base_url: z.string().optional(),
1786
+ api_key: z.string().optional(),
1787
+ api_key_env: z.string().optional(),
1788
+ timeout_ms: z.number().int().positive().optional(),
1789
+ /** Injectable fetch, mainly for tests; passes through untouched. */
1790
+ fetch_fn: z.custom(() => true).optional()
1791
+ }).passthrough();
1792
+ var agent_config_schema = z.object({
1793
+ system_prompt: z.string().optional(),
1794
+ max_turns: z.number().int().min(1).default(25),
1795
+ providers: z.array(provider_schema).min(1),
1796
+ work_dir: z.string().optional(),
1797
+ tools_enabled: z.union([z.literal("all"), z.array(z.string())]).default("all"),
1798
+ temperature: z.number().min(0).max(2).optional(),
1799
+ max_tokens: z.number().int().positive().optional(),
1800
+ context_budget_tokens: z.number().int().positive().default(1e5),
1801
+ compress_threshold: z.number().min(0.1).max(0.95).default(0.8),
1802
+ session_dir: z.string().optional(),
1803
+ terminal_timeout_ms: z.number().int().positive().default(6e4),
1804
+ /** Plugin entry module specifiers, relative to work_dir or absolute. */
1805
+ plugins: z.array(z.string()).default([]),
1806
+ log_level: z.enum(["debug", "info", "warn", "error"]).default("info")
1807
+ }).transform((config) => {
1808
+ const work_dir = config.work_dir ?? process.cwd();
1809
+ return {
1810
+ ...config,
1811
+ work_dir,
1812
+ providers: config.providers,
1813
+ session_dir: config.session_dir ?? `${work_dir}/.lich/sessions`
1814
+ };
1815
+ });
1816
+ function freeze_config(config) {
1817
+ Object.freeze(config);
1818
+ Object.freeze(config.providers);
1819
+ for (const provider of config.providers) {
1820
+ Object.freeze(provider);
1821
+ }
1822
+ return config;
1823
+ }
1824
+ function parse_agent_config(raw) {
1825
+ const config = agent_config_schema.parse(raw);
1826
+ const frozen = freeze_config(config);
1827
+ set_log_level(frozen.log_level);
1828
+ return frozen;
1829
+ }
1830
+
1831
+ // src/agent/events.ts
1832
+ var AgentEmitter = class {
1833
+ handlers = /* @__PURE__ */ new Set();
1834
+ on(handler) {
1835
+ this.handlers.add(handler);
1836
+ return () => {
1837
+ this.handlers.delete(handler);
1838
+ };
1839
+ }
1840
+ emit(event) {
1841
+ for (const handler of [...this.handlers]) {
1842
+ try {
1843
+ handler(event);
1844
+ } catch (handler_error) {
1845
+ logger.error("agent event handler threw", handler_error);
1846
+ }
1847
+ }
1848
+ }
1849
+ clear() {
1850
+ this.handlers.clear();
1851
+ }
1852
+ };
1853
+
1854
+ // src/util/sleep.ts
1855
+ function sleep(ms, signal) {
1856
+ return new Promise((resolve, reject) => {
1857
+ if (signal?.aborted === true) {
1858
+ reject(new Error("sleep_aborted"));
1859
+ return;
1860
+ }
1861
+ const on_abort = () => {
1862
+ clearTimeout(timer);
1863
+ reject(new Error("sleep_aborted"));
1864
+ };
1865
+ const timer = setTimeout(() => {
1866
+ signal?.removeEventListener("abort", on_abort);
1867
+ resolve();
1868
+ }, ms);
1869
+ signal?.addEventListener("abort", on_abort, { once: true });
1870
+ });
1871
+ }
1872
+
1873
+ // src/providers/failover.ts
1874
+ var DEFAULT_BACKOFF_BASE_MS = 500;
1875
+ var DEFAULT_BACKOFF_MAX_MS = 8e3;
1876
+ function classify_error(error) {
1877
+ if (error instanceof ProviderError) {
1878
+ return error.kind;
1879
+ }
1880
+ if (is_abort_like(error) === true || is_type_error(error) === true) {
1881
+ return "network";
1882
+ }
1883
+ return "unknown";
1884
+ }
1885
+ function compute_backoff_ms(attempt, base_ms = DEFAULT_BACKOFF_BASE_MS, max_ms = DEFAULT_BACKOFF_MAX_MS) {
1886
+ const exponential = Math.floor(base_ms * 2 ** attempt);
1887
+ const jitter = Math.floor(base_ms * attempt / 2);
1888
+ return Math.min(exponential + jitter, max_ms);
1889
+ }
1890
+ async function run_with_retries(fn, params) {
1891
+ const outcome = await execute_retry_loop(fn, params);
1892
+ if (outcome.ok === true) {
1893
+ return outcome.value;
1894
+ }
1895
+ throw outcome.error;
1896
+ }
1897
+ async function execute_retry_loop(fn, params) {
1898
+ let attempt = 1;
1899
+ while (attempt <= Math.max(params.max_attempts, 1)) {
1900
+ const outcome = await run_attempt(fn, attempt);
1901
+ if (outcome.ok === true) {
1902
+ return outcome;
1903
+ }
1904
+ if (caller_aborted(params.signal) === true) {
1905
+ return { ok: false, error: make_abort_error(outcome.error) };
1906
+ }
1907
+ const kind = classify_error(outcome.error);
1908
+ if (is_retryable_kind(kind) === false || attempt >= params.max_attempts) {
1909
+ return outcome;
1910
+ }
1911
+ const delay_ms = delay_for_error(outcome.error, attempt);
1912
+ params.on_retry?.(kind, attempt, delay_ms);
1913
+ await wait_out_delay(delay_ms, params.signal);
1914
+ if (params.signal?.aborted === true) {
1915
+ return { ok: false, error: make_abort_error(outcome.error) };
1916
+ }
1917
+ attempt += 1;
1918
+ }
1919
+ return { ok: false, error: make_abort_error(void 0) };
1920
+ }
1921
+ async function run_attempt(fn, attempt) {
1922
+ try {
1923
+ return { ok: true, value: await fn(attempt) };
1924
+ } catch (error) {
1925
+ return { ok: false, error };
1926
+ }
1927
+ }
1928
+ function is_retryable_kind(kind) {
1929
+ return kind === "rate_limit" || kind === "network";
1930
+ }
1931
+ function caller_aborted(signal) {
1932
+ return signal?.aborted === true;
1933
+ }
1934
+ function delay_for_error(error, attempt) {
1935
+ const backoff = compute_backoff_ms(attempt);
1936
+ if (error instanceof ProviderError && error.retry_after_ms !== void 0 && error.retry_after_ms > backoff) {
1937
+ return error.retry_after_ms;
1938
+ }
1939
+ return backoff;
1940
+ }
1941
+ async function wait_out_delay(delay_ms, signal) {
1942
+ if (delay_ms <= 0) {
1943
+ return;
1944
+ }
1945
+ try {
1946
+ await sleep(delay_ms, signal);
1947
+ } catch {
1948
+ }
1949
+ }
1950
+ function make_abort_error(cause) {
1951
+ const error = new Error("operation aborted before completion");
1952
+ error.name = "AbortError";
1953
+ if (cause !== void 0) {
1954
+ error.cause = cause;
1955
+ }
1956
+ return error;
1957
+ }
1958
+ function is_type_error(error) {
1959
+ return error instanceof TypeError;
1960
+ }
1961
+ function error_name(error) {
1962
+ if (typeof error === "object" && error !== null && "name" in error) {
1963
+ const name = error.name;
1964
+ return typeof name === "string" ? name : void 0;
1965
+ }
1966
+ return void 0;
1967
+ }
1968
+ function is_abort_like(error) {
1969
+ const name = error_name(error);
1970
+ return name === "AbortError" || name === "TimeoutError";
1971
+ }
1972
+
1973
+ // src/providers/anthropic.ts
1974
+ var DEFAULT_BASE_URL = "https://api.anthropic.com";
1975
+ var ANTHROPIC_VERSION = "2023-06-01";
1976
+ var DEFAULT_KEY_ENV = "ANTHROPIC_API_KEY";
1977
+ var MAX_ERROR_BODY_CHARS = 500;
1978
+ var OVERFLOW_BODY_PATTERN = /context|token|maximum/i;
1979
+ var OVERLOADED_STATUS = 529;
1980
+ var UNPARSEABLE_ARGS_NOTE = "[unparseable tool arguments]";
1981
+ var DEFAULT_MAX_TOKENS = 4096;
1982
+ var AnthropicProvider = class {
1983
+ name;
1984
+ model;
1985
+ config;
1986
+ constructor(config) {
1987
+ this.config = config;
1988
+ this.name = config.name;
1989
+ this.model = config.model;
1990
+ }
1991
+ async chat(messages, tools, options) {
1992
+ const api_key = resolve_api_key(this.config);
1993
+ if (api_key === void 0) {
1994
+ throw new ProviderError({
1995
+ kind: "auth",
1996
+ provider_name: this.config.name,
1997
+ message: `missing api key for provider "${this.config.name}" (set api_key or api_key_env, e.g. ${DEFAULT_KEY_ENV})`
1998
+ });
1999
+ }
2000
+ const response = await do_fetch(
2001
+ this.config.fetch_fn ?? fetch,
2002
+ build_endpoint(this.config),
2003
+ build_request_init(
2004
+ api_key,
2005
+ safe_stringify(build_request_body(this.config.model, messages, tools, options)),
2006
+ build_abort_signal(options, this.config.timeout_ms)
2007
+ ),
2008
+ this.config.name
2009
+ );
2010
+ if (response.ok === false) {
2011
+ throw await to_http_error(response, this.config.name);
2012
+ }
2013
+ return parse_chat_response(await read_success_json(response, this.config.name), this.config);
2014
+ }
2015
+ };
2016
+ function resolve_api_key(config) {
2017
+ const direct = config.api_key;
2018
+ if (direct !== void 0 && direct.length > 0) {
2019
+ return direct;
2020
+ }
2021
+ const env_name = config.api_key_env ?? DEFAULT_KEY_ENV;
2022
+ const from_env = process.env[env_name];
2023
+ if (from_env !== void 0 && from_env.length > 0) {
2024
+ return from_env;
2025
+ }
2026
+ return void 0;
2027
+ }
2028
+ function build_endpoint(config) {
2029
+ const base = config.base_url ?? DEFAULT_BASE_URL;
2030
+ return base.endsWith("/") === true ? `${base}v1/messages` : `${base}/v1/messages`;
2031
+ }
2032
+ function build_headers(api_key) {
2033
+ return {
2034
+ "content-type": "application/json",
2035
+ "x-api-key": api_key,
2036
+ "anthropic-version": ANTHROPIC_VERSION
2037
+ };
2038
+ }
2039
+ function build_abort_signal(options, timeout_ms) {
2040
+ const signals = [];
2041
+ if (timeout_ms !== void 0 && timeout_ms > 0) {
2042
+ signals.push(AbortSignal.timeout(timeout_ms));
2043
+ }
2044
+ if (options?.signal !== void 0) {
2045
+ signals.push(options.signal);
2046
+ }
2047
+ const [only_signal] = signals;
2048
+ if (only_signal !== void 0 && signals.length === 1) {
2049
+ return only_signal;
2050
+ }
2051
+ return AbortSignal.any(signals);
2052
+ }
2053
+ function build_request_body(model, messages, tools, options) {
2054
+ const body = {
2055
+ model,
2056
+ max_tokens: options?.max_tokens ?? DEFAULT_MAX_TOKENS,
2057
+ messages: to_anthropic_turns(messages)
2058
+ };
2059
+ const system_text = collect_system_text(messages);
2060
+ if (system_text !== void 0) {
2061
+ body.system = system_text;
2062
+ }
2063
+ if (tools.length > 0) {
2064
+ body.tools = to_anthropic_tools(tools);
2065
+ }
2066
+ if (options?.temperature !== void 0) {
2067
+ body.temperature = options.temperature;
2068
+ }
2069
+ return body;
2070
+ }
2071
+ function collect_system_text(messages) {
2072
+ const parts = [];
2073
+ for (const message of messages) {
2074
+ if (message.role === "system") {
2075
+ parts.push(message.content);
2076
+ }
2077
+ }
2078
+ const joined = parts.join("\n");
2079
+ return joined.length === 0 ? void 0 : joined;
2080
+ }
2081
+ function to_anthropic_turns(messages) {
2082
+ const turns = [];
2083
+ const pending_tool_results = [];
2084
+ for (const message of messages) {
2085
+ if (message.role === "system") {
2086
+ continue;
2087
+ }
2088
+ if (message.role === "tool") {
2089
+ pending_tool_results.push(tool_message_to_block(message));
2090
+ continue;
2091
+ }
2092
+ flush_tool_results(turns, pending_tool_results);
2093
+ if (message.role === "user") {
2094
+ turns.push({ role: "user", content: [{ type: "text", text: message.content }] });
2095
+ } else {
2096
+ turns.push({ role: "assistant", content: assistant_to_blocks(message) });
2097
+ }
2098
+ }
2099
+ flush_tool_results(turns, pending_tool_results);
2100
+ return turns;
2101
+ }
2102
+ function flush_tool_results(turns, pending_tool_results) {
2103
+ if (pending_tool_results.length === 0) {
2104
+ return;
2105
+ }
2106
+ turns.push({ role: "user", content: pending_tool_results.splice(0, pending_tool_results.length) });
2107
+ }
2108
+ function tool_message_to_block(message) {
2109
+ const block = {
2110
+ type: "tool_result",
2111
+ tool_use_id: message.tool_call_id,
2112
+ content: [{ type: "text", text: message.content }]
2113
+ };
2114
+ if (message.is_error === true) {
2115
+ return { ...block, is_error: true };
2116
+ }
2117
+ return block;
2118
+ }
2119
+ function assistant_to_blocks(message) {
2120
+ const blocks = [];
2121
+ if (message.content.length > 0) {
2122
+ blocks.push({ type: "text", text: message.content });
2123
+ }
2124
+ for (const tool_call of message.tool_calls ?? []) {
2125
+ blocks.push({ type: "tool_use", id: tool_call.id, name: tool_call.name, input: tool_call.args });
2126
+ }
2127
+ if (blocks.length === 0) {
2128
+ blocks.push({ type: "text", text: "" });
2129
+ }
2130
+ return blocks;
2131
+ }
2132
+ function to_anthropic_tools(tools) {
2133
+ return tools.map((tool) => ({
2134
+ name: tool.name,
2135
+ description: tool.description,
2136
+ input_schema: tool.parameters
2137
+ }));
2138
+ }
2139
+ function build_request_init(api_key, body, signal) {
2140
+ return {
2141
+ method: "POST",
2142
+ headers: build_headers(api_key),
2143
+ body,
2144
+ ...signal !== void 0 ? { signal } : {}
2145
+ };
2146
+ }
2147
+ async function do_fetch(fetch_fn, url, init, provider_name) {
2148
+ try {
2149
+ return await fetch_fn(url, init);
2150
+ } catch (error) {
2151
+ const label = is_abort_like2(error) === true ? "request aborted or timed out" : "fetch failed";
2152
+ throw new ProviderError({
2153
+ kind: "network",
2154
+ provider_name,
2155
+ message: `${label}: ${describe_error2(error)}`,
2156
+ cause: error
2157
+ });
2158
+ }
2159
+ }
2160
+ async function read_response_text(response, provider_name) {
2161
+ try {
2162
+ return await response.text();
2163
+ } catch (error) {
2164
+ throw new ProviderError({
2165
+ kind: "network",
2166
+ provider_name,
2167
+ message: `failed to read response body: ${describe_error2(error)}`,
2168
+ cause: error
2169
+ });
2170
+ }
2171
+ }
2172
+ async function read_success_json(response, provider_name) {
2173
+ const text = await read_response_text(response, provider_name);
2174
+ const dto = safe_json_parse(text);
2175
+ if (dto === void 0) {
2176
+ throw new ProviderError({
2177
+ kind: "bad_request",
2178
+ provider_name,
2179
+ message: `unparseable success response: ${truncate_text(text, MAX_ERROR_BODY_CHARS)}`
2180
+ });
2181
+ }
2182
+ return dto;
2183
+ }
2184
+ function parse_retry_after_ms(response) {
2185
+ const raw = response.headers.get("retry-after");
2186
+ if (raw === null) {
2187
+ return void 0;
2188
+ }
2189
+ const seconds = Number(raw);
2190
+ if (Number.isFinite(seconds) === false || seconds < 0) {
2191
+ return void 0;
2192
+ }
2193
+ return Math.round(seconds * 1e3);
2194
+ }
2195
+ function status_to_error_kind(status, body_text) {
2196
+ if (status === 401 || status === 403) {
2197
+ return "auth";
2198
+ }
2199
+ if (status === 429 || status === OVERLOADED_STATUS || status >= 500) {
2200
+ return "rate_limit";
2201
+ }
2202
+ if (status === 400 && OVERFLOW_BODY_PATTERN.test(body_text) === true) {
2203
+ return "overflow";
2204
+ }
2205
+ return "bad_request";
2206
+ }
2207
+ async function to_http_error(response, provider_name) {
2208
+ const body_text = truncate_text(await read_response_text(response, provider_name), MAX_ERROR_BODY_CHARS);
2209
+ const retry_after_ms = parse_retry_after_ms(response);
2210
+ return new ProviderError({
2211
+ kind: status_to_error_kind(response.status, body_text),
2212
+ provider_name,
2213
+ message: `${provider_name} http ${response.status}: ${body_text}`,
2214
+ status: response.status,
2215
+ ...retry_after_ms !== void 0 ? { retry_after_ms } : {}
2216
+ });
2217
+ }
2218
+ function parse_chat_response(dto, config) {
2219
+ return {
2220
+ message: parse_assistant_message(dto.content ?? []),
2221
+ usage: parse_usage(dto.usage),
2222
+ finish_reason: map_stop_reason(dto.stop_reason),
2223
+ model: dto.model ?? config.model,
2224
+ provider_name: config.name
2225
+ };
2226
+ }
2227
+ function parse_assistant_message(blocks) {
2228
+ const text_parts = [];
2229
+ const tool_calls = [];
2230
+ for (const block of blocks) {
2231
+ if (block.type === "text") {
2232
+ text_parts.push(block.text ?? "");
2233
+ } else if (block.type === "tool_use") {
2234
+ tool_calls.push(tool_use_to_call(block, text_parts));
2235
+ }
2236
+ }
2237
+ return {
2238
+ role: "assistant",
2239
+ content: text_parts.filter((part) => part.length > 0).join("\n"),
2240
+ ...tool_calls.length > 0 ? { tool_calls } : {}
2241
+ };
2242
+ }
2243
+ function tool_use_to_call(block, text_parts) {
2244
+ const args = block.input;
2245
+ if (is_record(args) === true) {
2246
+ return { id: block.id ?? "", name: block.name ?? "", args };
2247
+ }
2248
+ text_parts.push(UNPARSEABLE_ARGS_NOTE);
2249
+ return { id: block.id ?? "", name: block.name ?? "", args: {} };
2250
+ }
2251
+ function parse_usage(dto) {
2252
+ const prompt_tokens = dto?.input_tokens ?? 0;
2253
+ const completion_tokens = dto?.output_tokens ?? 0;
2254
+ return {
2255
+ prompt_tokens,
2256
+ completion_tokens,
2257
+ total_tokens: prompt_tokens + completion_tokens
2258
+ };
2259
+ }
2260
+ function map_stop_reason(raw) {
2261
+ if (raw === "end_turn") {
2262
+ return "stop";
2263
+ }
2264
+ if (raw === "tool_use") {
2265
+ return "tool_calls";
2266
+ }
2267
+ if (raw === "max_tokens") {
2268
+ return "length";
2269
+ }
2270
+ return "unknown";
2271
+ }
2272
+ function is_record(value) {
2273
+ return typeof value === "object" && value !== null;
2274
+ }
2275
+ function describe_error2(error) {
2276
+ return error instanceof Error ? error.message : String(error);
2277
+ }
2278
+ function error_name2(error) {
2279
+ if (typeof error === "object" && error !== null && "name" in error) {
2280
+ const name = error.name;
2281
+ return typeof name === "string" ? name : void 0;
2282
+ }
2283
+ return void 0;
2284
+ }
2285
+ function is_abort_like2(error) {
2286
+ const name = error_name2(error);
2287
+ return name === "AbortError" || name === "TimeoutError";
2288
+ }
2289
+
2290
+ // src/providers/ollama.ts
2291
+ var DEFAULT_BASE_URL2 = "http://localhost:11434";
2292
+ var MAX_ERROR_BODY_CHARS2 = 500;
2293
+ var OVERFLOW_BODY_PATTERN2 = /context|token|maximum|too long/i;
2294
+ var UNPARSEABLE_ARGS_NOTE2 = "[unparseable tool arguments]";
2295
+ var tool_call_counter = 0;
2296
+ var OllamaProvider = class {
2297
+ name;
2298
+ model;
2299
+ config;
2300
+ constructor(config) {
2301
+ this.config = config;
2302
+ this.name = config.name;
2303
+ this.model = config.model;
2304
+ }
2305
+ async chat(messages, tools, options) {
2306
+ const response = await do_fetch2(
2307
+ this.config.fetch_fn ?? fetch,
2308
+ build_endpoint2(this.config),
2309
+ build_request_init2(
2310
+ resolve_api_key2(this.config),
2311
+ safe_stringify(build_request_body2(this.config, messages, tools, options)),
2312
+ build_abort_signal2(options, this.config.timeout_ms)
2313
+ ),
2314
+ this.config.name
2315
+ );
2316
+ if (response.ok === false) {
2317
+ throw await to_http_error2(response, this.config.name);
2318
+ }
2319
+ return to_chat_response(await read_success_json2(response, this.config.name), this.config);
2320
+ }
2321
+ };
2322
+ function create_ollama_provider(config) {
2323
+ return new OllamaProvider(config);
2324
+ }
2325
+ function resolve_api_key2(config) {
2326
+ const from_named_env = config.api_key_env === void 0 ? void 0 : process.env[config.api_key_env];
2327
+ return first_non_empty([config.api_key, from_named_env]);
2328
+ }
2329
+ function first_non_empty(values) {
2330
+ for (const value of values) {
2331
+ if (value !== void 0 && value.length > 0) {
2332
+ return value;
2333
+ }
2334
+ }
2335
+ return void 0;
2336
+ }
2337
+ function build_endpoint2(config) {
2338
+ const base = config.base_url ?? DEFAULT_BASE_URL2;
2339
+ return base.endsWith("/") === true ? `${base}api/chat` : `${base}/api/chat`;
2340
+ }
2341
+ function build_headers2(api_key) {
2342
+ const headers = { "content-type": "application/json" };
2343
+ if (api_key !== void 0) {
2344
+ headers.authorization = `Bearer ${api_key}`;
2345
+ }
2346
+ return headers;
2347
+ }
2348
+ function build_abort_signal2(options, timeout_ms) {
2349
+ const signals = [];
2350
+ if (timeout_ms !== void 0 && timeout_ms > 0) {
2351
+ signals.push(AbortSignal.timeout(timeout_ms));
2352
+ }
2353
+ if (options?.signal !== void 0) {
2354
+ signals.push(options.signal);
2355
+ }
2356
+ const [only_signal] = signals;
2357
+ if (only_signal !== void 0 && signals.length === 1) {
2358
+ return only_signal;
2359
+ }
2360
+ return AbortSignal.any(signals);
2361
+ }
2362
+ function to_ollama_messages(messages) {
2363
+ const wire = [];
2364
+ for (const message of messages) {
2365
+ if (message.role === "system") {
2366
+ wire.push({ role: "system", content: message.content });
2367
+ } else if (message.role === "user") {
2368
+ wire.push({ role: "user", content: message.content });
2369
+ } else if (message.role === "assistant") {
2370
+ wire.push(assistant_to_wire(message));
2371
+ } else {
2372
+ wire.push(tool_to_wire(message));
2373
+ }
2374
+ }
2375
+ return wire;
2376
+ }
2377
+ function assistant_to_wire(message) {
2378
+ const wire_calls = (message.tool_calls ?? []).map((tool_call) => ({
2379
+ type: "function",
2380
+ function: { name: tool_call.name, arguments: tool_call.args }
2381
+ }));
2382
+ if (wire_calls.length === 0) {
2383
+ return { role: "assistant", content: message.content };
2384
+ }
2385
+ return { role: "assistant", content: message.content, tool_calls: wire_calls };
2386
+ }
2387
+ function tool_to_wire(message) {
2388
+ return { role: "tool", tool_name: message.name, content: message.content };
2389
+ }
2390
+ function to_ollama_tools(tools) {
2391
+ return tools.map((tool) => ({
2392
+ type: "function",
2393
+ function: { name: tool.name, description: tool.description, parameters: tool.parameters }
2394
+ }));
2395
+ }
2396
+ function build_request_body2(config, messages, tools, options) {
2397
+ const body = { model: config.model, messages: to_ollama_messages(messages), stream: false };
2398
+ const wire_tools = to_ollama_tools(tools);
2399
+ if (wire_tools.length > 0) {
2400
+ body.tools = wire_tools;
2401
+ }
2402
+ const wire_options = build_wire_options(options);
2403
+ if (wire_options !== void 0) {
2404
+ body.options = wire_options;
2405
+ }
2406
+ if (options?.think === true || config.think === true) {
2407
+ body.think = true;
2408
+ }
2409
+ if (config.keep_alive !== void 0) {
2410
+ body.keep_alive = config.keep_alive;
2411
+ }
2412
+ return body;
2413
+ }
2414
+ function build_wire_options(options) {
2415
+ const wire_options = {};
2416
+ if (options?.temperature !== void 0) {
2417
+ wire_options.temperature = options.temperature;
2418
+ }
2419
+ if (options?.max_tokens !== void 0) {
2420
+ wire_options.num_predict = options.max_tokens;
2421
+ }
2422
+ const has_any = wire_options.temperature !== void 0 || wire_options.num_predict !== void 0;
2423
+ return has_any === true ? wire_options : void 0;
2424
+ }
2425
+ function build_request_init2(api_key, body, signal) {
2426
+ return {
2427
+ method: "POST",
2428
+ headers: build_headers2(api_key),
2429
+ body,
2430
+ ...signal !== void 0 ? { signal } : {}
2431
+ };
2432
+ }
2433
+ async function do_fetch2(fetch_fn, url, init, provider_name) {
2434
+ try {
2435
+ return await fetch_fn(url, init);
2436
+ } catch (error) {
2437
+ const label = is_abort_like3(error) === true ? "request aborted or timed out" : "fetch failed";
2438
+ throw new ProviderError({
2439
+ kind: "network",
2440
+ provider_name,
2441
+ message: `${label}: ${describe_error3(error)}`,
2442
+ cause: error
2443
+ });
2444
+ }
2445
+ }
2446
+ async function read_response_text2(response, provider_name) {
2447
+ try {
2448
+ return await response.text();
2449
+ } catch (error) {
2450
+ throw new ProviderError({
2451
+ kind: "network",
2452
+ provider_name,
2453
+ message: `failed to read response body: ${describe_error3(error)}`,
2454
+ cause: error
2455
+ });
2456
+ }
2457
+ }
2458
+ async function read_success_json2(response, provider_name) {
2459
+ const text = await read_response_text2(response, provider_name);
2460
+ const dto = safe_json_parse(text);
2461
+ if (dto === void 0) {
2462
+ throw new ProviderError({
2463
+ kind: "bad_request",
2464
+ provider_name,
2465
+ message: `unparseable success response: ${truncate_text(text, MAX_ERROR_BODY_CHARS2)}`
2466
+ });
2467
+ }
2468
+ return dto;
2469
+ }
2470
+ function parse_retry_after_ms2(response) {
2471
+ const raw = response.headers.get("retry-after");
2472
+ if (raw === null) {
2473
+ return void 0;
2474
+ }
2475
+ const seconds = Number(raw);
2476
+ if (Number.isFinite(seconds) === false || seconds < 0) {
2477
+ return void 0;
2478
+ }
2479
+ return Math.round(seconds * 1e3);
2480
+ }
2481
+ function status_to_error_kind2(status, body_text) {
2482
+ if (status === 401 || status === 403) {
2483
+ return "auth";
2484
+ }
2485
+ if (status === 429 || status >= 500) {
2486
+ return "rate_limit";
2487
+ }
2488
+ if (status === 400 && OVERFLOW_BODY_PATTERN2.test(body_text) === true) {
2489
+ return "overflow";
2490
+ }
2491
+ return "bad_request";
2492
+ }
2493
+ async function to_http_error2(response, provider_name) {
2494
+ const body_text = truncate_text(await read_response_text2(response, provider_name), MAX_ERROR_BODY_CHARS2);
2495
+ const retry_after_ms = parse_retry_after_ms2(response);
2496
+ return new ProviderError({
2497
+ kind: status_to_error_kind2(response.status, body_text),
2498
+ provider_name,
2499
+ message: `${provider_name} http ${response.status}: ${body_text}`,
2500
+ status: response.status,
2501
+ ...retry_after_ms !== void 0 ? { retry_after_ms } : {}
2502
+ });
2503
+ }
2504
+ function to_chat_response(dto, config) {
2505
+ if (dto.error !== void 0 && dto.error.length > 0) {
2506
+ throw new ProviderError({
2507
+ kind: "bad_request",
2508
+ provider_name: config.name,
2509
+ message: `ollama reported an error in a 200 response: ${truncate_text(dto.error, MAX_ERROR_BODY_CHARS2)}`
2510
+ });
2511
+ }
2512
+ if (dto.message === void 0) {
2513
+ throw new ProviderError({
2514
+ kind: "bad_request",
2515
+ provider_name: config.name,
2516
+ message: "provider returned a success response without a message"
2517
+ });
2518
+ }
2519
+ const content_parts = [];
2520
+ const message_content = dto.message.content ?? "";
2521
+ if (message_content.length > 0) {
2522
+ content_parts.push(message_content);
2523
+ }
2524
+ const parsed_calls = parse_tool_calls(dto.message.tool_calls ?? [], content_parts);
2525
+ const has_tool_calls = parsed_calls.length > 0;
2526
+ return {
2527
+ message: {
2528
+ role: "assistant",
2529
+ content: content_parts.join("\n"),
2530
+ ...has_tool_calls === true ? { tool_calls: parsed_calls } : {}
2531
+ },
2532
+ usage: parse_usage2(dto),
2533
+ finish_reason: map_done_reason(dto.done_reason, has_tool_calls),
2534
+ model: dto.model ?? config.model,
2535
+ provider_name: config.name
2536
+ };
2537
+ }
2538
+ function next_tool_call_id() {
2539
+ tool_call_counter += 1;
2540
+ return `ollama_${Date.now().toString(36)}_${tool_call_counter}`;
2541
+ }
2542
+ function normalize_tool_arguments(raw_arguments, content_parts) {
2543
+ if (is_record2(raw_arguments) === true) {
2544
+ return raw_arguments;
2545
+ }
2546
+ if (typeof raw_arguments === "string") {
2547
+ const parsed = safe_json_parse(raw_arguments);
2548
+ if (is_record2(parsed) === true) {
2549
+ return parsed;
2550
+ }
2551
+ }
2552
+ content_parts.push(UNPARSEABLE_ARGS_NOTE2);
2553
+ return {};
2554
+ }
2555
+ function parse_tool_calls(raw_calls, content_parts) {
2556
+ const tool_calls = [];
2557
+ for (const raw_call of raw_calls) {
2558
+ const name = raw_call.function?.name ?? "";
2559
+ tool_calls.push({
2560
+ id: next_tool_call_id(),
2561
+ name,
2562
+ args: normalize_tool_arguments(raw_call.function?.arguments, content_parts)
2563
+ });
2564
+ }
2565
+ return tool_calls;
2566
+ }
2567
+ function parse_usage2(dto) {
2568
+ const prompt_tokens = dto.prompt_eval_count ?? 0;
2569
+ const completion_tokens = dto.eval_count ?? 0;
2570
+ return { prompt_tokens, completion_tokens, total_tokens: prompt_tokens + completion_tokens };
2571
+ }
2572
+ function map_done_reason(done_reason, has_tool_calls) {
2573
+ if (has_tool_calls === true) {
2574
+ return "tool_calls";
2575
+ }
2576
+ if (done_reason === "stop" || done_reason === "end_turn") {
2577
+ return "stop";
2578
+ }
2579
+ if (done_reason === "length" || done_reason === "max_tokens") {
2580
+ return "length";
2581
+ }
2582
+ return "unknown";
2583
+ }
2584
+ function is_record2(value) {
2585
+ return typeof value === "object" && value !== null;
2586
+ }
2587
+ function describe_error3(error) {
2588
+ return error instanceof Error ? error.message : String(error);
2589
+ }
2590
+ function error_name3(error) {
2591
+ if (typeof error === "object" && error !== null && "name" in error) {
2592
+ const name = error.name;
2593
+ return typeof name === "string" ? name : void 0;
2594
+ }
2595
+ return void 0;
2596
+ }
2597
+ function is_abort_like3(error) {
2598
+ const name = error_name3(error);
2599
+ return name === "AbortError" || name === "TimeoutError";
2600
+ }
2601
+
2602
+ // src/providers/openai.ts
2603
+ var DEFAULT_BASE_URL3 = "https://api.openai.com/v1";
2604
+ var WELL_KNOWN_HOST = "api.openai.com";
2605
+ var WELL_KNOWN_KEY_ENV = "OPENAI_API_KEY";
2606
+ var MAX_ERROR_BODY_CHARS3 = 500;
2607
+ var OVERFLOW_BODY_PATTERN3 = /context|token|length/i;
2608
+ var UNPARSEABLE_ARGS_NOTE3 = "[unparseable tool arguments]";
2609
+ var OpenAICompatProvider = class {
2610
+ name;
2611
+ model;
2612
+ config;
2613
+ constructor(config) {
2614
+ this.config = config;
2615
+ this.name = config.name;
2616
+ this.model = config.model;
2617
+ }
2618
+ async chat(messages, tools, options) {
2619
+ const api_key = resolve_api_key3(this.config);
2620
+ if (requires_api_key(this.config) === true && api_key === void 0) {
2621
+ throw new ProviderError({
2622
+ kind: "auth",
2623
+ provider_name: this.config.name,
2624
+ message: `missing api key for provider "${this.config.name}" (set api_key, api_key_env, or ${WELL_KNOWN_KEY_ENV})`
2625
+ });
2626
+ }
2627
+ const response = await do_fetch3(
2628
+ this.config.fetch_fn ?? fetch,
2629
+ build_endpoint3(this.config),
2630
+ build_request_init3(
2631
+ api_key,
2632
+ safe_stringify(build_request_body3(this.config.model, messages, tools, options)),
2633
+ build_abort_signal3(options, this.config.timeout_ms)
2634
+ ),
2635
+ this.config.name
2636
+ );
2637
+ if (response.ok === false) {
2638
+ throw await to_http_error3(response, this.config.name);
2639
+ }
2640
+ return parse_chat_response2(await read_success_json3(response, this.config.name), this.config);
2641
+ }
2642
+ };
2643
+ function url_host(url_text) {
2644
+ try {
2645
+ return new URL(url_text).host;
2646
+ } catch {
2647
+ return void 0;
2648
+ }
2649
+ }
2650
+ function is_well_known_host(config) {
2651
+ return url_host(config.base_url ?? DEFAULT_BASE_URL3) === WELL_KNOWN_HOST;
2652
+ }
2653
+ function requires_api_key(config) {
2654
+ return is_well_known_host(config);
2655
+ }
2656
+ function resolve_api_key3(config) {
2657
+ const from_named_env = config.api_key_env === void 0 ? void 0 : process.env[config.api_key_env];
2658
+ const from_well_known_env = is_well_known_host(config) === true ? process.env[WELL_KNOWN_KEY_ENV] : void 0;
2659
+ return first_non_empty2([config.api_key, from_named_env, from_well_known_env]);
2660
+ }
2661
+ function first_non_empty2(values) {
2662
+ for (const value of values) {
2663
+ if (value !== void 0 && value.length > 0) {
2664
+ return value;
2665
+ }
2666
+ }
2667
+ return void 0;
2668
+ }
2669
+ function build_endpoint3(config) {
2670
+ const base = config.base_url ?? DEFAULT_BASE_URL3;
2671
+ return base.endsWith("/") === true ? `${base}chat/completions` : `${base}/chat/completions`;
2672
+ }
2673
+ function build_headers3(api_key) {
2674
+ const headers = { "content-type": "application/json" };
2675
+ if (api_key !== void 0) {
2676
+ headers.authorization = `Bearer ${api_key}`;
2677
+ }
2678
+ return headers;
2679
+ }
2680
+ function build_abort_signal3(options, timeout_ms) {
2681
+ const signals = [];
2682
+ if (timeout_ms !== void 0 && timeout_ms > 0) {
2683
+ signals.push(AbortSignal.timeout(timeout_ms));
2684
+ }
2685
+ if (options?.signal !== void 0) {
2686
+ signals.push(options.signal);
2687
+ }
2688
+ const [only_signal] = signals;
2689
+ if (only_signal !== void 0 && signals.length === 1) {
2690
+ return only_signal;
2691
+ }
2692
+ return AbortSignal.any(signals);
2693
+ }
2694
+ function to_openai_messages(messages) {
2695
+ const wire = [];
2696
+ for (const message of messages) {
2697
+ if (message.role === "system") {
2698
+ wire.push({ role: "system", content: message.content });
2699
+ } else if (message.role === "user") {
2700
+ wire.push({ role: "user", content: message.content });
2701
+ } else if (message.role === "assistant") {
2702
+ wire.push(assistant_to_wire2(message));
2703
+ } else {
2704
+ wire.push(tool_to_wire2(message));
2705
+ }
2706
+ }
2707
+ return wire;
2708
+ }
2709
+ function assistant_to_wire2(message) {
2710
+ const wire_calls = (message.tool_calls ?? []).map((tool_call) => ({
2711
+ id: tool_call.id,
2712
+ type: "function",
2713
+ function: { name: tool_call.name, arguments: safe_stringify(tool_call.args) }
2714
+ }));
2715
+ if (wire_calls.length === 0) {
2716
+ return { role: "assistant", content: message.content };
2717
+ }
2718
+ return { role: "assistant", content: message.content, tool_calls: wire_calls };
2719
+ }
2720
+ function tool_to_wire2(message) {
2721
+ return { role: "tool", tool_call_id: message.tool_call_id, content: message.content };
2722
+ }
2723
+ function to_openai_tools(tools) {
2724
+ return tools.map((tool) => ({
2725
+ type: "function",
2726
+ function: { name: tool.name, description: tool.description, parameters: tool.parameters }
2727
+ }));
2728
+ }
2729
+ function build_request_body3(model, messages, tools, options) {
2730
+ const body = { model, messages: to_openai_messages(messages) };
2731
+ const wire_tools = to_openai_tools(tools);
2732
+ if (wire_tools.length > 0) {
2733
+ body.tools = wire_tools;
2734
+ }
2735
+ if (options?.temperature !== void 0) {
2736
+ body.temperature = options.temperature;
2737
+ }
2738
+ if (options?.max_tokens !== void 0) {
2739
+ body.max_tokens = options.max_tokens;
2740
+ }
2741
+ return body;
2742
+ }
2743
+ function build_request_init3(api_key, body, signal) {
2744
+ return {
2745
+ method: "POST",
2746
+ headers: build_headers3(api_key),
2747
+ body,
2748
+ ...signal !== void 0 ? { signal } : {}
2749
+ };
2750
+ }
2751
+ async function do_fetch3(fetch_fn, url, init, provider_name) {
2752
+ try {
2753
+ return await fetch_fn(url, init);
2754
+ } catch (error) {
2755
+ const label = is_abort_like4(error) === true ? "request aborted or timed out" : "fetch failed";
2756
+ throw new ProviderError({
2757
+ kind: "network",
2758
+ provider_name,
2759
+ message: `${label}: ${describe_error4(error)}`,
2760
+ cause: error
2761
+ });
2762
+ }
2763
+ }
2764
+ async function read_response_text3(response, provider_name) {
2765
+ try {
2766
+ return await response.text();
2767
+ } catch (error) {
2768
+ throw new ProviderError({
2769
+ kind: "network",
2770
+ provider_name,
2771
+ message: `failed to read response body: ${describe_error4(error)}`,
2772
+ cause: error
2773
+ });
2774
+ }
2775
+ }
2776
+ async function read_success_json3(response, provider_name) {
2777
+ const text = await read_response_text3(response, provider_name);
2778
+ const dto = safe_json_parse(text);
2779
+ if (dto === void 0) {
2780
+ throw new ProviderError({
2781
+ kind: "bad_request",
2782
+ provider_name,
2783
+ message: `unparseable success response: ${truncate_text(text, MAX_ERROR_BODY_CHARS3)}`
2784
+ });
2785
+ }
2786
+ return dto;
2787
+ }
2788
+ function parse_retry_after_ms3(response) {
2789
+ const raw = response.headers.get("retry-after");
2790
+ if (raw === null) {
2791
+ return void 0;
2792
+ }
2793
+ const seconds = Number(raw);
2794
+ if (Number.isFinite(seconds) === false || seconds < 0) {
2795
+ return void 0;
2796
+ }
2797
+ return Math.round(seconds * 1e3);
2798
+ }
2799
+ function status_to_error_kind3(status, body_text) {
2800
+ if (status === 401 || status === 403) {
2801
+ return "auth";
2802
+ }
2803
+ if (status === 429 || status >= 500) {
2804
+ return "rate_limit";
2805
+ }
2806
+ if (status === 400 && OVERFLOW_BODY_PATTERN3.test(body_text) === true) {
2807
+ return "overflow";
2808
+ }
2809
+ return "bad_request";
2810
+ }
2811
+ async function to_http_error3(response, provider_name) {
2812
+ const body_text = truncate_text(await read_response_text3(response, provider_name), MAX_ERROR_BODY_CHARS3);
2813
+ const retry_after_ms = parse_retry_after_ms3(response);
2814
+ return new ProviderError({
2815
+ kind: status_to_error_kind3(response.status, body_text),
2816
+ provider_name,
2817
+ message: `${provider_name} http ${response.status}: ${body_text}`,
2818
+ status: response.status,
2819
+ ...retry_after_ms !== void 0 ? { retry_after_ms } : {}
2820
+ });
2821
+ }
2822
+ function parse_chat_response2(dto, config) {
2823
+ const choice = dto.choices?.[0];
2824
+ if (choice === void 0 || choice.message === void 0) {
2825
+ throw new ProviderError({
2826
+ kind: "bad_request",
2827
+ provider_name: config.name,
2828
+ message: "provider returned a success response without choices"
2829
+ });
2830
+ }
2831
+ return {
2832
+ message: parse_assistant_message2(choice.message),
2833
+ usage: parse_usage3(dto.usage),
2834
+ finish_reason: map_finish_reason(choice.finish_reason),
2835
+ model: dto.model ?? config.model,
2836
+ provider_name: config.name
2837
+ };
2838
+ }
2839
+ function parse_assistant_message2(dto) {
2840
+ const content_parts = [];
2841
+ if (dto.content !== void 0 && dto.content !== null && dto.content.length > 0) {
2842
+ content_parts.push(dto.content);
2843
+ }
2844
+ const tool_calls = [];
2845
+ for (const raw_call of dto.tool_calls ?? []) {
2846
+ const raw_arguments = raw_call.function?.arguments ?? "";
2847
+ const parsed_arguments = raw_arguments.length === 0 ? {} : safe_json_parse(raw_arguments);
2848
+ if (is_record3(parsed_arguments) === true) {
2849
+ tool_calls.push({ id: raw_call.id ?? "", name: raw_call.function?.name ?? "", args: parsed_arguments });
2850
+ } else {
2851
+ tool_calls.push({ id: raw_call.id ?? "", name: raw_call.function?.name ?? "", args: {} });
2852
+ content_parts.push(UNPARSEABLE_ARGS_NOTE3);
2853
+ }
2854
+ }
2855
+ return {
2856
+ role: "assistant",
2857
+ content: content_parts.filter((part) => part.length > 0).join("\n"),
2858
+ ...tool_calls.length > 0 ? { tool_calls } : {}
2859
+ };
2860
+ }
2861
+ function parse_usage3(dto) {
2862
+ const prompt_tokens = dto?.prompt_tokens ?? 0;
2863
+ const completion_tokens = dto?.completion_tokens ?? 0;
2864
+ const total_tokens = dto?.total_tokens ?? prompt_tokens + completion_tokens;
2865
+ return { prompt_tokens, completion_tokens, total_tokens };
2866
+ }
2867
+ function map_finish_reason(raw) {
2868
+ if (raw === "stop") {
2869
+ return "stop";
2870
+ }
2871
+ if (raw === "tool_calls") {
2872
+ return "tool_calls";
2873
+ }
2874
+ if (raw === "length") {
2875
+ return "length";
2876
+ }
2877
+ return "unknown";
2878
+ }
2879
+ function is_record3(value) {
2880
+ return typeof value === "object" && value !== null;
2881
+ }
2882
+ function describe_error4(error) {
2883
+ return error instanceof Error ? error.message : String(error);
2884
+ }
2885
+ function error_name4(error) {
2886
+ if (typeof error === "object" && error !== null && "name" in error) {
2887
+ const name = error.name;
2888
+ return typeof name === "string" ? name : void 0;
2889
+ }
2890
+ return void 0;
2891
+ }
2892
+ function is_abort_like4(error) {
2893
+ const name = error_name4(error);
2894
+ return name === "AbortError" || name === "TimeoutError";
2895
+ }
2896
+
2897
+ // src/providers/router.ts
2898
+ var FAILOVER_MAX_ATTEMPTS = 3;
2899
+ var ProviderRouter = class {
2900
+ configs;
2901
+ cache = /* @__PURE__ */ new Map();
2902
+ constructor(configs) {
2903
+ if (configs.length === 0) {
2904
+ throw new Error("at least one provider is required");
2905
+ }
2906
+ this.configs = [...configs];
2907
+ }
2908
+ get(name) {
2909
+ if (this.cache.has(name) === true) {
2910
+ return this.cache.get(name);
2911
+ }
2912
+ const config = this.find_config(name);
2913
+ if (config === void 0) {
2914
+ return void 0;
2915
+ }
2916
+ const provider = build_provider(config);
2917
+ this.cache.set(name, provider);
2918
+ return provider;
2919
+ }
2920
+ list() {
2921
+ const providers = [];
2922
+ for (const config of this.configs) {
2923
+ const provider = this.get(config.name);
2924
+ if (provider !== void 0) {
2925
+ providers.push(provider);
2926
+ }
2927
+ }
2928
+ return providers;
2929
+ }
2930
+ default_provider() {
2931
+ const [first_config] = this.configs;
2932
+ if (first_config === void 0) {
2933
+ throw new Error("at least one provider is required");
2934
+ }
2935
+ const provider = this.get(first_config.name);
2936
+ if (provider === void 0) {
2937
+ throw new Error(`provider config "${first_config.name}" could not be built`);
2938
+ }
2939
+ return provider;
2940
+ }
2941
+ chat_with_failover(messages, tools, options) {
2942
+ return chat_with_failover(this, messages, tools, options);
2943
+ }
2944
+ find_config(name) {
2945
+ for (const config of this.configs) {
2946
+ if (config.name === name) {
2947
+ return config;
2948
+ }
2949
+ }
2950
+ return void 0;
2951
+ }
2952
+ };
2953
+ function build_provider(config) {
2954
+ if (config.kind === "anthropic") {
2955
+ return new AnthropicProvider(config);
2956
+ }
2957
+ if (config.kind === "ollama") {
2958
+ return create_ollama_provider(config);
2959
+ }
2960
+ return new OpenAICompatProvider(config);
2961
+ }
2962
+ async function chat_with_failover(router, messages, tools, options) {
2963
+ let last_error;
2964
+ for (const provider of router.list()) {
2965
+ if (options?.signal?.aborted === true) {
2966
+ throw last_error ?? make_router_abort_error();
2967
+ }
2968
+ const result = await attempt_provider(provider, messages, tools, options);
2969
+ if (result.ok === true) {
2970
+ return result.value;
2971
+ }
2972
+ last_error = result.error;
2973
+ log_fail_over(result.error);
2974
+ }
2975
+ throw last_error ?? new Error("no providers configured for failover");
2976
+ }
2977
+ function log_fail_over(error) {
2978
+ if (error.kind === "rate_limit" || error.kind === "network") {
2979
+ return;
2980
+ }
2981
+ logger.warn(`provider "${error.provider_name}" failed with ${error.kind}; failing over to next provider`);
2982
+ }
2983
+ function make_router_abort_error() {
2984
+ return new ProviderError({
2985
+ kind: "unknown",
2986
+ provider_name: "router",
2987
+ message: "aborted before any provider succeeded"
2988
+ });
2989
+ }
2990
+ async function attempt_provider(provider, messages, tools, options) {
2991
+ try {
2992
+ return {
2993
+ ok: true,
2994
+ value: await run_with_retries(() => provider.chat(messages, tools, options), {
2995
+ max_attempts: FAILOVER_MAX_ATTEMPTS,
2996
+ signal: options?.signal,
2997
+ on_retry: (kind, attempt, delay_ms) => {
2998
+ logger.warn(
2999
+ `provider "${provider.name}" ${kind} on attempt ${attempt}; retrying in ${delay_ms}ms`
3000
+ );
3001
+ }
3002
+ })
3003
+ };
3004
+ } catch (error) {
3005
+ return { ok: false, error: to_provider_error(error, provider.name) };
3006
+ }
3007
+ }
3008
+ function to_provider_error(error, fallback_name) {
3009
+ if (error instanceof ProviderError) {
3010
+ return error;
3011
+ }
3012
+ return new ProviderError({
3013
+ kind: "unknown",
3014
+ provider_name: fallback_name,
3015
+ message: error instanceof Error ? error.message : String(error),
3016
+ cause: error
3017
+ });
3018
+ }
3019
+
3020
+ // src/session/store.ts
3021
+ import { appendFile, mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
3022
+ import path9 from "path";
3023
+ var counter_state = { value: 0 };
3024
+ function slugify_label(label) {
3025
+ const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
3026
+ return slug.length > 0 ? `-${slug}` : "";
3027
+ }
3028
+ async function open_session(dir, label) {
3029
+ await mkdir2(dir, { recursive: true });
3030
+ counter_state.value += 1;
3031
+ const label_part = label === void 0 ? "" : slugify_label(label);
3032
+ const id = `${Date.now().toString(36)}-${counter_state.value}${label_part}`;
3033
+ const file_path = path9.join(dir, `${id}.jsonl`);
3034
+ return {
3035
+ id,
3036
+ path: file_path,
3037
+ append: async (record) => {
3038
+ await appendFile(file_path, `${safe_stringify(record)}
3039
+ `, "utf8");
3040
+ }
3041
+ };
3042
+ }
3043
+
3044
+ // src/context/tokens.ts
3045
+ var TOOL_MESSAGE_OVERHEAD_TOKENS = 8;
3046
+ function estimate_text_tokens(text) {
3047
+ return Math.ceil(text.length / 4);
3048
+ }
3049
+ function estimate_message_tokens(message) {
3050
+ const content_tokens = estimate_text_tokens(message.content);
3051
+ if (message.role === "assistant" && message.tool_calls !== void 0) {
3052
+ return content_tokens + estimate_text_tokens(safe_stringify(message.tool_calls));
3053
+ }
3054
+ if (message.role === "tool") {
3055
+ return content_tokens + TOOL_MESSAGE_OVERHEAD_TOKENS;
3056
+ }
3057
+ return content_tokens;
3058
+ }
3059
+ function estimate_messages_tokens(messages) {
3060
+ return messages.reduce((total, message) => total + estimate_message_tokens(message), 0);
3061
+ }
3062
+
3063
+ // src/context/compressor.ts
3064
+ var COMPRESSION_SYSTEM_PROMPT = "You compress agent conversation history into terse factual summaries. Preserve: goals, decisions, file paths, commands run, errors, open questions. Output plain text only.";
3065
+ var MAX_TRANSCRIPT_CHARS = 24e3;
3066
+ function should_compress(messages, budget_tokens, threshold) {
3067
+ return estimate_messages_tokens(messages) >= budget_tokens * threshold;
3068
+ }
3069
+ function format_history_line(message) {
3070
+ const rendered_calls = message.role === "assistant" && message.tool_calls !== void 0 ? ` tool_calls=${safe_stringify(message.tool_calls)}` : "";
3071
+ return `[${message.role}] ${message.content}${rendered_calls}`;
3072
+ }
3073
+ function build_summary_request(older, model_hint) {
3074
+ const transcript = older.map((message) => format_history_line(message)).join("\n");
3075
+ const hint = model_hint === void 0 ? "" : `
3076
+ (Continuing agent run as model: ${model_hint})`;
3077
+ return {
3078
+ role: "user",
3079
+ content: "Summarize the following earlier conversation so the agent can continue the task from the summary alone.\n\n" + truncate_text(transcript, MAX_TRANSCRIPT_CHARS)
3080
+ };
3081
+ }
3082
+ function log_compression_failure(error) {
3083
+ if (error instanceof ProviderError) {
3084
+ logger.warn(`context compression failed kind=${error.kind} provider=${error.provider_name}`, error);
3085
+ return;
3086
+ }
3087
+ logger.warn("context compression failed", error);
3088
+ }
3089
+ async function compress_messages(deps, messages, params) {
3090
+ const system_messages = messages.filter((message) => message.role === "system");
3091
+ const non_system = messages.filter((message) => message.role !== "system");
3092
+ const keep_recent = Math.max(0, params.keep_recent);
3093
+ const recent = non_system.slice(-keep_recent);
3094
+ const older = non_system.slice(0, Math.max(0, non_system.length - recent.length));
3095
+ if (older.length === 0) {
3096
+ return { messages: [...messages], summary_chars: 0 };
3097
+ }
3098
+ try {
3099
+ const result = await deps.chat(
3100
+ [{ role: "system", content: COMPRESSION_SYSTEM_PROMPT }, build_summary_request(older, params.model_hint)],
3101
+ [],
3102
+ { signal: params.signal }
3103
+ );
3104
+ const summary = result.message.content;
3105
+ const summary_message = {
3106
+ role: "user",
3107
+ content: `[context summary of earlier turns]
3108
+ ${summary}
3109
+ [end summary]`
3110
+ };
3111
+ return { messages: [...system_messages, summary_message, ...recent], summary_chars: summary.length };
3112
+ } catch (error) {
3113
+ log_compression_failure(error);
3114
+ return { messages: [...messages], summary_chars: 0 };
3115
+ }
3116
+ }
3117
+
3118
+ // src/agent/loop.ts
3119
+ var DEFAULT_COMPRESS_THRESHOLD = 0.8;
3120
+ var KEEP_RECENT_TURNS = 8;
3121
+ function turn_range(max_turns) {
3122
+ return Array.from({ length: Math.max(0, max_turns) }, (_unused, index) => index + 1);
3123
+ }
3124
+ function seed_system_prompt(messages, system_prompt) {
3125
+ const history = [...messages];
3126
+ if (system_prompt === void 0) {
3127
+ return history;
3128
+ }
3129
+ const system_index = history.findIndex((message) => message.role === "system");
3130
+ if (system_index === -1) {
3131
+ const seeded = { role: "system", content: system_prompt };
3132
+ return [seeded, ...history];
3133
+ }
3134
+ const existing = history[system_index];
3135
+ if (existing !== void 0 && existing.content === system_prompt) {
3136
+ return history;
3137
+ }
3138
+ const replaced = { role: "system", content: system_prompt };
3139
+ return [...history.slice(0, system_index), replaced, ...history.slice(system_index + 1)];
3140
+ }
3141
+ function format_tool_result_content(result) {
3142
+ if (result.error !== void 0) {
3143
+ return JSON.stringify({ ok: false, output: result.output, error: result.error });
3144
+ }
3145
+ return result.output;
3146
+ }
3147
+ async function run_tool_calls(deps, history, turn, calls, emitter) {
3148
+ for (const call of calls) {
3149
+ emitter?.emit({ type: "tool_call_start", turn, call });
3150
+ const result = await deps.tools.execute(call.name, call.args);
3151
+ const tool_message = {
3152
+ role: "tool",
3153
+ tool_call_id: call.id,
3154
+ name: call.name,
3155
+ content: format_tool_result_content(result)
3156
+ };
3157
+ if (result.ok !== true) {
3158
+ tool_message.is_error = true;
3159
+ }
3160
+ history.push(tool_message);
3161
+ emitter?.emit({ type: "tool_call_end", turn, call, result });
3162
+ }
3163
+ }
3164
+ async function call_chat(deps, history, params, emitter) {
3165
+ try {
3166
+ return await deps.chat(history, deps.definitions(), {
3167
+ temperature: params.temperature,
3168
+ max_tokens: params.max_tokens,
3169
+ signal: params.signal
3170
+ });
3171
+ } catch (error) {
3172
+ if (error instanceof ProviderError) {
3173
+ logger.error(`provider error kind=${error.kind} provider=${error.provider_name}`, error);
3174
+ } else {
3175
+ logger.error("agent chat call failed", error);
3176
+ }
3177
+ emitter?.emit({ type: "error", error });
3178
+ throw error;
3179
+ }
3180
+ }
3181
+ async function compress_if_needed(deps, history, params, emitter) {
3182
+ const budget_tokens = params.context_budget_tokens;
3183
+ if (budget_tokens === void 0) {
3184
+ return;
3185
+ }
3186
+ const threshold = params.compress_threshold ?? DEFAULT_COMPRESS_THRESHOLD;
3187
+ if (!should_compress(history, budget_tokens, threshold)) {
3188
+ return;
3189
+ }
3190
+ const non_system_count = history.filter((message) => message.role !== "system").length;
3191
+ if (non_system_count <= KEEP_RECENT_TURNS) {
3192
+ return;
3193
+ }
3194
+ emitter?.emit({ type: "compress_start", estimated_tokens: estimate_messages_tokens(history) });
3195
+ const outcome = await compress_messages(
3196
+ { chat: deps.chat },
3197
+ history,
3198
+ { budget_tokens, keep_recent: KEEP_RECENT_TURNS, signal: params.signal }
3199
+ );
3200
+ history.length = 0;
3201
+ for (const message of outcome.messages) {
3202
+ history.push(message);
3203
+ }
3204
+ emitter?.emit({ type: "compress_end", summary_chars: outcome.summary_chars });
3205
+ }
3206
+ function find_last_assistant(messages) {
3207
+ return [...messages].reverse().find((message) => message.role === "assistant");
3208
+ }
3209
+ async function run_conversation(deps, messages, params) {
3210
+ const history = seed_system_prompt(messages, params.system_prompt);
3211
+ const emitter = deps.emitter;
3212
+ for (const turn of turn_range(params.max_turns)) {
3213
+ if (params.signal?.aborted === true) {
3214
+ emitter?.emit({ type: "error", error: new DOMException("agent loop aborted", "AbortError") });
3215
+ return {
3216
+ messages: history,
3217
+ final: find_last_assistant(history),
3218
+ result: void 0,
3219
+ turns_used: turn - 1,
3220
+ stopped_reason: "aborted"
3221
+ };
3222
+ }
3223
+ emitter?.emit({ type: "turn_start", turn });
3224
+ await compress_if_needed(deps, history, params, emitter);
3225
+ emitter?.emit({ type: "llm_start", turn });
3226
+ const result = await call_chat(deps, history, params, emitter);
3227
+ emitter?.emit({ type: "llm_end", turn, result });
3228
+ history.push(result.message);
3229
+ const calls = result.message.tool_calls ?? [];
3230
+ if (calls.length === 0) {
3231
+ emitter?.emit({ type: "final", message: result.message, result });
3232
+ emitter?.emit({ type: "turn_end", turn });
3233
+ return { messages: history, final: result.message, result, turns_used: turn, stopped_reason: "final" };
3234
+ }
3235
+ await run_tool_calls(deps, history, turn, calls, emitter);
3236
+ }
3237
+ emitter?.emit({ type: "budget_exhausted", turns_used: params.max_turns });
3238
+ emitter?.emit({ type: "turn_end", turn: params.max_turns });
3239
+ return {
3240
+ messages: history,
3241
+ final: find_last_assistant(history),
3242
+ result: void 0,
3243
+ turns_used: params.max_turns,
3244
+ stopped_reason: "budget"
3245
+ };
3246
+ }
3247
+
3248
+ // src/agent/agent.ts
3249
+ var DEFAULT_AGENT_SYSTEM_PROMPT = "You are a capable, concise assistant. Use the available tools whenever they help you complete the user's task accurately, and report results plainly.";
3250
+ function filter_registry(base, enabled) {
3251
+ if (enabled === "all") {
3252
+ return base;
3253
+ }
3254
+ const allowed = new Set(enabled);
3255
+ const filtered = new ToolRegistry();
3256
+ for (const tool of base.list()) {
3257
+ if (allowed.has(tool.name) === true) {
3258
+ filtered.register(tool);
3259
+ }
3260
+ }
3261
+ return filtered;
3262
+ }
3263
+ function collect_usage(total) {
3264
+ return (event) => {
3265
+ if (event.type === "llm_end") {
3266
+ total.prompt_tokens += event.result.usage.prompt_tokens;
3267
+ total.completion_tokens += event.result.usage.completion_tokens;
3268
+ total.total_tokens += event.result.usage.total_tokens;
3269
+ }
3270
+ };
3271
+ }
3272
+ function append_meta(handle, meta) {
3273
+ return handle.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), kind: "meta", meta });
3274
+ }
3275
+ function register_plugin_tools(registry, plugins) {
3276
+ for (const loaded of plugins) {
3277
+ for (const tool of loaded.plugin.tools ?? []) {
3278
+ if (registry.has(tool.name) === true) {
3279
+ logger.warn(`plugin ${loaded.plugin.name} tool ${tool.name} already registered; skipping`);
3280
+ continue;
3281
+ }
3282
+ registry.register(tool);
3283
+ logger.info(`plugin ${loaded.plugin.name} registered tool ${tool.name}`);
3284
+ }
3285
+ }
3286
+ }
3287
+ function merge_plugin_hooks(plugins) {
3288
+ const hooks = [];
3289
+ for (const loaded of plugins) {
3290
+ if (loaded.plugin.hooks !== void 0) {
3291
+ hooks.push(loaded.plugin.hooks);
3292
+ }
3293
+ }
3294
+ return hooks;
3295
+ }
3296
+ var Agent = class {
3297
+ events;
3298
+ config;
3299
+ router;
3300
+ registry;
3301
+ executor;
3302
+ hook_runner;
3303
+ constructor(config, plugins = []) {
3304
+ this.config = config;
3305
+ this.events = new AgentEmitter();
3306
+ this.router = new ProviderRouter(config.providers);
3307
+ const base_registry = new ToolRegistry();
3308
+ register_builtin_tools(base_registry);
3309
+ this.registry = filter_registry(base_registry, config.tools_enabled);
3310
+ register_plugin_tools(this.registry, plugins);
3311
+ const base_executor = new ToolExecutor(this.registry, {
3312
+ work_dir: config.work_dir,
3313
+ env: { LICH_TERMINAL_TIMEOUT_MS: String(config.terminal_timeout_ms) }
3314
+ });
3315
+ const merged_hooks = merge_plugin_hooks(plugins);
3316
+ if (merged_hooks.length > 0) {
3317
+ this.hook_runner = new HookedToolRunner(base_executor, merged_hooks);
3318
+ this.executor = this.hook_runner;
3319
+ } else {
3320
+ this.hook_runner = void 0;
3321
+ this.executor = base_executor;
3322
+ }
3323
+ }
3324
+ async run(options) {
3325
+ const usage_total = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
3326
+ const stop_collecting = this.events.on(collect_usage(usage_total));
3327
+ await this.call_plugin_run_start(options.input);
3328
+ let outcome;
3329
+ try {
3330
+ const seed_messages = [...options.history ?? []];
3331
+ seed_messages.push({ role: "user", content: options.input });
3332
+ outcome = await run_conversation(this.loop_deps(), seed_messages, {
3333
+ system_prompt: this.config.system_prompt ?? DEFAULT_AGENT_SYSTEM_PROMPT,
3334
+ max_turns: this.config.max_turns,
3335
+ temperature: this.config.temperature,
3336
+ max_tokens: this.config.max_tokens,
3337
+ context_budget_tokens: this.config.context_budget_tokens,
3338
+ compress_threshold: this.config.compress_threshold,
3339
+ signal: options.signal
3340
+ });
3341
+ } finally {
3342
+ stop_collecting();
3343
+ if (outcome !== void 0) {
3344
+ await this.call_plugin_run_end(outcome);
3345
+ }
3346
+ }
3347
+ const session_path = await this.persist_session(outcome, options);
3348
+ const full_messages = [...options.history ?? [], ...outcome.messages];
3349
+ return { outcome, messages: full_messages, usage_total, session_path };
3350
+ }
3351
+ loop_deps() {
3352
+ return {
3353
+ chat: (messages, tools, chat_options) => this.router.chat_with_failover(messages, tools, chat_options),
3354
+ tools: this.executor,
3355
+ definitions: () => this.registry.definitions(),
3356
+ emitter: this.events
3357
+ };
3358
+ }
3359
+ /** Best-effort on_run_start fan-out; hook errors are logged, never fatal. */
3360
+ async call_plugin_run_start(input) {
3361
+ if (this.hook_runner === void 0) {
3362
+ return;
3363
+ }
3364
+ const ctx = { work_dir: this.config.work_dir };
3365
+ await this.hook_runner.call_run_start({ input_chars: input.length }, ctx);
3366
+ }
3367
+ /** Best-effort on_run_end fan-out; hook errors are logged, never fatal. */
3368
+ async call_plugin_run_end(outcome) {
3369
+ if (this.hook_runner === void 0) {
3370
+ return;
3371
+ }
3372
+ const ctx = { work_dir: this.config.work_dir };
3373
+ await this.hook_runner.call_run_end(
3374
+ { stopped_reason: outcome.stopped_reason, turns_used: outcome.turns_used },
3375
+ ctx
3376
+ );
3377
+ }
3378
+ /** Best-effort JSONL transcript: never fails the run, returns undefined path on error. */
3379
+ async persist_session(outcome, options) {
3380
+ try {
3381
+ const handle = await open_session(this.config.session_dir, options.label);
3382
+ await append_meta(handle, { event: "run_start", input_chars: options.input.length, history_size: outcome.messages.length });
3383
+ for (const message of outcome.messages) {
3384
+ await handle.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), kind: "message", message });
3385
+ }
3386
+ if (outcome.stopped_reason === "budget") {
3387
+ await append_meta(handle, { event: "budget_exhausted" });
3388
+ }
3389
+ return handle.path;
3390
+ } catch (error) {
3391
+ logger.warn("session persistence failed; continuing without transcript", error);
3392
+ return void 0;
3393
+ }
3394
+ }
3395
+ };
3396
+ function create_agent(raw_config) {
3397
+ return new Agent(parse_agent_config(raw_config));
3398
+ }
3399
+ async function create_agent_with_plugins(raw_config) {
3400
+ const config = parse_agent_config(raw_config);
3401
+ const { plugins, errors } = await load_plugins(config.plugins, config.work_dir);
3402
+ if (errors.length > 0) {
3403
+ logger.warn(`plugin load errors: ${plugin_errors_summary(errors)}`);
3404
+ }
3405
+ return new Agent(config, plugins);
3406
+ }
3407
+ async function run_agent(raw_config, input, options) {
3408
+ const agent = create_agent(raw_config);
3409
+ return agent.run({ input, signal: options?.signal, label: options?.label });
3410
+ }
3411
+
3412
+ export {
3413
+ safe_json_parse,
3414
+ truncate_text,
3415
+ logger,
3416
+ register_builtin_tools,
3417
+ ToolRegistry,
3418
+ ToolExecutor,
3419
+ HookedToolRunner,
3420
+ load_plugins,
3421
+ plugin_errors_summary,
3422
+ sleep,
3423
+ ProviderError,
3424
+ parse_agent_config,
3425
+ AgentEmitter,
3426
+ Agent,
3427
+ create_agent,
3428
+ create_agent_with_plugins,
3429
+ run_agent
3430
+ };
3431
+ //# sourceMappingURL=chunk-P52U5M3L.js.map