u-foo 2.5.5 → 2.5.6
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/package.json +1 -1
- package/src/agents/launch/ptyRunner.js +2 -2
- package/src/agents/launch/ptyWrapper.js +2 -2
- package/src/agents/prompts/native/index.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/bash.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/read.js +3 -2
- package/src/code/agent.js +34 -20
- package/src/code/launcher/ucode.js +5 -3
- package/src/code/launcher/ucodeDoctor.js +6 -3
- package/src/code/nativeRunner.js +31 -4
- package/src/code/taskDecomposer.js +11 -2
- package/src/code/tools/bash.js +19 -2
- package/src/code/tools/read.js +20 -2
package/package.json
CHANGED
|
@@ -61,8 +61,8 @@ function stripAnsi(text) {
|
|
|
61
61
|
function rewriteTitleOscPrefix(text, prefix) {
|
|
62
62
|
if (!prefix || !text) return text;
|
|
63
63
|
return String(text).replace(/\x1b\]([012]);([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, code, title, terminator) => {
|
|
64
|
-
if (!title || title.startsWith(`${prefix}
|
|
65
|
-
return `\x1b]${code};${prefix}
|
|
64
|
+
if (!title || title.startsWith(`${prefix}: `)) return match;
|
|
65
|
+
return `\x1b]${code};${prefix}: ${title}${terminator}`;
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -211,8 +211,8 @@ class PtyWrapper {
|
|
|
211
211
|
if (!this.titlePrefix || !text) return text;
|
|
212
212
|
const prefix = this.titlePrefix;
|
|
213
213
|
return text.replace(/\x1b\]([012]);([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, code, title, terminator) => {
|
|
214
|
-
if (!title || title.startsWith(`${prefix}
|
|
215
|
-
return `\x1b]${code};${prefix}
|
|
214
|
+
if (!title || title.startsWith(`${prefix}: `)) return match;
|
|
215
|
+
return `\x1b]${code};${prefix}: ${title}${terminator}`;
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -70,7 +70,7 @@ function getSystemPrompt({
|
|
|
70
70
|
// --- Dynamic sections (may change per session/turn) ---
|
|
71
71
|
const dynamicSectionDefs = [
|
|
72
72
|
systemPromptSection("ufoo", () => getUfooIntegrationSection()),
|
|
73
|
-
|
|
73
|
+
uncachedSection("environment", () =>
|
|
74
74
|
getEnvironmentSection({ workspaceRoot, model, provider }),
|
|
75
75
|
),
|
|
76
76
|
uncachedSection("skills", () => {
|
|
@@ -6,7 +6,7 @@ function getBashToolDescription() {
|
|
|
6
6
|
return `Run a single shell command in the workspace directory.
|
|
7
7
|
|
|
8
8
|
Usage notes:
|
|
9
|
-
- Default timeout is 60 seconds. Use timeoutMs to adjust for longer operations.
|
|
9
|
+
- Default timeout is 60 seconds. Use timeoutMs to adjust for longer operations (maximum 600 seconds; larger values are clamped).
|
|
10
10
|
- Do NOT use bash for file operations when a dedicated tool exists:
|
|
11
11
|
- Use read instead of cat/head/tail.
|
|
12
12
|
- Use write instead of echo/cat heredoc.
|
|
@@ -7,11 +7,12 @@ function getReadToolDescription() {
|
|
|
7
7
|
|
|
8
8
|
Usage notes:
|
|
9
9
|
- The path parameter is relative to the workspace root.
|
|
10
|
-
- By default reads the entire file.
|
|
10
|
+
- By default reads the entire file. Files larger than ~4MB are only partially read from the start; in that case truncated is true.
|
|
11
|
+
- Use startLine and endLine to read specific line ranges.
|
|
11
12
|
- Use maxBytes to limit the amount of data returned (default ~200KB).
|
|
12
13
|
- Cannot read directories — use bash with \`ls\` for that.
|
|
13
14
|
- Always read a file before editing it to understand its current content and structure.
|
|
14
|
-
-
|
|
15
|
+
- The content field contains the raw file text without line numbers. The result also includes totalLines (lines in the portion that was read) and truncated (true when the content was cut short by maxBytes or the large-file limit).`;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
module.exports = { READ_TOOL_NAME, getReadToolDescription };
|
package/src/code/agent.js
CHANGED
|
@@ -2,7 +2,7 @@ const readline = require("readline");
|
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const { execSync } = require("child_process");
|
|
5
|
-
const { runToolCall } = require("./dispatch");
|
|
5
|
+
const { runToolCall, TOOL_NAMES } = require("./dispatch");
|
|
6
6
|
const { runNativeAgentTask } = require("./nativeRunner");
|
|
7
7
|
const {
|
|
8
8
|
runDecomposedTask,
|
|
@@ -440,6 +440,7 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
440
440
|
const timeoutMs = Number.isFinite(state.timeoutMs) ? state.timeoutMs : 600000;
|
|
441
441
|
let streamed = false;
|
|
442
442
|
let streamLastChar = "";
|
|
443
|
+
let toolEventsThisAttempt = 0;
|
|
443
444
|
const onDelta = typeof options.onDelta === "function"
|
|
444
445
|
? options.onDelta
|
|
445
446
|
: null;
|
|
@@ -495,23 +496,27 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
495
496
|
: runNativeAgentTask;
|
|
496
497
|
const onPhase = typeof options.onPhase === "function" ? options.onPhase : null;
|
|
497
498
|
const onThinkingDelta = typeof options.onThinkingDelta === "function" ? options.onThinkingDelta : null;
|
|
498
|
-
const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) =>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
499
|
+
const invokeNative = (sessionIdValue = "", timeoutOverrideMs = timeoutMs) => {
|
|
500
|
+
toolEventsThisAttempt = 0;
|
|
501
|
+
return runNativeAgentImpl({
|
|
502
|
+
workspaceRoot,
|
|
503
|
+
provider,
|
|
504
|
+
model,
|
|
505
|
+
prompt: effectiveTaskPrompt,
|
|
506
|
+
systemPrompt: systemContext,
|
|
507
|
+
messages: Array.isArray(state.nlMessages) ? state.nlMessages : [],
|
|
508
|
+
sessionId: String(sessionIdValue || ""),
|
|
509
|
+
timeoutMs: timeoutOverrideMs,
|
|
510
|
+
onStreamDelta: onStream,
|
|
511
|
+
onThinkingDelta,
|
|
512
|
+
onPhase,
|
|
513
|
+
onToolEvent: (event) => {
|
|
514
|
+
toolEventsThisAttempt += 1;
|
|
515
|
+
pushToolLog(event);
|
|
516
|
+
},
|
|
517
|
+
signal: options.signal,
|
|
518
|
+
});
|
|
519
|
+
};
|
|
515
520
|
|
|
516
521
|
try {
|
|
517
522
|
let cliRes;
|
|
@@ -551,7 +556,9 @@ async function runNaturalLanguageTask(task = "", state = {}, options = {}) {
|
|
|
551
556
|
|
|
552
557
|
if (!cliRes || cliRes.ok === false) {
|
|
553
558
|
const errMsg = String((cliRes && cliRes.error) || "");
|
|
554
|
-
|
|
559
|
+
// Only replay the whole task when this attempt ran no tool calls;
|
|
560
|
+
// retrying after executed write/edit/bash steps would replay side effects.
|
|
561
|
+
if (isCliTimeoutError(errMsg) && toolEventsThisAttempt === 0) {
|
|
555
562
|
const extendedTimeoutMs = computeExtendedTimeout(timeoutMs);
|
|
556
563
|
cliRes = await invokeNative(String(state.sessionId || ""), extendedTimeoutMs);
|
|
557
564
|
}
|
|
@@ -1317,6 +1324,13 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
1317
1324
|
};
|
|
1318
1325
|
}
|
|
1319
1326
|
const tool = String(match[2] || "").trim().toLowerCase();
|
|
1327
|
+
if (String(match[1]).toLowerCase() === "run" && !TOOL_NAMES.includes(tool)) {
|
|
1328
|
+
// Natural language like "run the tests" is not a tool invocation.
|
|
1329
|
+
return {
|
|
1330
|
+
kind: "nl",
|
|
1331
|
+
task: text,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1320
1334
|
const payload = String(match[3] || "").trim();
|
|
1321
1335
|
let args = {};
|
|
1322
1336
|
try {
|
|
@@ -1546,7 +1560,7 @@ async function runUcodeCoreAgent({
|
|
|
1546
1560
|
}
|
|
1547
1561
|
}
|
|
1548
1562
|
if (result.kind === "resume") {
|
|
1549
|
-
const resumed = resumeSessionState(state, result.sessionId, workspaceRoot);
|
|
1563
|
+
const resumed = resumeSessionState(state, result.sessionId, state.workspaceRoot || resolvedWorkspaceRoot);
|
|
1550
1564
|
if (!resumed.ok) {
|
|
1551
1565
|
stdout.write(`Error: ${resumed.error}\n`);
|
|
1552
1566
|
} else {
|
|
@@ -224,8 +224,10 @@ function readLastArgValue(args = [], flag = "") {
|
|
|
224
224
|
if (!item) continue;
|
|
225
225
|
if (item === flag) {
|
|
226
226
|
const next = String(args[i + 1] || "").trim();
|
|
227
|
-
if (next
|
|
228
|
-
|
|
227
|
+
if (next && !next.startsWith("--")) {
|
|
228
|
+
value = next;
|
|
229
|
+
i += 1;
|
|
230
|
+
}
|
|
229
231
|
continue;
|
|
230
232
|
}
|
|
231
233
|
if (item.startsWith(`${flag}=`)) {
|
|
@@ -342,7 +344,7 @@ function resolveUcodeLaunch({
|
|
|
342
344
|
const promptFile = String(
|
|
343
345
|
env.UFOO_UCODE_PROMPT_FILE
|
|
344
346
|
|| config.ucodePromptFile
|
|
345
|
-
||
|
|
347
|
+
|| ""
|
|
346
348
|
).trim();
|
|
347
349
|
const bootstrapFile = String(
|
|
348
350
|
env.UFOO_UCODE_BOOTSTRAP_FILE
|
|
@@ -3,7 +3,6 @@ const path = require("path");
|
|
|
3
3
|
const { loadConfig } = require("../../config");
|
|
4
4
|
const {
|
|
5
5
|
resolveNativeFallbackCommand,
|
|
6
|
-
defaultBundledPromptFile,
|
|
7
6
|
} = require("./ucode");
|
|
8
7
|
const { inspectUcodeBuildSetup } = require("./ucodeBuild");
|
|
9
8
|
const { inspectUcodeRuntimeConfig } = require("./ucodeRuntimeConfig");
|
|
@@ -30,7 +29,7 @@ function inspectUcodeSetup({
|
|
|
30
29
|
const promptFile = String(
|
|
31
30
|
env.UFOO_UCODE_PROMPT_FILE
|
|
32
31
|
|| config.ucodePromptFile
|
|
33
|
-
||
|
|
32
|
+
|| ""
|
|
34
33
|
).trim();
|
|
35
34
|
const bootstrapFile = String(
|
|
36
35
|
env.UFOO_UCODE_BOOTSTRAP_FILE
|
|
@@ -130,7 +129,7 @@ function formatUcodeDoctor(result = {}) {
|
|
|
130
129
|
if (result.configuredCommand) {
|
|
131
130
|
lines.push(`configured command override (ignored in native-only mode): ${result.configuredCommand}`);
|
|
132
131
|
}
|
|
133
|
-
lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptExists ? "" : "
|
|
132
|
+
lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptFile && !result.promptExists ? " (missing)" : ""}`);
|
|
134
133
|
lines.push(`bootstrap: ${result.bootstrapFile || "(none)"}`);
|
|
135
134
|
if (result.build && result.build.coreRoot) {
|
|
136
135
|
lines.push(`build: ${result.build.distCliExists ? "ready" : "missing dist"}`);
|
|
@@ -170,6 +169,10 @@ function prepareAndInspectUcode({
|
|
|
170
169
|
projectRoot: inspection.projectRoot,
|
|
171
170
|
promptFile: inspection.promptFile,
|
|
172
171
|
targetFile: inspection.bootstrapFile,
|
|
172
|
+
// The native core already injects the ufoo protocol via the modular
|
|
173
|
+
// prompt (src/agents/prompts/native/ufoo.js); inlining it into the
|
|
174
|
+
// bootstrap file would append the same protocol text a second time.
|
|
175
|
+
includeDefaultProtocol: false,
|
|
173
176
|
});
|
|
174
177
|
return {
|
|
175
178
|
...inspection,
|
package/src/code/nativeRunner.js
CHANGED
|
@@ -14,6 +14,11 @@ const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
|
14
14
|
// for non-trivial tasks while still catching runaway loops. Override via env.
|
|
15
15
|
const DEFAULT_MAX_NATIVE_TOOL_CALLS = 100;
|
|
16
16
|
const DEFAULT_MAX_NATIVE_TOOL_ERRORS = 5;
|
|
17
|
+
// Anthropic Messages rejects max_tokens above the model's real cap (64K on
|
|
18
|
+
// current models), so the transports use different defaults. Override either
|
|
19
|
+
// via UFOO_UCODE_MAX_TOKENS (positive integer).
|
|
20
|
+
const DEFAULT_OPENAI_MAX_TOKENS = 131072;
|
|
21
|
+
const DEFAULT_ANTHROPIC_MAX_TOKENS = 64000;
|
|
17
22
|
|
|
18
23
|
function nowMs() {
|
|
19
24
|
return Date.now();
|
|
@@ -38,6 +43,10 @@ function resolveNativeToolBudget(env = process.env) {
|
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
function resolveMaxTokens(fallback) {
|
|
47
|
+
return normalizePositiveInt(process.env.UFOO_UCODE_MAX_TOKENS, fallback);
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
function enforceNativeToolBudget({
|
|
42
51
|
toolCallsExecuted = 0,
|
|
43
52
|
toolErrors = 0,
|
|
@@ -46,7 +55,7 @@ function enforceNativeToolBudget({
|
|
|
46
55
|
lastTool = "",
|
|
47
56
|
lastError = "",
|
|
48
57
|
} = {}) {
|
|
49
|
-
if (toolCallsExecuted
|
|
58
|
+
if (toolCallsExecuted >= maxToolCalls) {
|
|
50
59
|
throw new Error(`tool call budget exceeded (${maxToolCalls})`);
|
|
51
60
|
}
|
|
52
61
|
if (toolErrors >= maxToolErrors) {
|
|
@@ -534,7 +543,7 @@ async function runOpenAiLikeTurn({
|
|
|
534
543
|
} = {}) {
|
|
535
544
|
const payload = {
|
|
536
545
|
model,
|
|
537
|
-
max_tokens:
|
|
546
|
+
max_tokens: resolveMaxTokens(DEFAULT_OPENAI_MAX_TOKENS),
|
|
538
547
|
messages,
|
|
539
548
|
tools: buildCoreToolSpecs(),
|
|
540
549
|
tool_choice: "auto",
|
|
@@ -588,6 +597,8 @@ async function runOpenAiLikeTurn({
|
|
|
588
597
|
let rawBuffer = "";
|
|
589
598
|
let responseText = "";
|
|
590
599
|
const announcedToolNames = new Set();
|
|
600
|
+
let nextSyntheticIndex = 0;
|
|
601
|
+
let lastSyntheticIndex = -1;
|
|
591
602
|
|
|
592
603
|
while (true) {
|
|
593
604
|
const { done, value } = await reader.read();
|
|
@@ -633,7 +644,23 @@ async function runOpenAiLikeTurn({
|
|
|
633
644
|
|
|
634
645
|
if (Array.isArray(delta.tool_calls)) {
|
|
635
646
|
for (const callPart of delta.tool_calls) {
|
|
636
|
-
|
|
647
|
+
let index;
|
|
648
|
+
if (Number.isFinite(callPart.index)) {
|
|
649
|
+
index = callPart.index;
|
|
650
|
+
} else if (typeof callPart.id === "string" && callPart.id) {
|
|
651
|
+
// Provider omitted index: a chunk carrying an id starts a new
|
|
652
|
+
// call, so give it its own synthetic index instead of
|
|
653
|
+
// collapsing every call into slot 0.
|
|
654
|
+
while (toolCallMap.has(nextSyntheticIndex)) nextSyntheticIndex += 1;
|
|
655
|
+
index = nextSyntheticIndex;
|
|
656
|
+
nextSyntheticIndex += 1;
|
|
657
|
+
lastSyntheticIndex = index;
|
|
658
|
+
} else if (lastSyntheticIndex >= 0) {
|
|
659
|
+
// No index and no id: continuation of the latest synthetic call.
|
|
660
|
+
index = lastSyntheticIndex;
|
|
661
|
+
} else {
|
|
662
|
+
index = 0;
|
|
663
|
+
}
|
|
637
664
|
const previous = toolCallMap.get(index) || {
|
|
638
665
|
id: "",
|
|
639
666
|
type: "function",
|
|
@@ -755,7 +782,7 @@ async function runAnthropicTurn({
|
|
|
755
782
|
} = {}) {
|
|
756
783
|
const payload = {
|
|
757
784
|
model,
|
|
758
|
-
max_tokens:
|
|
785
|
+
max_tokens: resolveMaxTokens(DEFAULT_ANTHROPIC_MAX_TOKENS),
|
|
759
786
|
messages,
|
|
760
787
|
tools: buildAnthropicToolSpecs(),
|
|
761
788
|
stream: true,
|
|
@@ -280,6 +280,15 @@ function compileSummary(results) {
|
|
|
280
280
|
return summaryParts.join("\n\n");
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Quote a value for safe inclusion in a shell command (single-quote style).
|
|
285
|
+
* Kept local to avoid a circular dependency with agent.js.
|
|
286
|
+
*/
|
|
287
|
+
function shellQuote(value = "") {
|
|
288
|
+
const text = String(value == null ? "" : value);
|
|
289
|
+
return `'${text.replace(/'/g, `'\"'\"'`)}'`;
|
|
290
|
+
}
|
|
291
|
+
|
|
283
292
|
/**
|
|
284
293
|
* Create a progress reporter that sends updates via bus
|
|
285
294
|
*/
|
|
@@ -297,10 +306,10 @@ function createBusProgressReporter(shell, publisher) {
|
|
|
297
306
|
|
|
298
307
|
if (progress.type === "step_start") {
|
|
299
308
|
const message = `⏳ ${progress.name} (${progress.current}/${progress.total})`;
|
|
300
|
-
shell(`ufoo bus send ${publisher} ${JSON.stringify(message)}`);
|
|
309
|
+
shell(`ufoo bus send ${shellQuote(publisher)} ${shellQuote(JSON.stringify(message))}`);
|
|
301
310
|
} else if (progress.type === "step_complete" && progress.success) {
|
|
302
311
|
const message = `✅ ${progress.name} completed`;
|
|
303
|
-
shell(`ufoo bus send ${publisher} ${JSON.stringify(message)}`);
|
|
312
|
+
shell(`ufoo bus send ${shellQuote(publisher)} ${shellQuote(JSON.stringify(message))}`);
|
|
304
313
|
}
|
|
305
314
|
};
|
|
306
315
|
}
|
package/src/code/tools/bash.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { spawnSync } = require("child_process");
|
|
2
2
|
const { normalizeWorkspaceRoot } = require("./common");
|
|
3
3
|
|
|
4
|
+
const MAX_TIMEOUT_MS = 600000;
|
|
5
|
+
|
|
4
6
|
function runBashTool(input = {}, options = {}) {
|
|
5
7
|
try {
|
|
6
8
|
const command = String(input.command || "").trim();
|
|
@@ -11,7 +13,9 @@ function runBashTool(input = {}, options = {}) {
|
|
|
11
13
|
};
|
|
12
14
|
}
|
|
13
15
|
const workspaceRoot = normalizeWorkspaceRoot(options.workspaceRoot, options.cwd);
|
|
14
|
-
const timeoutMs = Number.isFinite(input.timeoutMs)
|
|
16
|
+
const timeoutMs = Number.isFinite(input.timeoutMs)
|
|
17
|
+
? Math.min(MAX_TIMEOUT_MS, Math.max(100, Math.floor(input.timeoutMs)))
|
|
18
|
+
: 60000;
|
|
15
19
|
const result = spawnSync(command, {
|
|
16
20
|
cwd: workspaceRoot,
|
|
17
21
|
shell: true,
|
|
@@ -25,13 +29,26 @@ function runBashTool(input = {}, options = {}) {
|
|
|
25
29
|
ok: false,
|
|
26
30
|
workspaceRoot,
|
|
27
31
|
code: typeof result.status === "number" ? result.status : -1,
|
|
32
|
+
signal: result.signal || "",
|
|
28
33
|
stdout: String(result.stdout || ""),
|
|
29
34
|
stderr: String(result.stderr || ""),
|
|
30
35
|
error: result.error.message || "bash failed",
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
if (typeof result.status !== "number") {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
workspaceRoot,
|
|
43
|
+
code: -1,
|
|
44
|
+
signal: result.signal || "",
|
|
45
|
+
stdout: String(result.stdout || ""),
|
|
46
|
+
stderr: String(result.stderr || ""),
|
|
47
|
+
error: `command killed by signal ${result.signal || "unknown"}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const code = result.status;
|
|
35
52
|
return {
|
|
36
53
|
ok: code === 0,
|
|
37
54
|
workspaceRoot,
|
package/src/code/tools/read.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const { resolveWorkspacePath } = require("./common");
|
|
3
3
|
|
|
4
|
+
const MAX_FULL_READ_BYTES = 4 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
function readFileBounded(resolved) {
|
|
7
|
+
const stat = fs.statSync(resolved);
|
|
8
|
+
if (stat.size <= MAX_FULL_READ_BYTES) {
|
|
9
|
+
return { raw: fs.readFileSync(resolved, "utf8"), partial: false };
|
|
10
|
+
}
|
|
11
|
+
const fd = fs.openSync(resolved, "r");
|
|
12
|
+
try {
|
|
13
|
+
const buffer = Buffer.alloc(MAX_FULL_READ_BYTES);
|
|
14
|
+
const bytesRead = fs.readSync(fd, buffer, 0, MAX_FULL_READ_BYTES, 0);
|
|
15
|
+
return { raw: buffer.slice(0, bytesRead).toString("utf8"), partial: true };
|
|
16
|
+
} finally {
|
|
17
|
+
fs.closeSync(fd);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
function runReadTool(input = {}, options = {}) {
|
|
5
22
|
try {
|
|
6
23
|
const filePath = String(input.path || input.file || "").trim();
|
|
@@ -9,13 +26,13 @@ function runReadTool(input = {}, options = {}) {
|
|
|
9
26
|
const endLine = Number.isFinite(input.endLine) ? Math.max(startLine, Math.floor(input.endLine)) : 0;
|
|
10
27
|
const maxBytes = Number.isFinite(input.maxBytes) ? Math.max(256, Math.floor(input.maxBytes)) : 200000;
|
|
11
28
|
|
|
12
|
-
const raw =
|
|
29
|
+
const { raw, partial } = readFileBounded(resolved);
|
|
13
30
|
const lines = raw.split(/\r?\n/);
|
|
14
31
|
const from = startLine - 1;
|
|
15
32
|
const to = endLine > 0 ? endLine : lines.length;
|
|
16
33
|
const selected = lines.slice(from, to);
|
|
17
34
|
let content = selected.join("\n");
|
|
18
|
-
let truncated =
|
|
35
|
+
let truncated = partial;
|
|
19
36
|
if (Buffer.byteLength(content, "utf8") > maxBytes) {
|
|
20
37
|
content = Buffer.from(content, "utf8").slice(0, maxBytes).toString("utf8");
|
|
21
38
|
truncated = true;
|
|
@@ -41,4 +58,5 @@ function runReadTool(input = {}, options = {}) {
|
|
|
41
58
|
|
|
42
59
|
module.exports = {
|
|
43
60
|
runReadTool,
|
|
61
|
+
MAX_FULL_READ_BYTES,
|
|
44
62
|
};
|