blun-king-cli 9.0.0 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LIESMICH.txt +1 -7
  2. package/README.md +4 -16
  3. package/bin/blun.js +248 -160
  4. package/bin/core-bootstrap.js +47 -0
  5. package/bin/king.js +277 -1
  6. package/bin/launcher-mode.js +2 -1
  7. package/bin/launcher-runtime.js +221 -0
  8. package/bin/plugin-bootstrap.js +0 -0
  9. package/bin/private-paths.js +0 -0
  10. package/bin/update-lease.js +399 -0
  11. package/bin/update-notice.js +1094 -0
  12. package/blun.mjs +4060 -6667
  13. package/package.json +3 -10
  14. package/skills/screenshot-lesen/SKILL.md +0 -1
  15. package/skills/web-lesen/SKILL.md +0 -1
  16. package/telegram-plugin/dist/bridge.mjs +1 -21
  17. package/mnemo/access_routes.js +0 -692
  18. package/mnemo/agent_governance.js +0 -4242
  19. package/mnemo/agent_mail.js +0 -901
  20. package/mnemo/bootstrap_auto.js +0 -137
  21. package/mnemo/brief_coordination.js +0 -226
  22. package/mnemo/code_read_tools.js +0 -375
  23. package/mnemo/context_preview_tools.js +0 -603
  24. package/mnemo/embeddings.js +0 -66
  25. package/mnemo/external_repo_ops.js +0 -575
  26. package/mnemo/facts/example-project-rules.json +0 -90
  27. package/mnemo/facts/example.json +0 -34
  28. package/mnemo/identity_schema.sql +0 -139
  29. package/mnemo/journal_schema.js +0 -561
  30. package/mnemo/loop_doctor_tools.js +0 -661
  31. package/mnemo/mail_secret_refs.js +0 -150
  32. package/mnemo/mcp.js +0 -9309
  33. package/mnemo/memory_consolidation.js +0 -1914
  34. package/mnemo/memory_health_tools.js +0 -165
  35. package/mnemo/package.json +0 -79
  36. package/mnemo/protected_scope_gate.js +0 -627
  37. package/mnemo/resource_access_control.js +0 -684
  38. package/mnemo/runtime_governance.js +0 -1256
  39. package/mnemo/runtime_turn_gate.js +0 -862
  40. package/mnemo/sandbox.js +0 -143
  41. package/mnemo/schema.sql +0 -389
  42. package/mnemo/shared_utils.js +0 -763
  43. package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
  44. package/mnemo/skills/agent_hand/SKILL.md +0 -43
  45. package/mnemo/skills/agent_hand/run.js +0 -63
  46. package/mnemo/skills/book_flight/SKILL.md +0 -34
  47. package/mnemo/skills/external_repo_review/SKILL.md +0 -43
  48. package/mnemo/skills/external_repo_review/run.js +0 -73
  49. package/mnemo/skills/pay_invoice/SKILL.md +0 -34
  50. package/mnemo/team_quality_ops.js +0 -944
  51. package/mnemo/timeline_report_tools.js +0 -810
  52. package/mnemo/write_gate_risk.js +0 -80
  53. package/mnemo/writer_health.js +0 -152
  54. package/skills/doku-ingestion/SKILL.md +0 -48
  55. package/skills/doku-ingestion/ingest_docs.py +0 -133
