@nexrall/code-core 1.4.11 → 1.4.12
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.
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +91 -46
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AA6iEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -52,8 +52,24 @@ const tsLangService_1 = require("./tsLangService");
|
|
|
52
52
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
53
53
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
54
54
|
const MAX_FETCH_BYTES = 200 * 1024; // 200 KB
|
|
55
|
-
const MAX_READ_BYTES = 500 * 1024; // 500 KB
|
|
55
|
+
const MAX_READ_BYTES = 500 * 1024; // 500 KB — still used by notebook_read (JSON.parse needs it whole)
|
|
56
56
|
const MAX_OUTPUT_CHARS = 100000; // bash / grep output cap (~100 KB)
|
|
57
|
+
// read_file has NO byte-size gate (unlike the old 500KB cutoff) — like Claude
|
|
58
|
+
// Code, a file of any size can be read; it's always streamed line-by-line so
|
|
59
|
+
// memory is bounded regardless of file size. Instead there are two independent
|
|
60
|
+
// caps per call, mirroring Claude Code's Read tool:
|
|
61
|
+
// - MAX_READ_LINES: a hard line-count ceiling (default window AND clamp on
|
|
62
|
+
// an explicit limit) so one call never floods context with thousands of
|
|
63
|
+
// short lines (JSON/minified/logs) just because the file is small in bytes.
|
|
64
|
+
// - MAX_READ_TOKENS: a hard token budget (rough chars/4 estimate). If the
|
|
65
|
+
// requested window would exceed it, we either return a PARTIAL page (no
|
|
66
|
+
// explicit limit — same UX as Claude Code's "first page + how to continue"
|
|
67
|
+
// notice) or an error (explicit limit given and it still doesn't fit —
|
|
68
|
+
// caller must shrink the limit or use search_files instead).
|
|
69
|
+
const MAX_READ_LINES = 2000;
|
|
70
|
+
const MAX_READ_TOKENS = 25000;
|
|
71
|
+
const READ_CHARS_PER_TOKEN = 4; // rough, conservative — consistent with loop.ts's resume estimator
|
|
72
|
+
const MAX_READ_CHARS = MAX_READ_TOKENS * READ_CHARS_PER_TOKEN; // 100,000 chars
|
|
57
73
|
// GAP B — output spillover. The inline bash result keeps only HEAD+TAIL (~100 KB),
|
|
58
74
|
// which loses the MIDDLE of a large log — often exactly where a stack trace's root
|
|
59
75
|
// cause or a failing assertion lives. To make the full log recoverable WITHOUT
|
|
@@ -217,51 +233,99 @@ function isBinaryFile(filePath) {
|
|
|
217
233
|
return false;
|
|
218
234
|
}
|
|
219
235
|
}
|
|
236
|
+
function formatNumberedLine(offset, indexInKept, line) {
|
|
237
|
+
return `${String(offset + indexInKept + 1).padStart(4, ' ')}\t${line}`;
|
|
238
|
+
}
|
|
220
239
|
/**
|
|
221
|
-
* Read a bounded
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
240
|
+
* Read a bounded window [offset, offset+limit) of a file by streaming it in
|
|
241
|
+
* chunks — memory is bounded by what's actually kept, never the whole file, so
|
|
242
|
+
* this is safe for files of ANY size (there is no byte-size gate on read_file
|
|
243
|
+
* at all, matching Claude Code). Two independent hard caps apply per call:
|
|
244
|
+
* - MAX_READ_LINES lines
|
|
245
|
+
* - MAX_READ_TOKENS tokens (~MAX_READ_CHARS chars) of formatted output
|
|
246
|
+
* If an explicit `requestedLimit` was given and the token cap is hit before
|
|
247
|
+
* that many lines were produced, the call fails with an actionable error
|
|
248
|
+
* (mirrors Claude Code: "a read that passes an explicit offset or limit and
|
|
249
|
+
* still exceeds the token limit returns an error"). Otherwise (default,
|
|
250
|
+
* no explicit limit) it silently returns a partial page with a continuation
|
|
251
|
+
* note — never an error just for being a big file.
|
|
225
252
|
*/
|
|
226
|
-
async function
|
|
253
|
+
async function readFileWindowed(resolved, offset, requestedLimit) {
|
|
227
254
|
return new Promise((resolve) => {
|
|
228
|
-
const
|
|
255
|
+
const explicitLimit = requestedLimit > 0;
|
|
256
|
+
const lineScanCap = explicitLimit ? Math.min(requestedLimit, MAX_READ_LINES) : MAX_READ_LINES;
|
|
257
|
+
const hardEndLine = offset + lineScanCap; // exclusive, 0-based
|
|
229
258
|
const kept = [];
|
|
259
|
+
let keptChars = 0;
|
|
230
260
|
let lineNo = 0; // 0-based index of the NEXT line to be completed
|
|
231
261
|
let carry = ''; // partial line spanning chunk boundaries
|
|
232
262
|
let stopped = false;
|
|
263
|
+
let hitTokenCap = false;
|
|
264
|
+
let sawEof = false;
|
|
233
265
|
const stream = fs.createReadStream(resolved, { encoding: 'utf-8', highWaterMark: 256 * 1024 });
|
|
266
|
+
// Returns false if adding this line would exceed the token budget (line NOT kept).
|
|
267
|
+
const tryPushLine = (line) => {
|
|
268
|
+
const formatted = formatNumberedLine(offset, kept.length, line);
|
|
269
|
+
const added = formatted.length + 1; // +1 for the join newline
|
|
270
|
+
if (keptChars + added > MAX_READ_CHARS) {
|
|
271
|
+
hitTokenCap = true;
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
kept.push(line);
|
|
275
|
+
keptChars += added;
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
234
278
|
const finish = () => {
|
|
235
279
|
if (stopped)
|
|
236
280
|
return;
|
|
237
281
|
stopped = true;
|
|
238
282
|
stream.destroy();
|
|
283
|
+
if (hitTokenCap && explicitLimit) {
|
|
284
|
+
resolve({
|
|
285
|
+
error: `Requested range (offset:${offset}, limit:${requestedLimit}) is too large — exceeds ${MAX_READ_TOKENS} tokens. ` +
|
|
286
|
+
`Pass a smaller limit, or use search_files to locate the relevant part first.`,
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
239
290
|
const first = offset + 1;
|
|
240
291
|
const last = offset + kept.length;
|
|
241
|
-
const numbered = kept.map((l, i) =>
|
|
242
|
-
const
|
|
292
|
+
const numbered = kept.map((l, i) => formatNumberedLine(offset, i, l)).join('\n');
|
|
293
|
+
const totalNote = sawEof ? `/${lineNo}` : '';
|
|
294
|
+
const note = sawEof
|
|
243
295
|
? ` (reached end of file at line ${last})`
|
|
244
|
-
: ` (more lines follow —
|
|
245
|
-
resolve({ output: `[File: ${resolved} —
|
|
296
|
+
: ` (more lines follow — pass offset:${last} to continue, or search_files to locate the relevant part)`;
|
|
297
|
+
resolve({ output: `[File: ${resolved} — lines ${first}-${last}${totalNote}${note}]\n${numbered}` });
|
|
246
298
|
};
|
|
247
299
|
stream.on('data', (chunk) => {
|
|
248
300
|
const text = carry + (typeof chunk === 'string' ? chunk : chunk.toString('utf-8'));
|
|
249
301
|
const lines = text.split('\n');
|
|
250
302
|
carry = lines.pop() ?? ''; // last element is an incomplete line (or '')
|
|
251
303
|
for (const line of lines) {
|
|
252
|
-
if (lineNo >= offset
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
if (lineNo >= offset) {
|
|
305
|
+
if (lineNo >= hardEndLine) {
|
|
306
|
+
finish();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!tryPushLine(line)) {
|
|
310
|
+
finish();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
258
313
|
}
|
|
314
|
+
lineNo++;
|
|
259
315
|
}
|
|
260
316
|
});
|
|
261
317
|
stream.on('end', () => {
|
|
318
|
+
if (stopped)
|
|
319
|
+
return;
|
|
262
320
|
// Flush the final carry (file not ending in newline) if it's in range.
|
|
263
|
-
if (
|
|
264
|
-
|
|
321
|
+
if (carry !== '' && lineNo >= offset && lineNo < hardEndLine) {
|
|
322
|
+
if (!tryPushLine(carry)) {
|
|
323
|
+
finish();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
lineNo++;
|
|
327
|
+
}
|
|
328
|
+
sawEof = true;
|
|
265
329
|
finish();
|
|
266
330
|
});
|
|
267
331
|
stream.on('error', (err) => { if (!stopped) {
|
|
@@ -278,38 +342,19 @@ async function readFile(input, workDir) {
|
|
|
278
342
|
return { error: 'Missing required parameter: path' };
|
|
279
343
|
try {
|
|
280
344
|
const resolved = resolvePath(filePath, workDir);
|
|
281
|
-
|
|
345
|
+
fs.statSync(resolved); // throws ENOENT/EISDIR etc with a clear message if the path is bad
|
|
282
346
|
// Reject binary files early — reading them as UTF-8 produces garbage.
|
|
347
|
+
// (isBinaryFile only samples the first 8KB, so this is cheap regardless of file size.)
|
|
283
348
|
if (isBinaryFile(resolved)) {
|
|
284
349
|
const ext = path.extname(resolved).toLowerCase();
|
|
285
350
|
return { error: `Cannot read binary file: ${resolved} (${ext || 'no extension'}). Use a text-based tool or convert it first.` };
|
|
286
351
|
}
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (limit <= 0) {
|
|
294
|
-
return { error: `File too large (${kb} KB, max ${MAX_READ_BYTES / 1024} KB). Pass offset + limit to read a section (e.g. {offset:0, limit:500}), or search_files to locate the relevant part first.` };
|
|
295
|
-
}
|
|
296
|
-
return await readLargeFileWindow(resolved, offset, limit, kb);
|
|
297
|
-
}
|
|
298
|
-
const content = fs.readFileSync(resolved, 'utf-8');
|
|
299
|
-
const allLines = content.split('\n');
|
|
300
|
-
const totalLines = allLines.length;
|
|
301
|
-
const startLine = offset; // 0-based
|
|
302
|
-
const endLine = limit > 0 ? Math.min(startLine + limit, totalLines) : totalLines;
|
|
303
|
-
const slice = allLines.slice(startLine, endLine);
|
|
304
|
-
// Prefix each line with its 1-based line number (like cat -n / Claude Code view)
|
|
305
|
-
const numbered = slice.map((l, i) => {
|
|
306
|
-
const lineNo = String(startLine + i + 1).padStart(4, ' ');
|
|
307
|
-
return `${lineNo}\t${l}`;
|
|
308
|
-
}).join('\n');
|
|
309
|
-
const rangeNote = (offset > 0 || limit > 0)
|
|
310
|
-
? ` lines ${startLine + 1}-${endLine}/${totalLines}`
|
|
311
|
-
: ` ${totalLines} lines`;
|
|
312
|
-
return { output: `[File: ${resolved} (${rangeNote})]\n${numbered}` };
|
|
352
|
+
// No byte-size gate — a file of any size can be read; it's always streamed
|
|
353
|
+
// line-by-line so memory is bounded regardless of file size (matches Claude
|
|
354
|
+
// Code, which has no separate "too large" error either). Instead, one call
|
|
355
|
+
// is capped at MAX_READ_LINES lines and MAX_READ_TOKENS tokens (see
|
|
356
|
+
// readFileWindowed for exact semantics of each cap).
|
|
357
|
+
return await readFileWindowed(resolved, offset, limit);
|
|
313
358
|
}
|
|
314
359
|
catch (err) {
|
|
315
360
|
return { error: err.message };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.12",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|