@@ -1,375 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const fs = require("fs");
5
- const os = require("os");
6
- const path = require("path");
7
-
8
- const MAX_FILE_BYTES = intEnv("MNEMO_CODE_MAX_FILE_BYTES", 2 * 1024 * 1024);
9
- const MAX_SYMBOLS = intEnv("MNEMO_CODE_MAX_SYMBOLS", 500);
10
- const MAX_UNFOLD_LINES = intEnv("MNEMO_CODE_MAX_UNFOLD_LINES", 500);
11
- const DEFAULT_UNFOLD_LINES = intEnv("MNEMO_CODE_DEFAULT_UNFOLD_LINES", 160);
12
-
13
- const CODE_READ_TOOL_DEFS = {
14
- mem_code_outline: {
15
- description: "Token-efficient code read step 1. Return imports/headings/symbols with line ranges for a file, without dumping the whole file. Call this before reading large code files.",
16
- inputSchema: {
17
- type: "object",
18
- properties: {
19
- file_path: { type: "string", description: "Absolute path, or path relative to workspace/root/process cwd." },
20
- workspace: { type: "string", description: "Optional base directory for relative file_path." },
21
- query: { type: "string", description: "Optional filter for symbol/import text." },
22
- include_imports: { type: "boolean", description: "Default true." },
23
- max_symbols: { type: "integer", description: "Default 200, hard max MNEMO_CODE_MAX_SYMBOLS." },
24
- max_imports: { type: "integer", description: "Default 80." }
25
- },
26
- required: ["file_path"]
27
- }
28
- },
29
- mem_code_unfold: {
30
- description: "Token-efficient code read step 2. Return only one symbol or bounded line range from a file, with optional context lines and line numbers.",
31
- inputSchema: {
32
- type: "object",
33
- properties: {
34
- file_path: { type: "string", description: "Absolute path, or path relative to workspace/root/process cwd." },
35
- workspace: { type: "string", description: "Optional base directory for relative file_path." },
36
- symbol: { type: "string", description: "Function/class/section name from mem_code_outline." },
37
- start_line: { type: "integer" },
38
- end_line: { type: "integer" },
39
- context_lines: { type: "integer", description: "Default 3." },
40
- max_lines: { type: "integer", description: "Default 160, hard max MNEMO_CODE_MAX_UNFOLD_LINES." },
41
- include_line_numbers: { type: "boolean", description: "Default true." }
42
- },
43
- required: ["file_path"]
44
- }
45
- }
46
- };
47
-
48
- function intEnv(name, fallback) {
49
- const n = parseInt(process.env[name] || "", 10);
50
- return Number.isFinite(n) && n > 0 ? n : fallback;
51
- }
52
-
53
- function handleCodeReadTool(name, args) {
54
- if (name === "mem_code_outline") return codeOutline(args || {});
55
- if (name === "mem_code_unfold") return codeUnfold(args || {});
56
- return { error: "unknown code read tool: " + name };
57
- }
58
-
59
- function codeOutline(args) {
60
- const loaded = loadTextFile(args);
61
- if (loaded.error) return loaded;
62
- const symbols = collectSymbols(loaded.lines, loaded.language);
63
- const query = String(args.query || "").trim().toLowerCase();
64
- const filteredSymbols = query
65
- ? symbols.filter(s => [s.kind, s.name, s.signature].join(" ").toLowerCase().includes(query))
66
- : symbols;
67
- const maxSymbols = Math.min(args.max_symbols || 200, MAX_SYMBOLS);
68
- const imports = args.include_imports === false ? [] : collectImports(loaded.lines, loaded.language, args.max_imports || 80, query);
69
- const shownSymbols = filteredSymbols.slice(0, maxSymbols);
70
- return {
71
- file_path: loaded.file_path,
72
- relative_path: loaded.relative_path,
73
- language: loaded.language,
74
- byte_count: loaded.byte_count,
75
- line_count: loaded.lines.length,
76
- truncated_bytes: loaded.truncated_bytes,
77
- rough_token_estimate_full_file: roughTokens(loaded.text),
78
- imports: { count: imports.length, items: imports },
79
- symbols: { count: filteredSymbols.length, shown: shownSymbols.length, truncated: filteredSymbols.length > shownSymbols.length, items: shownSymbols },
80
- next_step: "Use mem_code_unfold with {file_path, symbol} or {file_path, start_line, end_line}; do not read the whole file unless the outline proves it is necessary."
81
- };
82
- }
83
-
84
- function codeUnfold(args) {
85
- const loaded = loadTextFile(args);
86
- if (loaded.error) return loaded;
87
- const context = clampInt(args.context_lines, 3, 0, 50);
88
- const maxLines = clampInt(args.max_lines, DEFAULT_UNFOLD_LINES, 1, MAX_UNFOLD_LINES);
89
- let start = Number(args.start_line || 0);
90
- let end = Number(args.end_line || 0);
91
- let matchedSymbol = null;
92
-
93
- if (args.symbol) {
94
- const symbols = collectSymbols(loaded.lines, loaded.language);
95
- const wanted = String(args.symbol || "").trim().toLowerCase();
96
- const exact = symbols.filter(s => s.name.toLowerCase() === wanted || s.signature.toLowerCase().includes(wanted));
97
- const partial = exact.length ? exact : symbols.filter(s => [s.kind, s.name, s.signature].join(" ").toLowerCase().includes(wanted));
98
- if (!partial.length) {
99
- return { error: "symbol_not_found", file_path: loaded.file_path, symbol: args.symbol, hint: "Call mem_code_outline and use one of the returned symbol names.", candidates: symbols.slice(0, 30).map(s => ({ kind: s.kind, name: s.name, line: s.line })) };
100
- }
101
- if (partial.length > 1 && !exact.length) {
102
- return { error: "symbol_ambiguous", file_path: loaded.file_path, symbol: args.symbol, matches: partial.slice(0, 20).map(s => ({ kind: s.kind, name: s.name, line: s.line, signature: s.signature })) };
103
- }
104
- matchedSymbol = partial[0];
105
- start = matchedSymbol.line;
106
- end = matchedSymbol.end_line || matchedSymbol.line;
107
- }
108
-
109
- if (!start && !end) return { error: "symbol or start_line/end_line required", file_path: loaded.file_path };
110
- if (!start) start = end;
111
- if (!end) end = start;
112
- if (start > end) [start, end] = [end, start];
113
- start = clampInt(start - context, 1, 1, loaded.lines.length);
114
- end = clampInt(end + context, start, start, loaded.lines.length);
115
-
116
- let truncated = false;
117
- if (end - start + 1 > maxLines) {
118
- end = start + maxLines - 1;
119
- truncated = true;
120
- }
121
- const selected = loaded.lines.slice(start - 1, end);
122
- const includeNumbers = args.include_line_numbers !== false;
123
- const width = String(end).length;
124
- const content = selected.map((line, idx) => includeNumbers ? `${String(start + idx).padStart(width, " ")} | ${line}` : line).join("\n");
125
- return {
126
- file_path: loaded.file_path,
127
- relative_path: loaded.relative_path,
128
- language: loaded.language,
129
- symbol: matchedSymbol,
130
- start_line: start,
131
- end_line: end,
132
- lines_returned: selected.length,
133
- truncated,
134
- rough_token_estimate: roughTokens(content),
135
- content,
136
- next_step: truncated ? { start_line: end + 1, end_line: Math.min(loaded.lines.length, end + maxLines) } : null
137
- };
138
- }
139
-
140
- function loadTextFile(args) {
141
- const resolved = resolveCodePath(args);
142
- if (resolved.error) return resolved;
143
- const stat = fs.statSync(resolved.file_path);
144
- if (!stat.isFile()) return { error: "not_a_file", file_path: resolved.file_path };
145
- if (stat.size > MAX_FILE_BYTES) {
146
- return { error: "file_too_large", file_path: resolved.file_path, byte_count: stat.size, max_bytes: MAX_FILE_BYTES, hint: "Raise MNEMO_CODE_MAX_FILE_BYTES only if this is intentional." };
147
- }
148
- const buf = fs.readFileSync(resolved.file_path);
149
- if (looksBinary(buf)) return { error: "binary_file", file_path: resolved.file_path, byte_count: stat.size };
150
- const text = buf.toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
151
- return {
152
- file_path: resolved.file_path,
153
- relative_path: resolved.relative_path,
154
- language: inferLanguage(resolved.file_path),
155
- byte_count: stat.size,
156
- truncated_bytes: 0,
157
- text,
158
- lines: text.split("\n")
159
- };
160
- }
161
-
162
- function resolveCodePath(args) {
163
- const raw = String(args.file_path || args.path || "").trim();
164
- if (!raw) return { error: "file_path required" };
165
- const expanded = raw.replace(/^~(?=$|[\\/])/, os.homedir());
166
- const base = args.workspace || args.root || process.env.AGENT_WORKSPACE || process.env.MNEMO_WORKSPACE || process.cwd();
167
- const candidate = path.resolve(path.isAbsolute(expanded) ? expanded : path.join(base, expanded));
168
- let real = candidate;
169
- try { real = fs.realpathSync(candidate); } catch {
170
- return { error: "file_not_found", file_path: candidate };
171
- }
172
- const roots = allowedRoots();
173
- const matched = roots.find(root => isInside(real, root));
174
- if (!matched) {
175
- return { error: "path_not_allowed", file_path: real, allowed_roots: roots, hint: "Set MNEMO_CODE_ROOTS (path-delimited) or run the daemon from the project workspace." };
176
- }
177
- return { file_path: real, relative_path: path.relative(matched, real) || path.basename(real) };
178
- }
179
-
180
- function allowedRoots() {
181
- const roots = [];
182
- function add(root) {
183
- if (!root) return;
184
- for (const part of String(root).split(path.delimiter)) {
185
- const trimmed = part.trim();
186
- if (!trimmed) continue;
187
- try {
188
- const real = fs.realpathSync(path.resolve(trimmed.replace(/^~(?=$|[\\/])/, os.homedir())));
189
- if (!roots.some(r => samePath(r, real))) roots.push(real);
190
- } catch {}
191
- }
192
- }
193
- add(process.env.MNEMO_CODE_ROOTS);
194
- add(process.env.AGENT_WORKSPACE);
195
- add(process.env.MNEMO_WORKSPACE);
196
- add(process.cwd());
197
- add(path.resolve(__dirname, "..", ".."));
198
- add(os.homedir());
199
- return roots;
200
- }
201
-
202
- function samePath(a, b) {
203
- return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
204
- }
205
-
206
- function isInside(file, root) {
207
- const rel = path.relative(root, file);
208
- return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
209
- }
210
-
211
- function looksBinary(buf) {
212
- const len = Math.min(buf.length, 8192);
213
- for (let i = 0; i < len; i++) if (buf[i] === 0) return true;
214
- return false;
215
- }
216
-
217
- function inferLanguage(filePath) {
218
- const ext = path.extname(filePath).toLowerCase();
219
- const base = path.basename(filePath).toLowerCase();
220
- if ([".js", ".jsx", ".mjs", ".cjs"].includes(ext)) return "javascript";
221
- if ([".ts", ".tsx", ".mts", ".cts"].includes(ext)) return "typescript";
222
- if (ext === ".py") return "python";
223
- if (ext === ".go") return "go";
224
- if ([".rs"].includes(ext)) return "rust";
225
- if ([".java", ".kt", ".kts", ".scala"].includes(ext)) return "jvm";
226
- if ([".cs"].includes(ext)) return "csharp";
227
- if ([".c", ".h", ".cc", ".cpp", ".hpp", ".hh"].includes(ext)) return "cpp";
228
- if ([".php"].includes(ext)) return "php";
229
- if ([".rb"].includes(ext)) return "ruby";
230
- if ([".css", ".scss", ".sass", ".less"].includes(ext)) return "css";
231
- if ([".html", ".htm", ".svelte", ".vue"].includes(ext)) return "markup";
232
- if ([".md", ".mdx"].includes(ext) || base === "readme") return "markdown";
233
- if ([".json", ".jsonc"].includes(ext)) return "json";
234
- if ([".yml", ".yaml"].includes(ext)) return "yaml";
235
- if ([".sql"].includes(ext)) return "sql";
236
- if ([".sh", ".bash", ".zsh", ".ps1"].includes(ext)) return "shell";
237
- return "text";
238
- }
239
-
240
- function collectImports(lines, language, max, query) {
241
- const out = [];
242
- const patterns = importPatterns(language);
243
- if (!patterns.length) return out;
244
- for (let i = 0; i < lines.length && out.length < max; i++) {
245
- const trimmed = lines[i].trim();
246
- if (!trimmed) continue;
247
- if (patterns.some(re => re.test(trimmed))) {
248
- const item = { line: i + 1, text: trimmed.slice(0, 220) };
249
- if (!query || item.text.toLowerCase().includes(query)) out.push(item);
250
- }
251
- }
252
- return out;
253
- }
254
-
255
- function importPatterns(language) {
256
- if (["javascript", "typescript"].includes(language)) return [/^import\s+/, /^export\s+.*\s+from\s+/, /^const\s+.+\s*=\s*require\(/];
257
- if (language === "python") return [/^import\s+/, /^from\s+.+\s+import\s+/];
258
- if (language === "go") return [/^import\s+/, /^import\s*\(/];
259
- if (language === "rust") return [/^use\s+/, /^mod\s+/];
260
- if (language === "php") return [/^use\s+/, /^require(_once)?\s+/, /^include(_once)?\s+/];
261
- if (language === "jvm" || language === "csharp") return [/^import\s+/, /^using\s+/];
262
- if (language === "css") return [/^@import\s+/];
263
- return [];
264
- }
265
-
266
- function collectSymbols(lines, language) {
267
- const raw = [];
268
- for (let i = 0; i < lines.length; i++) {
269
- const hit = detectSymbol(lines[i], language);
270
- if (!hit) continue;
271
- raw.push(Object.assign(hit, { line: i + 1, signature: lines[i].trim().slice(0, 240) }));
272
- }
273
- for (let i = 0; i < raw.length; i++) {
274
- const startIdx = raw[i].line - 1;
275
- const nextLine = raw[i + 1] ? raw[i + 1].line - 1 : lines.length;
276
- raw[i].end_line = estimateEndLine(lines, startIdx, language, nextLine);
277
- }
278
- return raw;
279
- }
280
-
281
- function detectSymbol(line, language) {
282
- const s = line.trim();
283
- let m;
284
- if (!s || s.startsWith("//") || s.startsWith("*")) return null;
285
- if (["javascript", "typescript"].includes(language)) {
286
- if ((m = s.match(/^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/))) return { kind: "function", name: m[1] };
287
- if ((m = s.match(/^(?:export\s+)?(?:class|interface|type|enum)\s+([A-Za-z_$][\w$]*)\b/))) return { kind: s.includes(" class ") || s.startsWith("class ") ? "class" : "type", name: m[1] };
288
- if ((m = s.match(/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/))) return { kind: "function", name: m[1] };
289
- if ((m = s.match(/^(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/)) && !/^(if|for|while|switch|catch)\b/.test(s)) return { kind: "method", name: m[1] };
290
- } else if (language === "python") {
291
- if ((m = line.match(/^(\s*)class\s+([A-Za-z_][\w]*)\b/))) return { kind: "class", name: m[2], indent: m[1].length };
292
- if ((m = line.match(/^(\s*)(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/))) return { kind: "function", name: m[2], indent: m[1].length };
293
- } else if (language === "go") {
294
- if ((m = s.match(/^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/))) return { kind: "function", name: m[1] };
295
- if ((m = s.match(/^type\s+([A-Za-z_]\w*)\s+(?:struct|interface|func|\w+)/))) return { kind: "type", name: m[1] };
296
- } else if (language === "rust") {
297
- if ((m = s.match(/^(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_]\w*)\s*\(/))) return { kind: "function", name: m[1] };
298
- if ((m = s.match(/^(?:pub\s+)?(?:struct|enum|trait|impl)\s+([A-Za-z_]\w*)?/))) return { kind: s.split(/\s+/).includes("impl") ? "impl" : "type", name: m[1] || s.slice(0, 80) };
299
- } else if (["jvm", "csharp", "cpp", "php", "ruby"].includes(language)) {
300
- if ((m = s.match(/^(?:public|private|protected|internal|static|final|abstract|sealed|export|\s)*\s*(?:class|interface|enum|record|trait)\s+([A-Za-z_]\w*)/))) return { kind: "type", name: m[1] };
301
- if ((m = s.match(/^(?:public|private|protected|internal|static|final|abstract|virtual|override|async|\s)*\s*(?:function\s+)?(?:[\w:<>,\[\]?*&]+\s+)?([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:\{|=>|$)/)) && !/^(if|for|while|switch|catch)\b/.test(s)) return { kind: "function", name: m[1] };
302
- if (language === "ruby" && (m = s.match(/^def\s+([A-Za-z_]\w*[!?=]?)/))) return { kind: "function", name: m[1] };
303
- } else if (language === "markdown") {
304
- if ((m = line.match(/^(#{1,6})\s+(.+)/))) return { kind: "heading", name: m[2].trim().slice(0, 120), level: m[1].length };
305
- } else if (language === "css") {
306
- if (s.endsWith("{") && !s.startsWith("@media") && !s.startsWith("@keyframes")) return { kind: "selector", name: s.slice(0, -1).trim().slice(0, 120) };
307
- if ((m = s.match(/^@(media|keyframes|supports)\s+(.+)\s*\{/))) return { kind: "at-rule", name: `${m[1]} ${m[2]}`.slice(0, 120) };
308
- } else if (language === "json") {
309
- if ((m = line.match(/^\s{0,4}"([^"]+)"\s*:/))) return { kind: "key", name: m[1] };
310
- } else if (language === "yaml") {
311
- if ((m = line.match(/^([A-Za-z0-9_.-][^:#]*):\s*($|[^/])/))) return { kind: "key", name: m[1].trim() };
312
- } else if (language === "sql") {
313
- if ((m = s.match(/^CREATE\s+(?:OR\s+REPLACE\s+)?(TABLE|VIEW|FUNCTION|PROCEDURE|INDEX|TRIGGER)\s+([^\s(]+)/i))) return { kind: m[1].toLowerCase(), name: m[2] };
314
- } else if (language === "shell") {
315
- if ((m = s.match(/^([A-Za-z_][\w.-]*)\s*\(\)\s*\{/))) return { kind: "function", name: m[1] };
316
- if ((m = s.match(/^function\s+([A-Za-z_][\w.-]*)/))) return { kind: "function", name: m[1] };
317
- }
318
- return null;
319
- }
320
-
321
- function estimateEndLine(lines, startIdx, language, nextLine) {
322
- if (language === "python") return indentationEnd(lines, startIdx, nextLine);
323
- if (language === "markdown") return markdownEnd(lines, startIdx, nextLine);
324
- const braceEnd = braceEndLine(lines, startIdx);
325
- if (braceEnd) return Math.min(braceEnd, nextLine);
326
- return Math.max(startIdx + 1, nextLine);
327
- }
328
-
329
- function indentationEnd(lines, startIdx, nextLine) {
330
- const baseIndent = (lines[startIdx].match(/^\s*/) || [""])[0].length;
331
- for (let i = startIdx + 1; i < lines.length; i++) {
332
- const line = lines[i];
333
- if (!line.trim() || line.trim().startsWith("#")) continue;
334
- const indent = (line.match(/^\s*/) || [""])[0].length;
335
- if (indent <= baseIndent) return Math.max(startIdx + 1, i);
336
- }
337
- return nextLine;
338
- }
339
-
340
- function markdownEnd(lines, startIdx, nextLine) {
341
- const m = lines[startIdx].match(/^(#{1,6})\s+/);
342
- if (!m) return nextLine;
343
- const level = m[1].length;
344
- for (let i = startIdx + 1; i < lines.length; i++) {
345
- const h = lines[i].match(/^(#{1,6})\s+/);
346
- if (h && h[1].length <= level) return i;
347
- }
348
- return lines.length;
349
- }
350
-
351
- function braceEndLine(lines, startIdx) {
352
- let depth = 0;
353
- let seen = false;
354
- for (let i = startIdx; i < lines.length && i < startIdx + 2000; i++) {
355
- const code = lines[i].replace(/(["'`])(?:\\.|(?!\1).)*\1/g, "");
356
- for (const ch of code) {
357
- if (ch === "{") { depth++; seen = true; }
358
- else if (ch === "}") depth--;
359
- }
360
- if (seen && depth <= 0 && i > startIdx) return i + 1;
361
- }
362
- return null;
363
- }
364
-
365
- function clampInt(value, fallback, min, max) {
366
- const n = parseInt(value, 10);
367
- if (!Number.isFinite(n)) return fallback;
368
- return Math.max(min, Math.min(max, n));
369
- }
370
-
371
- function roughTokens(text) {
372
- return Math.ceil(String(text || "").length / 4);
373
- }
374
-
375
- module.exports = { CODE_READ_TOOL_DEFS, handleCodeReadTool, codeOutline, codeUnfold };